一、定义
- MapReduce是一个分布式运算程序的编程框架,是用户开发“基于Hadoop的数据分析应用”的核心框架。
- MapReduce核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个Hadoop集群上
二、优缺点
优点
-
MapReduce易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的PC机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一样的。就是因为这个特点使得MapReduce编程变得非常流行。 -
良好的扩展性
当计算资源得不到满足时,可以通过简单的增加机器来扩展计算能力 -
高容错性
一个集群中的一台机器挂了,它可以把上面的计算任务转移到另一个节点上运行,不至于这个任务运行失败,这个过程不需要人工参与,完全由Hadoop内部自己完成。 -
适合PB级以上的海量数据的离线处理
可以实现上千台集群并发工作,提供数据处理能力
缺点
- 不擅长实时计算
MapReduce无法像MySQL一样在毫秒或者秒级类返回结果 - 不擅长流式计算
流式计算输入的数据是动态的,而MapReduce的输入数据集是静态的,不能动态变化。这是因为MapReduce的自身设计特点决定了数据源必须是静态的 - 不擅长DAG(有向图)计算
多个应用程序存在依赖关系,后一个应用的输入为前一个应用的输出。在这种情况下MapReduce并不是不可以做,而是使用后每一个MapReduce作业的输出结果都会写入到磁盘,会造成大量的IO,导致性能低下
三、MapReduce的核心思想
以WordCount为例:
- 分布式的运算程序往往需要分成至少2个阶段。
- 第一个阶段的MapTask并发实例,完全并行运行,互不相干。
- 第二个阶段的ReduceTask并发实例互不相干,但是他们的数据依赖于上一个阶段的所有MapTask并发实例的输出。
- MapReduce编程模型只能包含一个Map阶段和一个Reduce阶段,如果用户的业务逻辑非常复杂,那就只能多个MapReduce程序,串行运行。
四、MapReduce进程
一个完整的MapReduce程序在分布式运行时有三个示例进程:
- MrAppMaster:负责整个程序的过程调度机状态协调
- MapTask:负责Map阶段的整个数据处理流程
- ReduceTask:负责Reduce阶段整个数据处理流程
五、常用的序列化类型
Java类型 | Hadoop Writable类型 |
---|---|
boolean | BooleanWritable |
byte | ByteWritable |
int | IntWritable |
float | FloatWritable |
long | LongWritable |
double | DoubleWritable |
String | Text |
map | MapWritable |
array | ArrayWritable |
六、WordCount案例
-
新建一个Maven工程,pom.xml文件添加:
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.8.2</version> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-common</artifactId> <version>2.7.2</version> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-client</artifactId> <version>2.7.2</version> </dependency> <dependency> <groupId>org.apache.hadoop</groupId> <artifactId>hadoop-hdfs</artifactId> <version>2.7.2</version> </dependency> </dependencies>
词频统计的源文件:
atguigu atguigu
ss ss
cls cls
jiao
banzhang
xue
hadoop -
编写Mapper阶段代码
代码规范:- 自定义Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式(KV类型可自定义)
- 重写map()方法,实现自己的业务逻辑
- Mapper的输出数据是KV对 的形式(KV类型可以自定义)
- map()方法(MapTask)对每一个<K,V>调用一次
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Mapper; import java.io.IOException; public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> { private Text k = new Text(); private IntWritable v = new IntWritable(1); @Override protected void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { // 1.获取一行数据 String line = value.toString(); // 2.切割 String[] words = line.split(" "); // 3.输出 for (String word : words) { k.set(word); context.write(k, v); } } }
说明:Mapper<LongWritable, Text, Text, IntWritable>
- LongWritable, Text是输入的KV;LongWritable代表行偏移量,Text代码输入的数据,以源文件的前两行内容为例:第一行输入是:<0, atguigu atguigu>,第二行输入是:<60, ss ss>
- Text, IntWritable是输出的KV;Text代表输出的K,第一行的输出是:<atguigu, 1>,<atguigu, 1>,第二行的输出是<ss, 1>,<ss, 1>
-
编写Reduce阶段代码
代码规范:- 自定义Reducer继承父类
- Reducer的输入数据对应Mapper阶段的输出数据
- 自定义Reducer重写reduce()方法,业务逻辑在reduce()中。
- ReduceTask进程对每一组相同K的<K,V>调用一次reduce()方法
import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Reducer; import java.io.IOException; public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> { private IntWritable v = new IntWritable(); @Override protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { // 1.累计求和 int sum = 0; for (IntWritable count : values) { sum += count.get(); } v.set(sum); // 2.输出 context.write(key, v); } }
说明:Reducer<Text, IntWritable, Text, IntWritable>
- 第一个Text, IntWritable是代码Mapper的输出结果
- 第二个Text, IntWritable,代表经过Reduce最终要输出的结果
以一组<ss, 1>为例,经过Mapper之后,reduce(Text key, Iterable values, Context context) 里面的key是一组相同的key,也就是ss,values就是[1,1],经过转换后输出的是:<ss,2>这样就完成了词频统计
-
编写Driver代码
Driver相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是封装了MapReduce程序相关运行参数的job对象import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.lib.input.FileInputFormat; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import java.io.IOException; public class WordCountDriver { public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException { // 1. 获取配置信息以及封装任务 Configuration configuration = new Configuration(); Job job = Job.getInstance(configuration); // 2.设置jar加载路径 job.setJarByClass(WordCountReducer.class); // 3.设置map类与reduce类 job.setMapperClass(WordCountMapper.class); job.setReducerClass(WordCountReducer.class); // 4.设置map输出 job.setMapOutputKeyClass(Text.class); job.setMapOutputValueClass(IntWritable.class); // 5.设置最终输出kv类型 job.setOutputKeyClass(Text.class); job.setOutputValueClass(IntWritable.class); // 6.设置输入和输出路径 FileInputFormat.setInputPaths(job, new Path(args[0])); FileOutputFormat.setOutputPath(job, new Path(args[1])); // 7.提交 boolean b = job.waitForCompletion(true); System.exit( b ? 0 : 1); } }
本地执行测试:
以idea为例:配置运行时的路径
还可以使用maven打包到集群上运行。
七、自定义序列化
7.1 序列化概述
什么是序列化?
序列化就是把内存中的对象转换成字节序列以便存储到磁盘(持久化)和网络传输。
反序列化就是字节序列转(磁盘持久化的数据)换成内存中的对象。
为什么需要序列化?
序列化可以存储“活”的对象,也可以将“活”的对象发送到远程计算机
Hadoop为什么不用Java序列化机制?
Java的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很对额外的信息(各种信息校验,Header,继承体现等),不便于在网络中高效的传输,所以Hadoop开发了自己的序列化机制(Writable)。
Hadoop序列化的特点:
- 紧凑:高效使用存储空间
- 快速:读写数据的额外开销小
- 可扩展性:随着通信协议的升级而升级
- 互操作性:支持多语言交互
7.2 自定义bean对象序列化接口
步骤:
-
实现Writable接口
-
要有一个空参构造器(反序列化时,需要反射调用空构造函数)
-
重新序列化方法:write(DataOutput out)
-
重写反序列化方法:readFields(DataInput in)
-
重写toString方法,修改为想要的结果
说明:
- 注意序列化的饭序列化的顺序必须保持一致
- 如果需要将自定义的bean放在key中传输,则还需要实现Comparable接口(MapReduce框架汇中Shuffle过程要求对key并行能进行排序)
7.3 统计流量为例
以统计每一个手机号耗费的总上行流量、下行流量、总流量为例:
数据如下:
1 13736230513 192.196.100.1 www.atguigu.com 2481 24681 200
2 13846544121 192.196.100.2 264 0 200
3 13956435636 192.196.100.3 132 1512 200
4 13966251146 192.168.100.1 240 0 404
5 18271575951 192.168.100.2 www.atguigu.com 1527 2106 200
6 84188413 192.168.100.3 www.atguigu.com 4116 1432 200
7 13590439668 192.168.100.4 1116 954 200
8 15910133277 192.168.100.5 www.hao123.com 3156 2936 200
9 13729199489 192.168.100.6 240 0 200
10 13630577991 192.168.100.7 www.shouhu.com 6960 690 200
11 15043685818 192.168.100.8 www.baidu.com 3659 3538 200
12 15959002129 192.168.100.9 www.atguigu.com 1938 180 500
13 13560439638 192.168.100.10 918 4938 200
14 13470253144 192.168.100.11 180 180 200
15 13682846555 192.168.100.12 www.qq.com 1938 2910 200
16 13992314666 192.168.100.13 www.gaga.com 3008 3720 200
17 13509468723 192.168.100.14 www.qinghua.com 7335 110349 404
18 18390173782 192.168.100.15 www.sogou.com 9531 2412 200
19 13975057813 192.168.100.16 www.baidu.com 11058 48243 200
20 13768778790 192.168.100.17 120 120 200
21 13568436656 192.168.100.18 www.alibaba.com 2481 24681 200
22 13568436656 192.168.100.19 1116 954 200
数据含义:
7 13560436666 120.196.100.99 1116 954 200
id 手机号码 网络ip 上行流量 下行流量 网络状态码
想要的结果格式:
13560436666 1116 954 2070
手机号码 上行流量 下行流量 总流量
需求分析:
- 需求:统计每个手机号耗费的总的上行流量、下行流量、总流量
- 期望输出格式:手机号 上行流量 下行流量 总流量
- 自定义bean序列化机制,包含上行流量、下行流量、总流量
- Map阶段
- 读取一行数据,将字段进行切分
- 抽取出手机号,上行流量,下行流量
- 以手机号为key,bean对象为value输出
- Reduce阶段
1. 累加每个手机号的上行流量于下行流量得到总流量
代码如下:
自定义bean对象
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class FlowBean implements Writable {
private Long upFlow;
private Long downFlow;
private Long sumFow;
public FlowBean(Long upFlow, Long downFlow) {
this.upFlow = upFlow;
this.downFlow = downFlow;
}
public FlowBean() {
}
// 序列化
@Override
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFow);
}
// 反序列化(要求于序列化顺序一直)
@Override
public void readFields(DataInput in) throws IOException {
this.upFlow = in.readLong();
this.downFlow = in.readLong();
this.sumFow = in.readLong();
}
// 省略了get于set
// 根据上行流量于下行流量得到总流量
public void set(Long upFlow, Long downFlow){
this.upFlow = upFlow;
this.downFlow = downFlow;
this.sumFow = upFlow + downFlow;
}
// 重新定义输出格式
@Override
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFow;
}
}
编写Map阶段代码:
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowCountMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
Text k = new Text();
FlowBean v = new FlowBean();
@Override
protected void map(LongWritable key, Text values, Context context) throws IOException, InterruptedException {
// 1.读一行数据
String line = values.toString();
// 2.分割数据
String[] fields = line.split("\t");
// 3.封装对象
String phoneNum = fields[1];
// 3.1获取上行流量
long upFlow = Long.parseLong(fields[fields.length - 3]);
long downFlow = Long.parseLong(fields[fields.length - 2]);
k.set(phoneNum);
v.set(upFlow, downFlow);
// 4.写数据
context.write(k, v);
}
}
编写Reduce阶段代码
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowCountReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
FlowBean flowBean = new FlowBean();
@Override
protected void reduce(Text key, Iterable<FlowBean> values, Context context) throws IOException, InterruptedException {
// 1.遍历将上行流量与下行流量分别相加
long sumUpFlow = 0;
long sumDownFlow = 0;
for (FlowBean flowBean : values) {
sumUpFlow += flowBean.getUpFlow();
sumDownFlow += flowBean.getDownFlow();
}
// 2.封装对象
flowBean.set(sumUpFlow, sumDownFlow);
// 3.写出
context.write(key, flowBean);
}
}
编写Driver代码:
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class FlowsumDriver {
public static void main(String[] args) throws IOException, ClassNotFoundException, InterruptedException {
args = new String[]{"e:/input/flow.txt", "e:/output/"};
// 1.获取job实例与配置信息
Job job = Job.getInstance(new Configuration());
// 2.指定jar路径
job.setJarByClass(FlowsumDriver.class);
// 3.设置map与reduce
job.setMapperClass(FlowCountMapper.class);
job.setReducerClass(FlowCountReducer.class);
// 4.设置map的输出k和v
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
// 5.设置最终输出的kv类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
// 6.指定输入输出目录
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 7.运行
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
运行得到的结果:
打开最后一个文件:
13470253144 180 180 360
13509468723 7335 110349 117684
13560439638 918 4938 5856
13568436656 3597 25635 29232
13590439668 1116 954 2070
13630577991 6960 690 7650
13682846555 1938 2910 4848
13729199489 240 0 240
13736230513 2481 24681 27162
13768778790 120 120 240
13846544121 264 0 264
13956435636 132 1512 1644
13966251146 240 0 240
13975057813 11058 48243 59301
13992314666 3008 3720 6728
15043685818 3659 3538 7197
15910133277 3156 2936 6092
15959002129 1938 180 2118
18271575951 1527 2106 3633
18390173782 9531 2412 11943
84188413 4116 1432 5548