MapReduce框架原理之MapReduce工作流程
MapReduce工作流程
1. 流程图
- MapReduce流程图(1)
- MapReduce流程图(2)
2. 流程详解
MapReduce执行机制
这里只是指出个人理解的部分,帮助记忆MapReduce的工作流程,实际上细节还有很多,有不妥的地方还请多多指教.
实际上,我们在Driver调用了job.waitForCompletion后,客户端并不是马上将job提交给YARN,在向YARN提交job之前,客户端会先通过反射,获取到job将要使用到的InputFormat,以获得逻辑的切片规则,并将切片规则记录到本地的文件中:
windowns端的话在C:\tmp\hadoop-PC_NAME\mapred\staging\PC_NAMEJOBID\.staging\job_localJOBID目录下
(执行完毕后会被删除)
(注:InputFormat只是进行逻辑切片规则的指定,而不是真正的进行物理切片,真正的物理切片动作是再RecordReader中进行的
)
制定好切片规则后(当然这个过程很复杂,还有很多其他步骤),会将这些job文件(job.xml等)提交给YARN,然后YARN会根据切片规则等信息分配出相应的MapTask,在MapTask阶段通过RecordReader来进行真正的文件切分(物理上的),并将切片后的数据制作成K,V对的形式,交给Mapper的map方法,一般来讲一个切片的数据会启用一个MapTask,在MapTask和ReduceTask中还有一步shuffle的操作,这个先略过,后面会进行说明.经过MapTask阶段的处理,数据来到了ReduceTask,并且一般来讲,相同Key的数据会交给同一个ReduceTask来处理,在ReduceTask中的reduce方法里,写了我们的业务代码,将数据封装成我们需要的格式后输出到目标目录,输出时会根据所指定的OutputFormatClass
类型,来对数据进行输出格式化.
3. shuffle机制
上面提到过shuffle,它在MapTask和ReduceTask中都有存在.
3.1 MapTask中:
- 从map方法通过context.write(key, value)输出出来的K,V数据,会被写入到内存的
环形缓冲区
,它的默认大小为100M
,在其中某一点作为起点,顺时针方向写入数据,逆时针方向写入数据对应的索引等元数据信息.(在环状缓冲区内会进行分区,和第1次排序,快排
) - 每当写入的数据达到环状缓冲区80%的容量时,会发生溢出操作,将数据持久到磁盘,根据数据的量大小,会生成N个溢出文件.
- 这些文件会被合并成大的溢出文件交给Mapper.(
第2次排序,归并排序
) - 在溢出和合并的过程中都会调用Partitioner进行分区并同时针对Key进行排序,默认的分区数总是0.
3.2 ReduceTask中:
- ReduceTask根据自己的分区号,去MapTask机器上找相应的结果分区数据.
- ReduceTask会取到分布在不同MapTask上的同一个分区的数据,ReduceTask会将这些文件再进行合并(
第3次排序,归并排序
) - 行成大文件后,Shuffle的过程就结束了,后面进入ReduceTask的逻辑运算过程(从文件中取出一个一个的键值对Group,调用reduce方法)
Shuffle环形缓冲区大小会影响到MapReduce的执行效率,原则上说,缓冲区越大磁盘IO的次数就越少,执行速度越快. 缓冲区的大小可以通过调整,io.sort.mb来进行修改,默认是100M
3.3 Partition分区:
- 问题引出
要求将统计结果按条件输出到不同文件中
(分区),比如将统计结果按手机归属地不同省份输出到不同文件中. - 默认Partitioner分区:
protected int getPartition(int hash, int numReduceTasks) {
return (hash & 2147483647) % numReduceTasks;
}
mapred-default.xml
<property>
<name>mapreduce.job.reduces</name>
<value>1</value>
<description>The default number of reduce tasks per job. Typically set to 99%
of the cluster's reduce capacity, so that if a node fails the reduces can
still be executed in a single wave.
Ignored when mapreduce.framework.name is "local".
</description>
</property>
在Java中,Integer的值用32进制来表示的,而2147483647
代表Integer的最大值.这里用hash
值和Integer的最大值做位与运算,最大值总是01111111…即首位是0,其余位置都是1,任何正数与值进行位与计算,结果都是其本身,而负数则被转换成正数.
而默认的numReduceTasks
是1,所以mod结果总是0,即代表默认的只有一个ReduceTask,并且对数据不进行分区.
-
自定义Partitioner步骤
(1)自定义类继承Partitioner,重写getPartition()方法
(2)在Driver类中设置自定义的PartitionerClass
job.setPartitionerClass(xxxxx.class);
(3)自定义Parititioner后要根据其逻辑设置相应的ReduceTastk
job.setNumReduceTasks(5); -
分区总结
(1)如果ReduceTask的数量 > getPartition的结果数,则最终生成的文件会多产生几个part-r-000xx的空文件;
(2)如果1 < ReduceTask的数量 < getPartition的结果数,则有一部分分区数据无处安放,则会抛异常;
(3)如果ReduceTask的数量 = 1,则不管MapTask端输出多少个分区文件,最终结果都交给这一个ReduceTask来处理,最终也只产生一个part-r-00000文件;
(4)分区号必须从0开始逐一累加.
e.g:
假设自定义分区数为5,则:
<1> job.setNumReduceTasks(1);会正常运行,只不过只产生一个输出文件
<2> job.setNumReduceTasks(2);会报错
<3> job.setNumRedeceTasks(6);会正常执行,只不过会产生6个输出文件,其中一个是空的
-
实操演练
(1)需求,将手机按号段输出到不同文件, 136、137、138、139分别放到4个文件,其他号段放一个文件中.
(2)业务代码基于我的另一篇有关Hadoop序列化的博客中的demo,来进行演示,如果对序列化相关内容感兴趣可以参考Hadoop之序列化
自定义Partitioner
package partition;
import cstmbean.FlowBean;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Partitioner;
public class PhonePartitioner extends Partitioner<Text, FlowBean> {
@Override
public int getPartition(Text text, FlowBean flowBean, int i) {
String code = text.toString().substring(0, 3);
int partition = 0;
switch(code) {
case "136":
partition = 0;
break;
case "137":
partition = 1;
break;
case "138":
partition = 2;
break;
case "139":
partition = 3;
break;
default:
partition = 4;
}
return partition;
}
}
Driver类中
job.setPartitionerClass(PhonePartitioner.class);
job.setNumReduceTasks(5);
执行后会生成下面的5个文件
3.4 WritableComParable排序
-
排序概述
排序是MapReduce的重要操作之一.
MapTask和ReduceTask均会对数据按照key
进行排序.该操作属于Hadoop的默认行为.任何应用程序中的数据均会被排序,而不管逻辑上是否需要
.
默认的排序是按照字典顺序排序
,而且实现排序的方法是快速排序
.对于MapTask,它会将处理的结果暂时放到环形缓冲区中,
当环形缓冲区使用率达到阈值时(默认80%)&