MapReduce
一、概述
1.1 、定义
MapReduce 是一个分布式运算程序的编程框架,是用户开发“基于 Hadoop 的数据分析应用”的核心框架。
MapReduce 核心功能是将用户编写的业务逻辑代码和自带默认组件整合成一个完整的分布式运算程序,并发运行在一个 Hadoop 集群上。
1.2、优缺点
1.2.1、优点
易于编程
它简单的实现一些接口,就可以完成一个分布式程序,这个分布式程序可以分布到大量廉价的 PC 机器上运行。也就是说你写一个分布式程序,跟写一个简单的串行程序是一模一 样的。就是因为这个特点使得 MapReduce 编程变得非常流行。
良好的扩展性
当你的计算资源不能得到满足的时候,你可以通过简单的增加机器来扩展它的计算能力。
高容错性
MapReduce 设计的初衷就是使程序能够部署在廉价的 PC 机器上,这就要求它具有很高的容错性。比如其中一台机器挂了,它可以把上面的计算任务转移到另外一个节点上运行, 不至于这个任务运行失败,而且这个过程不需要人工参与,而完全是由 Hadoop 内部完成的。
适合 PB 级以上海量数据的离线处理
可以实现上千台服务器集群并发工作,提供数据处理能力。
1.2.2、缺点
不擅长实时计算
MapReduce 无法像 MySQL 一样,在毫秒或者秒级内返回结果。
不擅长流式计算
流式计算的输入数据是动态的,而 MapReduce 的输入数据集是静态的,不能动态变化。这是因为 MapReduce 自身的设计特点决定了数据源必须是静态的。
)不擅长 DAG(有向无环图)计算
多个应用程序存在依赖关系,后一个应用程序的输入为前一个的输出。在这种情况下, MapReduce 并不是不能做,而是使用后,每个 MapReduce 作业的输出结果都会写入到磁盘, 会造成大量的磁盘 IO,导致性能非常的低下。
1.3、核心思想
- 分布式的运算程序往往需要分成至少 2 个阶段。
- 第一个阶段的 MapTask 并发实例,完全并行运行,互不相干。
- 第二个阶段的 ReduceTask 并发实例互不相干,但是他们的数据依赖于上一个阶段 的所有 MapTask 并发实例的输出。
- MapReduce 编程模型只能包含一个 Map 阶段和一个 Reduce 阶段,如果用户的业 务逻辑非常复杂,那就只能多个 MapReduce 程序,串行运行。
总结:分析 WordCount 数据流走向深入理解 MapReduce 核心思想。
Map阶段是切分操作,Reduce阶段是计算操作
==WordCount相当于就是HelloWorld程序一样==
1.4、MapReduce 进程
一个完整的 MapReduce 程序在分布式运行时有三类实例进程:
- MrAppMaster:负责整个程序的过程调度及状态协调。
- MapTask:负责 Map 阶段的整个数据处理流程。
- ReduceTask:负责 Reduce 阶段的整个数据处理流程。
1.5、官方 WordCount 源码
hadoop包里面由/share文件夹,用来存储官方的一些jar包,其中在/share/hadoop/mapreduce
下存了一些mapreduce有关的jar包:
其中 WordCount 就在hadoop-mapreduce-example-3.3.3.jar包中。我们将这个包传到Windows。然后反编译源码。
==采用反编译工具反编译源码,发现 WordCount 案例有 Map 类、Reduce 类和驱动类。且数据的类型是 Hadoop 自身封装的序列化类型。==
1.6、重用数据序列化类型
Java类型 | Hadoop Writable 类型 |
---|---|
Boolean | BooleanWritable |
Byte | ByteWritable |
Integer / int | IntWritable |
Float | FloatWritable |
Long | LongWritable |
Double | DoubleWritable |
String | Text |
Map | MapWritable |
Array | ArrayWritable |
Null | NullWritable |
除了标红的一组之外,都是在在原本的类型后面加Writable。
1.7、MapReduce 编程规范
1、Mapper
- 用户自定义的Mapper要继承自己的父类
- Mapper的输入数据是KV对的形式(KV的类型可自定义)
- Mapper中的业务逻辑写在map()方法中
- Mapper的输出数据是KV对的形式(KV的类型可自定义)
- map()方法(MapTask进程)对每一个
调用一次
2、Reducer
- 用户自定义的Reducer要继承之际的父类
- Reducer的输入数据对应Mapper的输出数据类型,也是KV
- Reducer的业务逻辑写在reduce()方法中
- ReduceTask进程对每一组相同k的组调用一次reduce()方法
3、Driver
相当于YARN集群的客户端,用于提交我们整个程序到YARN集群,提交的是:封装了MapReduce程序相关运行参数的==job对象==
1.8、WordCount案例实操
本地测试
需求
在给定的文本文件中统计输出每一个单词出现的总次数
需求分析
按照 MapReduce 编程规范,分别编写 Mapper,Reducer,Driver。
环境准备
就是创建maven工程,然后引入hadoop依赖,和HDFS的API操作中的一样。
编写程序
Mapper
第一个是hadoop2.x和hadoop3.x需要继承的类。第二个是hadoop1.x的。
1
2
3public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
}注意Mapper中的类型都是包org.apache.hadoop.io中的包
此时的import:
1
2
3
4import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import 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;
/**
* 输入:
* KEY : LongWritable // 第几行
* VALUE : Text // 该行的内容
* 输出:
* KEY : Text // 被统计的单词
* VALUE : IntWritable // 该单词的数量
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private Text outKEY = new Text();
private IntWritable outVALUE = new IntWritable(1);
/**
* 重写map方法
* @param key 输入数据的KEY
* @param value 输入数据的VALUE
* @param context 充当上下文,进行map和reduce之间的联络
*/
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
// 1.获取一行 zyr zyr zyr
String line = value.toString();
// 2.切割 [zyr,zyr,zyr]
String[] words = line.split(" ");
// 3.循环写出
for (String word : words) {
// 封装outKEY
outKEY.set(word);
// 写出
context.write(outKEY,outVALUE);
}
}
}Reducer
导包规则和Mapper中的一样,不要导错了。
完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45package com.zhang.mapreduce.wordcount;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* 这里和Mapper中的输入输出刚好相反
* 输入:
* KEY : Text // 被统计的单词
* VALUE : IntWritable // 该单词的数量
* 输出:
* KEY : Text // 被统计的单词
* VALUE : IntWritable // 该单词的数量
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable outValue = new IntWritable();
/**
* 重写reduce
* @param key 输入的key
* @param values 输入的value,是一个可迭代对象(不是迭代器,它时一个集合,如果想要遍历它,需要先用value.iterator()得到一个迭代器,或者增强for循环,集合的遍历方式都可)
* 例如zyr zyr zyr,被mapper弄成了三个,所以传入的K-V实际上是三组 zyr-1。但是Reducer在接收数据时会转化为:zyr-(1,1,1)
* @param context 上下文作用
*/
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
int sum = 0;
// 累加 zyr-(1,1,1)
for (IntWritable value : values) {
sum += value.get();
}
outValue.set(sum);
// 写出
context.write(key,outValue);
}
}Driver
写Driver类有固定套路,==最需要注意的是导包(一般就是hadoop中的包,然后两个一样的选长的)==:
- 获取配置信息以及获取 job 对象
- 关联本 Driver 程序的 jar
- 关联 Mapper 和 Reducer 的 jar
- 设置 Mapper 输出的 kv 类型
- 设置最终输出 kv 类型
- 设置输入和输出路径
提交 job
完整代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import 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, InterruptedException, ClassNotFoundException {
// 1. 获取配置信息以及获取 job 对象
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
// 2. 关联本 Driver 程序的 jar
job.setJarByClass(WordCountDriver.class);
// 3. 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(WordCountMapper.class); // 关联 Mapper
job.setReducerClass(WordCountReducer.class); // 关联 Reducer
// 4. 设置 Mapper 输出的 kv 类型 ,注意有Map,没有map的是第5步调用的方法
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5. 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6. 设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\hello.txt"));
FileOutputFormat.setOutputPath(job,new Path("D:\\桌面\\hello_output"));
// 7. 提交 job
boolean result = job.waitForCompletion(true);// 参数true表示需要获取job的工作信息,false不用
// 根据结果返回值,如果result == true(成功)返回0,如果失败返回1
System.exit(result ? 0 : 1);
}
}
本地测试
- 需要配置好HADOOP_HOME环境变量以及Windows运行依赖
- 在IDEA上运行程序(driver写成的一个main函数)
运行结果:
控制台输出很多数据,是因为第7步的时候的参数是true。但是如果不是false也会有很多输出,只是没有true的时候那么多
产生一个文件夹,里面有四个文件
前面两个是校验。_SUCCESS中啥也没有,第四个文件才是结果。
打开part-r-00000,查看结果
part表示部分(块)
r表示reducer处理结果,
00000是第几块(00000,00001,—-99999),这里数据小,只有00000
发现结果符合预期。
1.9、提交到集群测试
注意:
Driver类中:
1 | FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\hello.txt")); |
配死了文件的路径,我们需要改一下,不然集群上面没有,会报错。
改成:
1 | FileInputFormat.setInputPaths(job,new Path(args[0])); |
这样就可以像之前的hadoop自带的WordCount一样使用了。
用maven打jar包,需要添加的打包插件依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<build>
<plugins>
<!-- 下面配置表示把我们pom中的依赖一起打包进去-->
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>将程序打成 jar 包
可以看出带依赖的打包文件的内存都比较大。(这部分不会可以去学学Maven,Java相关开发一般用得比较多)
这里没有用到多余的依赖,Linux上都有,所以用不带依赖的jar包,改名为wc.jar,并拷贝到Hadoop集群的
/usr/local/hadoop
路径下启动Hadoop集群,创建测试文件夹和文件
先创建好输入输出的文件夹,并产生word.txt文件
执行WordCount程序
调用的时候要拷贝全类名,因为我们这个jar包中有两个Driver类
1
hadoop jar wc.jar com.zhang.mapreduce.wordcount2.WordCountDriver /input output/
效果:
yarn的任务资源调度页面也出现新的命令。
在NameNode web端中:
可以看到效果是正确的。
二、Hadoop序列化
2.1、概念
什么是序列化
序列化就是把内存中的对象,转换成字节序列(或其他数据传输协议)以便于(持久化)存储到磁盘和网络传输。
反序列化就是将收到字节序列(或其他数据传输协议)或者是磁盘的持久化数据,转换成内存中的对象。
序列化:内存=>磁盘 反序列化:磁盘=>内存
为什么要序列化
一般来说,“活的”对象只生存在内存里,关机断电就没有了。而且“活的”对象只能由本地的进程使用,不能被发送到网络上的另外一台计算机。 然而序列化可以存储“活的” 对象,可以将“活的”对象发送到远程计算机。
为什么不用 Java 的序列化
Java 的序列化是一个重量级序列化框架(Serializable),一个对象被序列化后,会附带很多额外的信息(各种校验信息,Header,继承体系等),不便于在网络中高效传输。所以, Hadoop 自己开发了一套序列化机制(Writable)。
Hadoop 序列化特点:
- 紧凑 :高效使用存储空间。
- 快速:读写数据的额外开销小。
- 互操作:支持多语言的交互
2.2、自定义bean对象实现序列化接口(Writable)
在企业开发中往往常用的基本序列化类型不能满足所有需求,比如在 Hadoop 框架内部传递一个 bean 对象,那么该对象就需要实现序列化接口。我们就要自定义这个接口。
具体步骤,分为七步:
必须实现(implements)Writable接口
反序列化时,需要反射调用空参构造函数,所以必须有空参构造函数
1
2
3public FlowBean() {
super();
}重写序列化方法
1
2
3
4
5
6
public void write(DataOutput out) throws IOException {
out.writeLong(upFlow);
out.writeLong(downFlow);
out.writeLong(sumFlow);
}重写反序列化方法
1
2
3
4
5
6
public void readFields(DataInput in) throws IOException {
upFlow = in.readLong();
downFlow = in.readLong();
sumFlow = in.readLong();
}注意反序列化的顺序和序列化的顺序完全一致
要想把结果显示在文件中,需要重写toString(),可用”\t”分开,方便后续使用。
如果需要自定义的bean放在key中传输,则还需要实现Comparable接口,因为MapReducer框架中的Shuffle过程要求对key必须能排序。(如果只在value中传输就可以不实现)
1
2
3
4
5
public int compareTo(FlowBean o) {
// 倒序排列,从大到小
return this.sumFlow > o.getSumFlow() ? -1 : 1;
}
2.3、序列化案例实操
需求
统计每一个手机号耗费的总上行流量、总下行流量、总流量
输入数据
phone_data.txt 为了便于切割,用的tab。注意有手机号重复的,应当一起加起来,有IP地址缺失的(上行和下行流量分开取数组倒数几个)。
输入数据格式
1
27 13560436666 120.196.100.99 1116 954 200
%id 手机号码 网络ip 上行流量 下行流量 网络状态码期望输出数据格式
1
213560436666 1116 954 2070
%手机号码 上行流量 下行流量 总流量
需求分析
总结:
Map阶段:
- 输入:
- key:行号 LongWritable
- value:一行的信息 Text
- 输出:
- key:手机号 Text
- value:bean对象 FlowBean
Reduce阶段
输入:
key:手机号 Text
value:bean对象 FlowBean
输出:
key:手机号 Text
value:bean对象 FlowBean
bean对象:具有以下属性:
- 手机号,以key的形式传播了,可有可无
- 上行流量
- 下行流量
- 总流量可以计算得到,在那里计算都一样。可有可无
编写 MapReduce 程序
编写流量统计的Bean对象——FlowBean
还是提醒导包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79package com.zhang.mapreduce.writable;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* 1. 实现writable接口
* 2. 重写序列化和反序列化方法
* 3. 重写空参构造
* 4. 重写toString方法
*/
public class FlowBean implements Writable {
// 属性
private Long upFlow; // 上行流量
private Long downFlow; // 下行流量
private Long sumFlow; // 总流量
// 空参构造
public FlowBean() {
}
/**
* 重写序列化方法
* @param dataOutput
*/
public void write(DataOutput dataOutput) throws IOException {
// 序列化的顺序无所谓,但是要保证序列化和反序列化的顺序一致
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
/**
* 重写反序列化方法
* @param dataInput
*/
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
// 重写toString()方法
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
// getter和setter
public Long getUpFlow() {
return upFlow;
}
public void setUpFlow(Long upFlow) {
this.upFlow = upFlow;
}
public Long getDownFlow() {
return downFlow;
}
public void setDownFlow(Long downFlow) {
this.downFlow = downFlow;
}
public Long getSumFlow() {
return sumFlow;
}
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
}编写Mapper类——FlowMapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowMapper extends Mapper<LongWritable, Text, Text, FlowBean> {
private Text outKey = new Text();
private FlowBean outValue = new FlowBean();
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
// 1. 获取一行信息
// 7 13560436666 120.196.100.99 1116 954 200
String line = value.toString();
// 2. 切割
// [7,13560436666,120.196.100.99,1116,954,200]
String[] split = line.split("\t");
// 3. 提取想要的数据
// 手机号:13560436666=>split[1],上行流量:1116=>split[len - 3],下行流量:954=>split[len - 2]
String phoneNum = split[1];
String up = split[split.length - 3];
String down = split[split.length - 2];
// 4. 封装输出Key-Value
outKey.set(phoneNum);
outValue.setUpFlow(Long.parseLong(up));
outValue.setDownFlow(Long.parseLong(down));
outValue.setSumFlow();
// 5.写出
context.write(outKey, outValue);
}
}编写Reducer类——FlowReducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<Text, FlowBean, Text, FlowBean> {
private FlowBean outValue = new FlowBean();
protected void reduce(Text key, Iterable<FlowBean> values, Reducer<Text, FlowBean, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
// 1. 遍历 values,将其中的上行流量,下行流量分别累加
// phoneNum - [bean1,bean2]
Long sumUp = 0L;
Long sumDown = 0L;
for (FlowBean value : values) {
sumUp += value.getUpFlow();
sumDown += value.getDownFlow();
}
// 2. 封装 outKV
outValue.setUpFlow(sumUp);
outValue.setDownFlow(sumDown);
outValue.setSumFlow();
// 3. 写出
context.write(key, outValue);
}
}编写Driver驱动类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42import 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 FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1. 获取配置信息以及获取 job 对象
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//2. 关联本 Driver 程序的 jar
job.setJarByClass(FlowDriver.class);
//3. 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4. 设置 Mapper 输出的 kv 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5. 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6. 设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\phone_data .txt"));
FileOutputFormat.setOutputPath(job,new Path("D:\\桌面\\output"));
//7. 提交 job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}测试结果
结果正确。
三、MapReduce 框架原理
3.1、InputFormat 数据输入
3.1.1 切片于MapTask并行度决定机制
问题引出
MapTask 的并行度决定 Map 阶段的任务处理并发度,进而影响到整个 Job 的处理速度。
思考:1G 的数据,启动 8 个 MapTask,可以提高集群的并发处理能力。那么 1K 的数 据,也启动 8 个 MapTask,会提高集群性能吗?MapTask 并行任务是否越多越好呢?哪些因素影响了 MapTask 并行度?
MapTask 并行度决定机制
数据块:Block 是 HDFS 物理上把数据分成一块一块。数据块是 HDFS 存储数据单位。
数据切片:数据切片只是在逻辑上对输入进行分片,并不会在磁盘上将其切分成片进行存储。数据切片是 MapReduce 程序计算输入数据的单位,一个切片会对应启动一个 MapTask。
3.1.2、Job提交流程源码详解
1 | // 自己代码中调用的函数 |
3.1.3、 FileInputFormat 切片源码和切片机制
FileInputFormat 切片源码解析(input.getSplits(job))
程序先找到你数据存储的目录。
开始遍历处理(规划切片)目录下的每一个文件
- 遍历第一个文件ss.txt
- 获取文件大小fs.sizeOf(ss.txt)
- 计算切片大小 computeSplitSize(Math.max(minSize,Math.min(maxSize,blocksize)))=blocksize=128M
- 默认情况下,切片大小=blocksize
- 开始切,形成第1个切片:ss.txt—0:128M 第2个切片ss.txt—128:256M 第3个切片ss.txt—256M:300M (每次切片时,都要判断切完剩下的部分是否大于块的==1.1倍==,不大于1.1倍就划分一块切片)
- 将切片信息写到一个切片规划文件中 f)整个切片的核心过程在getSplit()方法中完成
- InputSplit只记录了切片的元数据信息,比如起始位置、长度以及所在的节点列表等。
- 提交切片规划文件到YARN上,YARN上的MrAppMaster就可以根据切片规划文件计算开启MapTask个数
FileInputFormat 切片机制
切片机制
- 简单地按照文件的内容长度进行切片
- 切片大小,默认等于Block大小
- 切片时不考虑数据集整体,而是逐个针对每一个文件单独切片
案例分析:
源码中计算切片大小的公式:
Math.max(minSize, Math.min(maxSize, blockSize))
mapreduce.input,fileformat.split.minsize=1
默认值为1mapreduce.input.fileinputformat.split.maxsize=Long.MAX_VALUE
默认值时Long的最大值==所以默认情况下,切片大小=blocksize==
切片大小设置
maxsize
(切片最大值):参数如果比blockSize小,则会让切片变小,而且就等于配置的这个参数的值minsize
(切片最小值):参数调的比blockSize大,则可以让切片变得比blockSize还大
获取切片信息的API
获取切片的文件名称:
1
String name = inputSplit.getPath().getName();
根据文件类型获取切片信息
1
FileSplit inputSplit = (FileSplit) context.getInputSplit();
3.1.4、TextInputFormat
思考:在运行 MapReduce 程序时,输入的文件格式包括:基于行的日志文件、二进制格式文件、数据库表等。那么,针对不同的数据类型,MapReduce 是如何读取这些数据的呢?
FileInputFormat 常见的接口实现类包括:TextInputFormat、KeyValueTextInputFormat、 NLineInputFormat、CombineTextInputFormat 和自定义 InputFormat 等。
TextInputFormat 是默认的 FileInputFormat 实现类。按行读取每条记录。键是存储该行在整个文件中的起始字节偏移量, LongWritable 类型。值是这行的内容,不包括任何行终止符(换行符和回车符),Text 类型。
以下是一个示例,比如,一个分片包含了如下 4 条文本记录
1 | Rich learning form |
每条记录表示为以下键/值对:
1 | (0,Rich learning form) |
3.1.5、CombineTextInputFormat 切片机制
框架默认的 TextInputFormat 切片机制是对任务按文件规划切片,不管文件多小,都会是一个单独的切片,都会交给一个 MapTask,这样如果有大量小文件,就会产生大量的 MapTask,处理效率极其低下。
==CombineTextInputFormat 切片机制就是解决大量小文件的办法之一==
应用场景
CombineTextInputFormat 用于小文件过多的场景,它可以将多个小文件从逻辑上规划到一个切片中,这样,多个小文件就可以交给一个 MapTask 处理。
虚拟存储切片最大值设置
1
CombineTextInputFormat.setMaxInputSplitSize(job, 4194304);// 4m
注意:虚拟存储切片最大值设置最好根据实际的小文件大小情况来设置具体的值。
切片机制
生成切片过程包括:虚拟存储过程和切片过程二部分。
虚拟存储过程:
将输入目录下所有文件大小,依次和设置的 setMaxInputSplitSize 值比较
- 如果不大于设置的最大值,逻辑上划分一个块。
- 如果输入文件大于设置的最大值且大于两倍, 那么以最大值切割一块;
当剩余数据大小超过设置的最大值且不大于最大值 2 倍,此时将文件均分成 2 个虚拟存储块(防止出现太小切片)。
例如 setMaxInputSplitSize 值为 4M,输入文件大小为 8.02M,则先逻辑上分成一个 4M。剩余的大小为 4.02M,如果按照 4M 逻辑划分,就会出现 0.02M 的小的虚拟存储 文件,所以将剩余的 4.02M 文件切分成(2.01M 和 2.01M)两个文件。
切片过程:
- 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值
- 大于等于则单独形成一个切片。
- 如果不大于则跟下一个虚拟存储文件进行合并,共同形成一个切片。
- 判断虚拟存储的文件大小是否大于 setMaxInputSplitSize 值
测试举例:
有 4 个小文件大小分别为 1.7M、5.1M、3.4M 以及 6.8M 这四个小文件。
则虚拟存储之后形成 6 个文件块,大小分别为:1.7M,(2.55M、2.55M),3.4M 以及(3.4M、3.4M)
最终会形成 3 个切片,大小分别为:(1.7+2.55)M,(2.55+3.4)M,(3.4+3.4)M
3.2、MapReduce 工作流程
上面的流程是整个 MapReduce 最全工作流程,但是 Shuffle 过程只是从第 7 步开始到第 16 步结束,具体 Shuffle 过程详解,如下:
- MapTask 收集我们的 map()方法输出的 kv 对,放到内存缓冲区中
- 从内存缓冲区不断溢出本地磁盘文件,可能会溢出多个文件
- 多个溢出文件会被合并成大的溢出文件
- 在溢出过程及合并的过程中,都要调用 Partitioner 进行分区和针对 key 进行排序
- ReduceTask 根据自己的分区号,去各个 MapTask 机器上取相应的结果分区数据
- ReduceTask 会抓取到同一个分区的来自不同 MapTask 的结果文件,ReduceTask 会将这些文件再进行合并(归并排序)
- 合并成大文件后,Shuffle 的过程也就结束了,后面进入 ReduceTask 的逻辑运算过 程(从文件中取出一个一个的键值对 Group,调用用户自定义的 reduce()方法)
注意:
- Shuffle 中的缓冲区大小会影响到 MapReduce 程序的执行效率,原则上说,缓冲区越大,磁盘 io 的次数越少,执行速度就越快。
- 缓冲区的大小可以通过参数调整,参数:mapreduce.task.io.sort.mb 默认 100M。
为什么排序,排序排的是什么?
我的思考:排序是拍的输出数据的key的顺序。我们之前做的WordCount的案例的结果就是按照单词(也就是输出数据的key)的字典序进行排序的。所以这里的快排和归并排序就是排输出数据的key的顺序
3.3、Shuffle 机制
3.3.1、Shuffle机制
Map方法之后,Reduce方法之前的数据处理过程称之为Shuffle(洗牌)。
3.3.2、Partition 分区
需求
要求将统计结果按照条件输出到不同文件中(分区)。
比如:将统计结果按照手机归属地不同省份输出到不同文件中(分区)
默认 Partitioner 分区
1
2
3
4
5public class HashPartitioner<K, V> extends Partitioner<K, V> {
public int getPartition(K key, V value, int numReduceTasks) {
return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks;
}
}默认分区是根据key的hashCode对ReduceTasks个数取模得到的。用户没法控制哪个key存储到哪个分区。
自定义 Partitioner 步骤
自定义继承Partitioner的类,重写getPartition()方法
1
2
3
4
5
6
7
8public class CustomPartitioner extends Partitioner<Text, FlowBean> {
public int getPartition(Text key, FlowBean value, int numPartitions) {
// 控制分区代码逻辑
… …
return partition;
}
}在Job驱动中,设置自定义Patitioner
1
job.setPartitionerClass(CustomPatitioner.class);
自定义Patition后,要根据自定义Patitioner的逻辑设置相应数量的ReduceTask
1
job.setNumReduceTasks(5);
分区总结
- 如果
ReduceTask的数量 > getPartition的结果数
,则会多产生几个空的输出文件part-r-000xx; - 如果
1 < ReduceTask的数量 < getPartition的结果数
,则有一部分分区数据无处安放,会Exception; - 如果
ReduceTask的数量 = 1
,则不管MapTask端输出多少个分区文件,最终结果都交给这一个
ReduceTask,最终也就只会产生一个结果文件 part-r-00000; 分区号必须从零开始,逐一累加
例如:假设自定义分区数为5,则
job.setNumReduceTasks(1); 小于5,会正常运行,只不过会产生一个输出文件
- job.setNumReduceTasks(2); 大于1,小于5,会报错
- job.setNumReduceTasks(6); 大于5,程序会正常运行,会产生空文件
- 如果
3.3.3、 Partition 分区案例实操
需求
将统计结果按照手机归属地不同省份输出到不同文件中(分区)
输入数据
phone_data.txt
期望输出数据
手机号136、137、138、139开头都分别放到一个独立的文件中,其他开头通通放到一个文件中(总共5个文件)
需求分析
在案例2.3的基础上修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class ProvincePartitioner extends Partitioner<Text,FlowBean> {
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
// text 是手机号
String phoneNum = text.toString();
// 获取前三位
String prePhoneNum = phoneNum.substring(0, 3);
// 分区号
int partition;
// 设置分区
if("136".equals(prePhoneNum)){
partition = 0;
} else if("137".equals(prePhoneNum)) {
partition = 1;
} else if("138".equals(prePhoneNum)) {
partition = 2;
} else if("139".equals(prePhoneNum)) {
partition = 3;
} else {
partition = 4;
}
// 返回分区号
return partition;
}
}再修改一下Driver类,添加8,9两步(在第5步后面)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48import 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 FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1. 获取配置信息以及获取 job 对象
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//2. 关联本 Driver 程序的 jar
job.setJarByClass(FlowDriver.class);
//3. 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4. 设置 Mapper 输出的 kv 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(FlowBean.class);
//5. 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//8. 指定自定义分区器
job.setPartitionerClass(ProvincePartitioner.class);
//9. 同时指定相应数量的 ReduceTask
job.setNumReduceTasks(5);
//6. 设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\phone_data .txt"));
FileOutputFormat.setOutputPath(job,new Path("D:\\桌面\\output"));
//7. 提交 job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}注意:NumReduceTasks的数量最好和分区数相同,效果最好。但是也可以是1(相当于白分区了(不设置的默认值也是1))或比5大(会多产生空文件)。但是大于1,小于5就会报错。原因看上一节。
结果
确实产生了五个分区文件。
内容:
3.3.4、WritableComparable 排序
排序是MapReduce框架中最重要的操作之一。
MapTask和ReduceTask均会对数据按照key进行排序。该操作属于Hadoop的默认行为。任何应用程序中的数据均会被排序,而不管逻辑上是否需要。
默认排序是按照字典顺序排序,且实现该排序的方法是快速排序。
对于MapTask,它会将处理的结果暂时放到环形缓冲区中,当环形缓冲区使用率达到一定阈值后,再对缓冲区中的数据进行一次快速排序,并将这些有序数据溢写到磁盘上,而当数据处理完毕后,它会对磁盘上所有文件进行归并排序。
对于ReduceTask,它从每个MapTask上远程拷贝相应的数据文件
- 如果文件大小超过一定阈值,则溢写磁盘上,否则存储在内存中。
- 如果磁盘上文件数目达到一定阈值,则进行一次归并排序以生成一个更大文件;
- 如果内存中文件大小或者数目超过一定阈值,则进行一次合并后将数据溢写到磁盘上。
- 当所有数据拷贝完毕后,ReduceTask统一对内存和磁盘上的所有数据进行一次归并排序。
排序分类
部分排序
MapReduce根据输入记录的键对数据集排序。保证输出的每个文件内部有序。
全排序
最终输出结果只有一个文件,且文件内部有序。实现方式是只设置一个ReduceTask。但该方法在处理大型文件时效率极低,因为一台机器处理所有文件,完全丧失了MapReduce所提供的并行架构。
辅助排序:(GroupingComparator 分组)
在Reduce端对key进行分组。应用于:在接收的key为bean对象时,想让一个或几个字段相同(全部字段比较不相同)的key进入到同一个reduce方法时,可以采用分组排序。
二次排序
在自定义排序过程中,如果compareTo中的判断条件为两个即为二次排序。
自定义排序 WritableComparable 原理分析
bean 对象做为 key 传输,需要实现
WritableComparable
接口重写compareTo
方法,就可以实现排序。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public int compareTo(FlowBean bean) {
int result;
// 按照总流量大小,倒序排列
if (this.sumFlow > bean.getSumFlow()) {
result = -1;
}else if (this.sumFlow < bean.getSumFlow()) {
result = 1;
}else {
result = 0;
}
return result;
}
3.3.5、WritableComparable 排序案例实操(全排序)
需求
根据案例2.3序列化案例产生的结果,再次对总流量进行倒序排序,如果总流量一样,就上行流量升序排序。
需求分析
Map阶段:
输入:
- key:LongWritable 行号
- value:Text 一行数据
输出:
- key:FlowBean 自定义Bean(要求可排序)
- value:Text 手机号
Reduce阶段:
输入:
- key:FlowBean 自定义Bean
- value:Text 手机号
输出:
- key:Text 手机号
- value:FlowBean 自定义Bean
代码实现
FlowBean 对象在在需求 1 基础上增加了比较功能
- 原本实现Writable接口,现在修改为实现WritableComparable接口(注意,这是一个接口,之前实现成了Writable和Comparable两个接口,就无法比较会报错),泛型是FlowBean(同类比较)
实现ompareTo方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96package com.zhang.mapreduce.writableComparable;
import org.apache.hadoop.io.WritableComparable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
/**
* 1. 实现writable接口
* 2. 重写序列化和反序列化方法
* 3. 重写空参构造
* 4. 重写toString方法
*/
public class FlowBean implements WritableComparable<FlowBean> {
private Long upFlow; // 上行流量
private Long downFlow; // 下行流量
private Long sumFlow; // 总流量
// 空参构造
public FlowBean() {
}
/**
* 重写序列化方法
* @param dataOutput
*/
public void write(DataOutput dataOutput) throws IOException {
// 序列化的顺序无所谓,但是要保证序列化和反序列化的顺序一致
dataOutput.writeLong(upFlow);
dataOutput.writeLong(downFlow);
dataOutput.writeLong(sumFlow);
}
/**
* 重写反序列化方法
* @param dataInput
*/
public void readFields(DataInput dataInput) throws IOException {
this.upFlow = dataInput.readLong();
this.downFlow = dataInput.readLong();
this.sumFlow = dataInput.readLong();
}
// 重写toString()方法
public String toString() {
return upFlow + "\t" + downFlow + "\t" + sumFlow;
}
public int compareTo(FlowBean o) {
// 总流量的倒序排序
if(this.sumFlow > o.sumFlow){
return -1;
} else if (this.sumFlow < o.sumFlow) {
return 1;
} else {
// 按上行流量正序排序
if(this.upFlow > o.upFlow){
return 1;
} else if ( this.upFlow < o.upFlow){
return -1;
} else {
return 0;
}
}
}
public Long getUpFlow() {
return upFlow;
}
public void setUpFlow(Long upFlow) {
this.upFlow = upFlow;
}
public Long getDownFlow() {
return downFlow;
}
public void setDownFlow(Long downFlow) {
this.downFlow = downFlow;
}
public Long getSumFlow() {
return sumFlow;
}
public void setSumFlow() {
this.sumFlow = this.upFlow + this.downFlow;
}
}
编写Mapper类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class FlowMapper extends Mapper<LongWritable, Text, FlowBean, Text> {
private FlowBean outKey = new FlowBean();
private Text outValue = new Text();
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, FlowBean, Text>.Context context)
throws IOException, InterruptedException {
// 获取一行 13315688577 4481 22681 27162
String line = value.toString();
// 切割 [13315688577,4481,22681,27162]
String[] split = line.split("\t");
// 封装
outKey.setUpFlow(Long.parseLong(split[1]));
outKey.setDownFlow(Long.parseLong(split[2]));
outKey.setSumFlow();
outValue.set(split[0]);
// 写出
context.write(outKey, outValue);
}
}编写Reduce类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class FlowReducer extends Reducer<FlowBean, Text, Text, FlowBean> {
protected void reduce(FlowBean key, Iterable<Text> values, Reducer<FlowBean, Text, Text, FlowBean>.Context context)
throws IOException, InterruptedException {
for (Text value : values) {
context.write(value, key);
}
}
}编写Driver类
- 修改Mapper输出的key和value
修改输入输出的路径(输入数据是之前的结果,要把其他三个文件删掉,否则会一起存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41import 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 FlowDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
//1. 获取配置信息以及获取 job 对象
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
//2. 关联本 Driver 程序的 jar
job.setJarByClass(FlowDriver.class);
//3. 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(FlowMapper.class);
job.setReducerClass(FlowReducer.class);
//4. 设置 Mapper 输出的 kv 类型
job.setMapOutputKeyClass(FlowBean.class);
job.setMapOutputValueClass(Text.class);
//5. 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(FlowBean.class);
//6. 设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\output"));
FileOutputFormat.setOutputPath(job,new Path("D:\\桌面\\output2"));
//7. 提交 job
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}
效果:
结果的确倒序排序了。
3.3.6、WritableComparable排序案例实操(区内排序)
需求
要求每个省份手机号输出的文件中按照总流量内部排序。
需求分析
基于前一个需求,增加自定义分区类,分区按照省份手机号设置
案例实操
添加自定义分区类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class ProvincePartitioner extends Partitioner<Text,FlowBean> {
public int getPartition(Text text, FlowBean flowBean, int numPartitions) {
// text 是手机号
String phoneNum = text.toString();
// 获取前三位
String prePhoneNum = phoneNum.substring(0, 3);
// 分区号
int partition;
// 设置分区
if("136".equals(prePhoneNum)){
partition = 0;
} else if("137".equals(prePhoneNum)) {
partition = 1;
} else if("138".equals(prePhoneNum)) {
partition = 2;
} else if("139".equals(prePhoneNum)) {
partition = 3;
} else {
partition = 4;
}
// 返回分区号
return partition;
}
}在驱动类中添加分区
1
2
3
4
5//8. 指定自定义分区器
job.setPartitionerClass(ProvincePartitioner.class);
//9. 同时指定相应数量的 ReduceTask
job.setNumReduceTasks(5);
效果
3.3.7、Combiner 合并
Combiner是MR程序中Mapper和Reducer之外的一种组件。
Combiner组件的父类就是Reducer。
Combiner和Reducer的区别在于运行的位置
- Combiner是在每一个MapTask所在的节点运行;
- Reducer是接收全局所有Mapper的输出结果;
Combiner的意义就是对每一个MapTask的输出进行局部汇总,以减小网络传输量。
Combiner能够应用的前提是不能影响最终的业务逻辑,而且,Combiner的输出kv应该跟Reducer的输入kv类型要对应起来。
自定义Combiner 实现步骤
自定义一个Combiner继承Reducer,重写Reduce方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14public class WordCountCombiner extends Reducer<Text, IntWritable, Text,IntWritable> {
private IntWritable outV = new IntWritable();
protected void reduce(Text key, Iterable<IntWritable> values, Contextcontext) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
outV.set(sum);
context.write(key,outV);
}
}在Job驱动类中设置:
1
job.setCombinerClass(WordCountCombiner.class);
3.3.8、Combiner合并案例实操
需求
统计过程中对每一个 MapTask 的输出进行局部汇总,以减小网络传输量即采用 Combiner 功能。
需求分析
案例实操——方案一
增加一个WordCountCombiner类继承Reducer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class WordCountCombiner extends Reducer<Text, IntWritable,Text, IntWritable> {
private IntWritable outValue = new IntWritable();
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
int sum = 0;
for (IntWritable value : values) {
sum += value.get();
}
outValue.set(sum);
context.write(key, outValue);
}
}在WordCountDriver驱动类中指定Combiner
1
job.setCombinerClass(WordCountCombiner.class);
案例实操——方案二
将WordCountReducer 作为 Combiner 在 WordCountDriver驱动类中指定
1
job.setCombinerClass(WordCountReducer.class);
3.4、OutPutFormat 数据输出
OutputFormat是MapReduce输出的基类,所有实现MapReduce输出都实现了 OutputFormat 接口。下面我们介绍几种常见的OutputFormat实现类。
3.4.1、OutputFormat 接口实现类
默认输出格式TextOutputFormat
自定义OutputFormat:
应用场景:
输出数据到MySQL/HBase/Elasticsearch等存储框架中。
自定义OutputFormat步骤
- 自定义一个类继承
FileOutPutFormat
- 改写
RecordWriter
,具体改写输出数据的方法write()
- 自定义一个类继承
3.4.2、自定义OutputFormat案例实操
需求
过滤输入的 log 日志,包含 study 的网站输出到 e:/study.log,不包含 study 的网站输出到 e:/other.log。
需求分析
案例实操
编写 LogMapper 类(Map阶段不做任何操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class LogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 在Map阶段不做任何处理
context.write(value, NullWritable.get());
}
}编写 LogReducer 类(Reduce阶段基本上也是不做任何操作)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
public class LogReducer extends Reducer<Text, NullWritable, Text, NullWritable> {
protected void reduce(Text key, Iterable<NullWritable> values, Reducer<Text, NullWritable, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 防止有相同数据,丢数据
for (NullWritable value : values) {
context.write(key, NullWritable.get());
}
}
}自定义一个 LogOutputFormat 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import java.io.IOException;
public class LogOutputFormat extends FileOutputFormat<Text, NullWritable> {
public RecordWriter<Text, NullWritable> getRecordWriter(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
LogRecordWriter logRecordWriter = new LogRecordWriter(taskAttemptContext);
return logRecordWriter;
}
}编写 LogRecordWriter 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import org.apache.commons.io.IOUtils;
import org.apache.hadoop.fs.FSDataOutputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.RecordWriter;
import org.apache.hadoop.mapreduce.TaskAttemptContext;
import java.io.IOException;
public class LogRecordWriter extends RecordWriter<Text, NullWritable> {
private FSDataOutputStream study;
private FSDataOutputStream other;
// 构造方法,参数是job,要利用job的配置对象创建文件系统
public LogRecordWriter(TaskAttemptContext job) {
// 创建两个流,一个流存喊study的数据,一个流存其他的不含study的数据
try {
FileSystem fileSystem = FileSystem.get(job.getConfiguration());
study = fileSystem.create(new Path("D:\\桌面\\study.log"));
other = fileSystem.create(new Path("D:\\桌面\\other.log"));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void write(Text text, NullWritable nullWritable) throws IOException, InterruptedException {
String log = text.toString();
// 具体写
if (log.contains("study")){
study.writeBytes(log + "\n");
} else {
other.writeBytes(log + "\n");
}
}
public void close(TaskAttemptContext taskAttemptContext) throws IOException, InterruptedException {
// 关流
IOUtils.close(study);
IOUtils.close(other);
}
}编写 LogDriver 类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 LogDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Configuration configuration = new Configuration();
Job job = Job.getInstance(configuration);
job.setJarByClass(LogDriver.class);
job.setMapperClass(LogMapper.class);
job.setReducerClass(LogReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
//设置自定义的 outputFormat
job.setOutputFormatClass(LogOutputFormat.class);
FileInputFormat.setInputPaths(job, new Path("D:\\桌面\\log.txt"));
// 虽然我们自定义了outputFormat,但是因为我们的outputFormat继承自fileOutputFormat而fileOutputFormat要输出一个_SUCCESS 文件,所以在这还得指定一个输出目录
FileOutputFormat.setOutputPath(job, new Path("D:\\桌面\\output"));
boolean result = job.waitForCompletion(true);
System.exit(result ? 0 : 1);
}
}效果
第二个指定FileOutputFormat实际上就是输出一个标记而已,指定的文件夹中只有success。
输出结果也满足预期。
3.5、MapReduce 内核源码解析
3.5.1、MapTask工作机制
- Read 阶段:MapTask 通过 InputFormat 获得的 RecordReader,从输入 InputSplit 中 解析出一个个 key/value。
- Map 阶段:该节点主要是将解析出的 key/value 交给用户编写 map()函数处理,并 产生一系列新的 key/value。
- Collect 收集阶段:在用户编写 map()函数中,当数据处理完成后,一般会调用 OutputCollector.collect()输出结果。在该函数内部,它会将生成的 key/value 分区(调用 Partitioner),并写入一个环形内存缓冲区中。
- Spill 阶段(溢写阶段):当环形缓冲区满后,MapReduce 会将数据写到本地磁盘上, 生成一个临时文件。需要注意的是,将数据写入本地磁盘之前,先要对数据进行一次本地排序,并在必要时对数据进行合并、压缩等操作。
- 步骤 1:利用快速排序算法对缓存区内的数据进行排序,排序方式是,先按照分区编号 Partition 进行排序,然后按照 key 进行排序。这样,经过排序后,数据以分区为单位聚集在 一起,且同一分区内所有数据按照 key 有序。
- 步骤 2:按照分区编号由小到大依次将每个分区中的数据写入任务工作目录下的临时文 件 output/spillN.out(N 表示当前溢写次数)中。如果用户设置了 Combiner,则写入文件之 前,对每个分区中的数据进行一次聚集操作。
- 步骤 3:将分区数据的元信息写到内存索引数据结构 SpillRecord 中,其中每个分区的元 信息包括在临时文件中的偏移量、压缩前数据大小和压缩后数据大小。如果当前内存索引大 小超过 1MB,则将内存索引写到文件 output/spillN.out.index 中。
- Merge 阶段:当所有数据处理完成后,MapTask 对所有临时文件进行一次合并, 以确保最终只会生成一个数据文件。 当所有数据处理完后,MapTask 会将所有临时文件合并成一个大文件,并保存到文件 output/file.out 中,同时生成相应的索引文件 output/file.out.index。 在进行文件合并过程中,MapTask 以分区为单位进行合并。对于某个分区,它将采用多 轮递归合并的方式。每轮合并 mapreduce.task.io.sort.factor(默认 10)个文件,并将产生的文 件重新加入待合并列表中,对文件排序后,重复以上过程,直到最终得到一个大文件。 让每个 MapTask 最终只生成一个数据文件,可避免同时打开大量文件和同时读取大量小文件产生的随机读取带来的开销。
3.5.2、ReduceTask 工作机制
- Copy 阶段:ReduceTask 从各个 MapTask 上远程拷贝一片数据,并针对某一片数 据,如果其大小超过一定阈值,则写到磁盘上,否则直接放到内存中。
- Sort 阶段:在远程拷贝数据的同时,ReduceTask 启动了两个后台线程对内存和磁 盘上的文件进行合并,以防止内存使用过多或磁盘上文件过多。按照 MapReduce 语义,用 户编写 reduce()函数输入数据是按 key 进行聚集的一组数据。为了将 key 相同的数据聚在一 起,Hadoop 采用了基于排序的策略。由于各个 MapTask 已经实现对自己的处理结果进行了 局部排序,因此,ReduceTask 只需对所有数据进行一次归并排序即可。
- Reduce 阶段:reduce()函数将计算结果写到 HDFS 上。
3.5.3、ReduceTask并行度决定机制
回顾:MapTask并行度由切片个数决定,切片个数由输入文件和切片规则决定。
思考:ReduceTask并行度由谁决定?
设置ReduceTask并行度(个数)
ReduceTask的并行度同样影响真个Job的执行并发度和执行效率,但于MapTask的并发数由切片数决定不同,ReduceTask数量的决定是可以直接手动设置:
1
2// 默认值是 1,手动设置为 4
job.setNumReduceTasks(4);实验:测试ReduceTask多少合适
实验环境:1个Master节点,16个Slave节点:CPU:8GHZ,内存:2G
实验结论:
注意事项
ReduceTask=0,表示没有Reduce阶段,输出文件个数和Map个数一致。
ReduceTask默认值就是1,所以输出文件个数为一个。
如果数据分布不均匀,就有可能在Reduce阶段产生数据倾斜
ReduceTask数量并不是任意设置,还要考虑业务逻辑需求,有些情况下,需要计算全局汇总结果,就只能有1个ReduceTask。
具体多少个ReduceTask,需要根据集群性能而定。
如果分区数不是1,但是ReduceTask为1,是否执行分区过程。
答案是:不执行分区过程。因为在MapTask的源码中,执行分区的前提是先判断ReduceNum个数是否大于1。不大于1肯定不执行。
3.6、Join应用
3.6.1、Reduce Join
Map 端的主要工作:为来自不同表或文件的 key/value 对,打标签以区别不同来源的记录。然后用连接字段作为 key,其余部分和新加的标志作为 value,最后进行输出。
Reduce 端的主要工作:在 Reduce 端以连接字段作为 key 的分组已经完成,我们只需要在每一个分组当中将那些来源于不同文件的记录(在 Map 阶段已经打标志)分开,最后进行合并就 ok 了。
3.6.2、Reduce Join 案例实操
需求
将商品信息表中数据根据商品 pid 合并到订单数据表中。
数据:
order.txt
pd.txt
需求分析
通过将关联条件(pid)作为 Map 输出的 key,将两表满足 Join 条件的数据并携带数据所来源的文件信息,发往同一个 ReduceTask,在 Reduce 中进行数据的串联。
- Map阶段:
- 输入:
- key:LongWritable 行号
- value:Text 每一行的内容
- 输出:
- key:Text id
- value:TableBean 自定义Bean
- 输入:
- Reduce阶段:
- 输入:
- key:
- value:
- 输出:
- key:
- value:
- 输入:
- Map阶段:
代码实现
创建商品和订单合并后的TableBean类
用Lombok创建的无参构造和getter和setter。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.hadoop.io.Writable;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
public class TableBean implements Writable {
// id pid amount
// pid name
private String id; // 订单id
private String pid; // 商品id
private int amount; // 商品数量
private String name; // 商品名称
private String flag; // 标记是那个表
public void write(DataOutput dataOutput) throws IOException {
dataOutput.writeUTF(id);
dataOutput.writeUTF(pid);
dataOutput.writeInt(amount);
dataOutput.writeUTF(name);
dataOutput.writeUTF(flag);
}
public void readFields(DataInput dataInput) throws IOException {
this.id = dataInput.readUTF();
this.pid = dataInput.readUTF();
this.amount = dataInput.readInt();
this.name = dataInput.readUTF();
this.flag = dataInput.readUTF();
}
public String toString() {
// id name amount
return id + '\t' + name + '\t' + amount;
}
}编写TableMapper类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;
import java.io.IOException;
public class TableMapper extends Mapper<LongWritable, Text, Text, TableBean> {
private String fileName;
private Text outKey = new Text();
private TableBean outValue = new TableBean();
protected void setup(Mapper<LongWritable, Text, Text, TableBean>.Context context) throws IOException, InterruptedException {
// 初始化 order.txt pd.txt
FileSplit split = (FileSplit) context.getInputSplit();
fileName = split.getPath().getName();
}
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, TableBean>.Context context)
throws IOException, InterruptedException {
// 1. 获取一行
String line = value.toString();
// 2. 判断是哪一个文件的
if (fileName.contains("order")) { // 处理的是 order.txt
// 3. 分割 [1001,01,1]
String[] split = line.split("\t");
// 4. 封装kv
outKey.set(split[1]);
outValue.setId(split[0]);
outValue.setPid(split[1]);
outValue.setAmount(Integer.parseInt(split[2]));
outValue.setName("");
outValue.setFlag("order");
} else { // 处理的是 pd.txt
// 3. 分割 [01,小米]
String[] split = line.split("\t");
// 4. 封装
outKey.set(split[0]);
outValue.setId("");
outValue.setPid(split[0]);
outValue.setAmount(0);
outValue.setName(split[1]);
outValue.setFlag("pd");
}
// 5.写出
context.write(outKey, outValue);
}
}编写TableReducer类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51import org.apache.commons.beanutils.BeanUtils;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
public class TableReducer extends Reducer<Text, TableBean, TableBean, NullWritable> {
protected void reduce(Text key, Iterable<TableBean> values, Reducer<Text, TableBean, TableBean, NullWritable>.Context context)
throws IOException, InterruptedException {
// 01 1001 1 order
// 01 1004 4 order
// 01 小米 pd
// 准备初始化集合
ArrayList<TableBean> orderBeans = new ArrayList<>();
TableBean pdBean = new TableBean();
// 循环遍历
for (TableBean value : values) {
if("order".equals(value.getFlag())){ // 处理订单表
TableBean tempTableBean = new TableBean();
try {
BeanUtils.copyProperties(tempTableBean, value);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
orderBeans.add(tempTableBean);
} else { // 处理商品表
try {
BeanUtils.copyProperties(pdBean, value);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
}
// 循环遍历 orderBeans ,赋值 name
for (TableBean orderBean : orderBeans) {
orderBean.setName(pdBean.getName());
// 写出
context.write(orderBean, NullWritable.get());
}
}
}编写TableDriver类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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 TableDriver {
public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {
Job job = Job.getInstance(new Configuration());
job.setJarByClass(TableDriver.class);
job.setMapperClass(TableMapper.class);
job.setReducerClass(TableReducer.class);
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(TableBean.class);
job.setOutputKeyClass(TableBean.class);
job.setOutputValueClass(NullWritable.class);
FileInputFormat.setInputPaths(job, new Path("D:\\桌面\\input"));
FileOutputFormat.setOutputPath(job, new Path("D:\\桌面\\output"));
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
总结
缺点:这种方式中,合并的操作是在 Reduce 阶段完成,Reduce 端的处理压力太大,Map 节点的运算负载则很低,资源利用率不高,且在 Reduce 阶段极易产生数据倾斜。
==解决方案:Map 端实现数据合并(利用Map Join)。==
3.6.3、Map Join
使用场景
Map Join适用于一张表十分小、一张表很大的场景。
优点
思考:在Reduce端处理过多的表,非常容易产生数据倾斜。怎么办?
在Map端缓存多张表,提前处理业务逻辑,这样增加Map端业务,减少Reduce端数据的压力,尽可能的减少数据倾斜。
具体办法:采用DistributedCache
在Mapper的setup阶段,将文件读取到缓存集合中
在Driver驱动类中加载缓存
1
2
3
4//缓存普通文件到 Task 运行节点。
job.addCacheFile(new URI("file:///e:/cache/pd.txt"));
//如果是集群运行,需要设置 HDFS 路径
job.addCacheFile(new URI("hdfs://hadoop102:8020/cache/pd.txt"));
3.6.4、Map Join 案例实操
需求
需求分析
MapJoin适用于关联表中有表的情况
实现代码
在MapJoinDriver驱动类中添加缓存文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
import java.net.URI;
import java.net.URISyntaxException;
public class MapJoinDriver {
public static void main(String[] args) throws IOException, URISyntaxException, InterruptedException, ClassNotFoundException {
// 1 获取 job 信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 设置加载 jar 包路径
job.setJarByClass(MapJoinDriver.class);
// 3 关联 mapper
job.setMapperClass(MapJoinMapper.class);
// 4 设置 Map 输出 KV 类型
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(NullWritable.class);
// 5 设置最终输出 KV 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 加载缓存数据
job.addCacheFile(new URI("file:///D:/桌面/input/pd.txt"));
// Map 端 Join 的逻辑不需要 Reduce 阶段,设置 reduceTask 数量为 0
job.setNumReduceTasks(0);
// 6 设置输入输出路径
FileInputFormat.setInputPaths(job, new Path("D:\\桌面\\input\\order.txt"));
FileOutputFormat.setOutputPath(job, new Path("D:\\桌面\\output"));
// 7 提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}在MapJoinMapper类中的setup方法中读取缓存文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55import org.apache.commons.lang3.StringUtils;
import org.apache.hadoop.fs.FSDataInputStream;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IOUtils;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.HashMap;
public class MapJoinMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
HashMap<String, String> pdMap = new HashMap<>();
private Text outKey = new Text();
protected void setup(Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 获取缓存文件,并把缓存文件内容封装到集合 pd.txt中
URI[] cacheFiles = context.getCacheFiles();
// 获取流
FileSystem fs = FileSystem.get(context.getConfiguration());
FSDataInputStream fis = fs.open(new Path(cacheFiles[0]));
// 从流中读取数据
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
String line;
while (StringUtils.isNotEmpty(line = reader.readLine())){
// 切割 [01,小米]
String[] split = line.split("\t");
pdMap.put(split[0],split[1]);
}
// 关流
IOUtils.closeStream(reader);
}
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 处理 order.txt
String line = value.toString();
// 分割 [1001,01,1]
String[] fields = line.split("\t");
// 获取pid
String pName = pdMap.get(fields[1]);
// 封装kv
outKey.set(fields[0] + "\t" + pName + "\t" + fields[2]);
context.write(outKey, NullWritable.get());
}
}
效果
3.7、数据清洗(ETL)
“ETL,是英文 Extract-Transform-Load 的缩写,用来描述将数据从来源端经过抽取 (Extract)、转换(Transform)、加载(Load)至目的端的过程。ETL 一词较常用在数据仓库,但其对象并不限于数据仓库。
在运行核心业务 MapReduce 程序之前,往往要先对数据进行清洗,清理掉不符合用户 要求的数据。清理的过程往往只需要运行 Mapper 程序,不需要运行 Reduce 程序。
需求
取出日志中字段个数小于等于11的日志
需求分析
需要在Map阶段对输入的数据根据规则进行过滤
实现代码
编写WebLogMapper类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import java.io.IOException;
public class WebLogMapper extends Mapper<LongWritable, Text, Text, NullWritable> {
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, NullWritable>.Context context) throws IOException, InterruptedException {
// 1. 获取一行
String line = value.toString();
// 3. ETL
boolean result = parseLog(line,context);
if (!result) {
return;
}
// 4. 写出
context.write(value, NullWritable.get());
}
private boolean parseLog(String line, Context context) {
// 2. 切割
String[] split = line.split(" ");
// 判断日志长度是否大于11
if(split.length > 11) {
return true;
}
return false;
}
}编写WebLogDriver类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34import com.zhang.mapreduce.outputFormat.LogDriver;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
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;
public class WebLogDriver {
public static void main(String[] args) throws Exception {
// 输入输出路径需要根据自己电脑上实际的输入输出路径设置
args = new String[] { "D:/桌面/input", "D:/桌面/output" };
// 1 获取 job 信息
Configuration conf = new Configuration();
Job job = Job.getInstance(conf);
// 2 加载 jar 包
job.setJarByClass(LogDriver.class);
// 3 关联 map
job.setMapperClass(WebLogMapper.class);
// 4 设置最终输出类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(NullWritable.class);
// 设置 reduceTask 个数为 0
job.setNumReduceTasks(0);
// 5 设置输入和输出路径
FileInputFormat.setInputPaths(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
// 6 提交
boolean b = job.waitForCompletion(true);
System.exit(b ? 0 : 1);
}
}
3.8、MapReduce开发总结
输入数据接口:
InputFormat
- 默认使用的实现类是:
TextInputFormat
TextInputFormat
的功能逻辑是:一次读一行文本,然后将该行的起始偏移量作为key,行内容作为value返回CombineTextInputFormat
可以把多个小文件合并成一个切片处理,提高处理效率。
- 默认使用的实现类是:
逻辑处理接口:Mapper
用户根据业务需求实现其中三个方法:
map()
、setip()
、cleanup()
Partitioner 分区
- 有默认实现
HashPartitioner
,逻辑是根据key的哈希值和numReduces
来返回一个分区号;key.hashCode()&Integer.MAX_VALUE%numReduces
- 如果业务上有特变的需求,可以自定义分区。
- 有默认实现
Comparable 排序
- 当我们用自定义的对象作为key来输出时,就必须要是实现
WritableComparable
接口,重写其中的compareTo()
方法 - 部分排序:对最终输出的每一个文件进行内部排序
- 全排序:对所有数据进行排序,通常只有一个Reduce
- 二次排序:排序的条件有两个
- 当我们用自定义的对象作为key来输出时,就必须要是实现
Combiner 合并
Combiner 合并可以提高程序执行效率,减少IO传输。但是使用时必须不能影响原有的业务处理结果
逻辑处理接口:Reducer
用户根据业务需求实现其中三个方法:
reduce()
、setup()
、cleanup()
输出数据接口:OutputFormat
- 默认实现类时
TextOutputFormat
,功能逻辑时:将每一个KV对,向目标文本文件输出一行 - 用户还可以自定义
OutputFormat
- 默认实现类时
四、Hadoop数据压缩
4.1、概述
压缩的好处和坏处
压缩的优点:以减少磁盘IO、减少磁盘存储空间。
压缩的缺点:增加CPU开销。
压缩原则
- 运算密集型的Job,少用压缩
- IO密集型的Job,多用压缩
4.2 MR支持的压缩编码
压缩算法对比介绍
| 压缩格式 | hadoop自带? | 算法 | 文件扩展名 | 是否可切片 | 换成压缩格式后。原来的程序是否需要修改 |
| ———— | —————————————————————- | ———- | ————— | ————————————————- | —————————————————————————————— |
| DEFLATE | 是,直接使用 | DEFLATE | .deflate | 否 | 和文本处理一样,不需要修改 |
| Gzip | 是,直接使用 | DEFLATE | .gz | 否 | 和文本处理一样,不需要修改 |
| bzip2 | 是,直接使用 | bzip2 | .bz2 | 是 | 和文本处理一样,不需要修改 |
| LZO | 否,需要安装 | LZO | .lzo | 是 | 需要建索引,还需要指定输入格式 |
| Snappy | 是,直接使用 | Snappy | .snappy | 否 | 和文本处理一样,不需要修改 |压缩性能的比较
压缩方式选择
压缩方式选择时重点考虑:
- 压缩/解压缩速度
- 压缩率(压缩后存储大小)
- 压缩后是否 可以支持切片。
4.3.1、Gzip压缩
优点:压缩率比较高;
缺点:不支持Split;压缩/解压速度一般;
4.3.2、Bzip2压缩
优点:压缩率高;支持Split;
缺点:压缩/解压速度慢
4.3.3 Lzo压缩
优点:压缩/解压速度比较块;支持Split;
缺点:压缩率一般;想支持切片需要额外创建索引
4.3.4 Snappy压缩
优点:压缩饿解压速度块;
缺点:不支持Split;压缩率一般
压缩位置选择
压缩可以在MapReduce作用的任意阶段启用
4.4、压缩参数配置
为了支持多种压缩/解压缩算法,Hadoop 引入了编码/解码器
要在Hadoop中启用压缩,可以配置如下参数
| 参数 | 默认值 | 阶段 | 建议 |
| —————————————————————————————— | —————————————————————————————— | ——————————————————————- | —————————————————————————————— |
| io.compression.codecs(在 core-site.xml 中配置) | 无,这个需要在命令行输入hadoop checknative
查看 | 输入压缩 | Hadoop 使用文件扩展 名判断是否支持某种 编解码器 |
| mapreduce.map.output.compress(在 mapred-site.xml 中配置) | false | mapper 输出 | 这个参数设为 true 启 用压缩 |
| mapreduce.map.output.compress.codec(在 mapred-site.xml 中配置) | org.apache.hadoop.io.com press.DefaultCodec | mapper 输出 | 企业多使用 LZO 或 Snappy 编解码器在此 阶段压缩数据 |
| mapreduce.output.fileoutputformat.compress(在 mapred-site.xml 中配置) | false | reducer 输出 | 这个参数设为 true 启 用压缩 |
| mapreduce.output.fileoutputformat.compress.codec(在 mapred-site.xml 中配置) | org.apache.hadoop.io.com press.DefaultCodec | reducer 输出 | 使用标准工具或者编 解码器,如 gzip 和 bzip2 |
4.5、压缩案例实操
以最简单的WordCount为例
4.5.1、Map输出端采用压缩
即使你的 MapReduce 的输入输出文件都是未压缩的文件,你仍然可以对 Map 任务的中间结果输出做压缩,因为它要写在硬盘并且通过网络传输到 Reduce 节点,对其压缩可以提高很多性能,这些工作只要设置两个属性即可,我们来看下代码怎么设置。
提供的 Hadoop 源码支持的压缩格式有:BZip2Codec、DefaultCodec
修改WordCountDriver类,添加20-23行代码
1
2
3
4// 开启 map 端输出压缩
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置 map 端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53import 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.io.compress.BZip2Codec;
import org.apache.hadoop.io.compress.CompressionCodec;
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, InterruptedException, ClassNotFoundException {
// 1. 获取配置信息以及获取 job 对象
Configuration configuration = new Configuration();
// 开启 map 端输出压缩
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置 map 端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job = Job.getInstance(configuration);
// 2. 关联本 Driver 程序的 jar
job.setJarByClass(WordCountDriver.class);
// 3. 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(WordCountMapper.class); // 关联 Mapper
job.setReducerClass(WordCountReducer.class); // 关联 Reducer
// 4. 设置 Mapper 输出的 kv 类型 ,注意有Map,没有map的是第5步调用的方法
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5. 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6. 设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\hello.txt"));
FileOutputFormat.setOutputPath(job,new Path("D:\\桌面\\hello_output"));
// 7. 提交 job
boolean result = job.waitForCompletion(false);// 参数true表示需要获取job的工作信息,false不用
// 根据结果返回值,如果result == true(成功)返回0,如果失败返回1
System.exit(result ? 0 : 1);
}
}Mapper 保持不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46import 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;
/**
* 输入:
* KEY : LongWritable // 第几行
* VALUE : Text // 该行的内容
* 输出:
* KEY : Text // 被统计的单词
* VALUE : IntWritable // 该单词的数量
*/
public class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private Text outKEY = new Text();
private IntWritable outVALUE = new IntWritable(1); // 一个单词数量是 1
/**
* 重写map方法
* @param key 输入数据的KEY
* @param value 输入数据的VALUE
* @param context 充当上下文,进行map和reduce之间的联络
*/
protected void map(LongWritable key, Text value, Mapper<LongWritable, Text, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
// 1.获取一行 zyr zyr zyr
String line = value.toString();
// 2.切割 [zyr,zyr,zyr]
String[] words = line.split(" ");
// 3.循环写出
for (String word : words) {
// 封装outKEY
outKEY.set(word);
// 写出
context.write(outKEY,outVALUE);
}
}
}Reducer 保持不变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import java.io.IOException;
/**
* 这里和Mapper中的输入输出刚好相反
* 输入:
* KEY : Text // 被统计的单词
* VALUE : IntWritable // 该单词的数量
* 输出:
* KEY : Text // 被统计的单词
* VALUE : IntWritable // 该单词的数量
*/
public class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable outValue = new IntWritable();
/**
* 重写reduce
* @param key 输入的key
* @param values 输入的value,是一个可迭代对象(不是迭代器,它时一个集合,如果想要遍历它,需要先用value.iterator()得到一个迭代器,或者增强for循环,集合的遍历方式都可)
* 例如zyr zyr zyr,被mapper弄成了三个,所以传入的K-V实际上是三组 zyr-1。但是Reducer在接收数据时会转化为:zyr-(1,1,1)
* @param context 上下文作用
*/
protected void reduce(Text key, Iterable<IntWritable> values, Reducer<Text, IntWritable, Text, IntWritable>.Context context)
throws IOException, InterruptedException {
int sum = 0;
// 累加 zyr-(1,1,1)
for (IntWritable value : values) {
sum += value.get();
}
outValue.set(sum);
// 写出
context.write(key,outValue);
}
}效果和之前一样,因为设置的压缩式Mapper输出压缩,到了Reducer又加压缩了。
4.5.2、Reduce输出端采用压缩
基于WordCount案例吃力
修改WordCountDriver驱动类,添加46-49行的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60import 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.io.compress.BZip2Codec;
import org.apache.hadoop.io.compress.CompressionCodec;
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, InterruptedException, ClassNotFoundException {
// 1. 获取配置信息以及获取 job 对象
Configuration configuration = new Configuration();
// 开启 map 端输出压缩
configuration.setBoolean("mapreduce.map.output.compress", true);
// 设置 map 端输出压缩方式
configuration.setClass("mapreduce.map.output.compress.codec", BZip2Codec.class, CompressionCodec.class);
Job job = Job.getInstance(configuration);
// 2. 关联本 Driver 程序的 jar
job.setJarByClass(WordCountDriver.class);
// 3. 关联 Mapper 和 Reducer 的 jar
job.setMapperClass(WordCountMapper.class); // 关联 Mapper
job.setReducerClass(WordCountReducer.class); // 关联 Reducer
// 4. 设置 Mapper 输出的 kv 类型 ,注意有Map,没有map的是第5步调用的方法
job.setMapOutputKeyClass(Text.class);
job.setMapOutputValueClass(IntWritable.class);
// 5. 设置最终输出 kv 类型
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
// 6. 设置输入和输出路径
FileInputFormat.setInputPaths(job,new Path("D:\\桌面\\hello.txt"));
FileOutputFormat.setOutputPath(job,new Path("D:\\桌面\\hello_output"));
// 设置 reduce 端输出压缩开启
FileOutputFormat.setCompressOutput(job, true);
// 设置压缩的方式
FileOutputFormat.setOutputCompressorClass(job, BZip2Codec.class);
// FileOutputFormat.setOutputCompressorClass(job, GzipCodec.class);
// FileOutputFormat.setOutputCompressorClass(job,DefaultCodec.class);
// 7. 提交 job
boolean result = job.waitForCompletion(false);// 参数true表示需要获取job的工作信息,false不用
// 根据结果返回值,如果result == true(成功)返回0,如果失败返回1
System.exit(result ? 0 : 1);
}
}Mapper和Reducer保持不变
效果:
==各个阶段的压缩方式不同也影响结果==