假设本文用到的输入文件有如下两个。
文件1:
big data
offline data
online data
offline online data
文件2:
hello data
hello online
hello offline
按照时间顺序,MapReduce任务执行包括:输入分片Map、Shuffle和Reduce等阶段,一个阶段的输出正好是下一阶段的输入。
结合上文的示例文件更加深入和详细地介绍上述过程,结合单词计数实例的MapReduce执行阶段和流程图:
1.输入分片
在进行Map计算之前,MapReduce会根据输入文件计算输入分片。每个输入分片对应一个Map任务,输入分片存储的并非数据本身,而是一个分片长度和一个记录数据的位置的数组。输入分片往往和HDFS的block(块)关系很密切,假如设定HDFS的块的大小是64MB,如果输入只有一个文件大小为150MB,那么MapReduce会把此大文件切分为三片(分别为64MB、64MB和22MB),同样,如果输入为两个文件,其大小分别是20MB和100MB,那么MapReduce会把20MB文件作为一个输入分片,100MB则切分为两个即64MB和36MB的输入分片。对于上述实例文件1和2,由于非常小,因此分别被作为split1和split2输入Map任务1和2中(此处只为说明问题,实际处理中应将小文件进行合并,否则如果输入多个文件而且文件大小均远小于块大小,会导致生成多个不必要的Map任务,这也是MapReduce优化计算的一个关键点)。
2. Map阶段
Map任务会根据用户自定义的映射规则,输出一系列的<key,value>
作为中间结果。比如本例:
big 1
data 1
offline 1
data 1
online 1
data 1
offline 1
online 1
data 1
3.Combiner阶段
Combiner阶段是可选的。Combiner其实也是一种Reduce操作,但它是一个本地化的Reduce操作,是Map运算的本地后续操作,主要是在Map计算出中间文件前做一个简单的合并重复键值的操作,例如上述文件1中data出现了4次,Map计算时如果碰到一个data的单词就会记录为1,这样就重复计算了4次,Map任务输出就会有冗余,这样后续处理和网络传输都被消费不必要的资源,因此通过Combiner操作可以解决和优化此问题,但这一操作是有风险的,使用它的原则是Combiner的输出不会影响到Reduce计算的最终输入,例如,如果计算只是求总数、最大值及最小值,可以使用Combiner操作,但是如果做平均值计算使用Combiner,最终的Reduce计算结果就会出错。如4个<data,1> 经Combiner合并后成<data,4>;
4.Shuffle阶段
为了让Reduce可以并行的处理Map的结果,Map任务的输出必须经过一个名叫Shuffle的阶段才能交给Reduce处理。总体来说,Shuffle阶段包含在Map和Reduce两个阶段中,在Map阶段的Shuffle阶段是对Map的结果进行分区(partition)、排序(sort)、合并(combine)、归并(merge)等操作,得到<key,value>
形式的中间结果,然后将属于同一个分区的输出归并(merge)在一起并写在磁盘上,同时按照不同的分区划分发送给对应的Reduce(Map输出的划分和Reduce任务的对应关系由JobTracker确定)的整个过程;Reduce阶段的Shuffle又会将各个Map输出的同一个分区划分的输出进行合并,然后对合并的结果进行排序,最后交给Reduce处理的整个过程。
(1)Map阶段Shuffle
关键字提取:
- 数据分片
- 运算结果写入缓存
- 缓存达到阈值,溢写到磁盘文件
- 溢写前会进行分区,分区内排序和合并(可选的,即Combine)
- 分区默认采用哈希函数
- 排序是默认的操作
- 排序后可以合并(Combine),合并不能改变最终结果
- 归并成大文件
- 每次溢写会生成一个溢写文件,这些溢写文件最终要被归并成一个大文件
- 归并的意思:生成key和对应的value-list
- 文件归并是,如果溢写文件超过min.num.spills.for.combine的值(默认为3)时,可以再次进行合并
通常MapReduce计算的都是海量数据,而且Map输出还需要对结果进行排序,内存开销很大,因此完全在内存中完成是不可能也是不现实的,所以Map输出时会在内存里开启一个环形内存缓冲区,并且在配置文件里为这个缓冲区设定了一个阈值(默认为80%,可自定义修改此配置)。同时,Map还为输出操作启动一个守护线程,如果缓存区的内存使用达到了阈值,那么这个守护线程就会把这80%的内存区内容写到磁盘上,这个过程就叫分隔,另外的20%内存可以供Map输出继续使用,写入磁盘和写入内存操作是互不干扰的,如果缓存区被撑满了,那么Map就会阻塞写入内存的操作,待写入磁盘操作完成后再继续执行写入内存操作。
缓冲区内容分隔到磁盘前,会首先进行分区操作,分区的数目由Reduce的数目决定。对于本例,Reduce数目为2个,那么分区数就是2,然后对于每个分区,后台线程还会按照键值对需要写出的数据进行排序,如果配置了Combine函数,还会进行Combine操作,以使得更少地数据被写入磁盘并发送给Reducer。
每次的分隔操作都会生成一个分隔文件,全部的Map输出完成后,可能会有很多的分隔文件,因此在Map任务结束前,还要进行归并操作,即将这些分隔文件按照分区合并为单独的文件。在归并过程中,同样也会进行排序,如果定义了Combiner函数,也会进行combiner操作。
至此,Map阶段的所有工作都已结束,最终生成的文件也会存放在TaskTracker能访问的某个本地目录内。每个Reduce Task不断地从JobTracker那里获取Map Task是否完成的信息,如果Reduce Task得到通知,获知某台TaskTracker上的Map Task执行完成,Shuffle的后半段过程,也就是Reduce阶段的Shuffle,便开始启动。
(2)Reduce阶段Shuffle
Shuffle在Reduc阶段可以分为三个阶段:Copy Map输出、Merge阶段和Reduce处理。
关键字提取:
- 领取数据,copy map 输出
- 归并数据
- 数据输入给Reduce任务
- Reduce任务通过RPC向JobTracker询问Map任务是否已经完成,若完成,则领取数据
- Reduce领取数据先放入缓存,来自不同Map机器,先归并,再合并,写入磁盘(多个溢写文件归并成一个或多个大文件,文件中的键值对是排序的)
- 当数据量很少时,不需要溢写到磁盘,直接在缓存中归并,然后输出给Reduce
1)Copy Map输出:如上文所述,Map任务完成后,会通知父TaskTracker状态已完成,TaskTracker进而通知JobTracker(这些通知一般通过心跳机制完成)。对于Job来说,JobTracker记录了Map输出和TaskTracker的映射关系。同时Reduce也会定期向JobTracker获取Map的输出与否以及输出位置,一旦拿到输出位置,Reduce任务就会启动Copy线程,通过HTTP方式请求Map Task所在的TaskTracker获取其输出文件。因为Map Task早已结束,这些文件就被TaskTracker存储在maptask所在的本地磁盘中。
2)Merge阶段:此处的归并和Map阶段的合并类似,复制过来的数据会首先放入内存缓冲区中,这里的内存缓冲区大小比Map阶段要灵活很多,它基于JVM的heap size设置,因为Shuffle阶段Reduce Task并不运行,因此绝大部分内存应该给Shuffle使用;同时此Shuffle的归并阶段根据要处理的数据量不同,也可能会有分隔到磁盘的过程,如果设置了Combiner函数,Combiner操作也会进行。
3)Reduce Task的输入:不断合并后,最后会生成一个最终结果(可能在内存,也可能在磁盘),至此Reduce Task的输入准备完毕,下一步就是真正的Reduce操作。
5.Reduce阶段
经过Map和Reduce阶段的Shuffle过程后,Reduce任务的输入终于准备完毕,相关的数据已经被合并和汇总,Reduce任务只需调用Reduce函数即可,对于本例即对每个键,调用sum逻辑合并value并输出到HDFS即可,比如对于Reduce Task1的offline的键,只需将集合{2,1}相加,输出offline 3即可。
参考: