MapReduce
执行流程
MapTask执行流程
- Read:读取阶段
- MapTask会调用InputFormat中的 getSplits 方法来对文件进行切片
- 切片之后,针对每一个Split,产生一个 RecordReader 流用于读取数据
- 数据是以 Key-Value 形式来产生,交给 map 方法来处理。每一个键值对触发调用一次 map 方法
- Map:映射阶段
- map 方法在获取到键值对之后,按照要求对键值对中的数据进行拆分解析,解析之后按照要求输出键值对形式的结果
- Collect:收集阶段
- MapTask拆分产生数据之后,并不是直接将数据传输给ReduceTask,而是会调用
OutputCollector.collect 方法来收集输出结果 - OutputCollector.collect 在收集到数据之后,会先按照指定的规则,对数据进行分区,
分区完成之后,会将数据写到缓冲区中 - 缓冲区本质上是一个环形的字节数组,默认大小是100M(可以通过属性
mapreduce.task.io.sort.mb 来调节),默认阈值是0.8(可以通过属mapreduce.map.sort.spill.percent 来调节)
- MapTask拆分产生数据之后,并不是直接将数据传输给ReduceTask,而是会调用
- Spill:溢写阶段
- 当缓冲区使用达到指定阈值的时候,MapTask会将缓冲区中的数据冲刷(flush)到磁盘上,这
个过程称之为溢写(spill) - 在溢写的时候,会按照如下步骤进行
- 排序(sort)。此时,是将毫无规律的数据整理成有序数据,采用的Quick Sort(快速排
序)。需要注意的是,数据在排序的时候,是按照分区内进行的排序,即先按照分区大小进行分区号的升序,然后每一个分区内按照指定规则排序。因此,数据是分区内有序 - 合并(combine)。如果用户指定了 Combiner ,那么此时会将数据进行合并处理
- 写出(flush)。按照分区的顺序,将每一个分区中的数据依次写入任务的工作目录的临时
文件 output/spillN.out 中。 N 表示溢写次数。溢写次数不能完全由原始数据大小来决定,还得考虑 map 方法的处理过程。此时,单个结果文件中是分区且有序的,整体而
言是局部有序 - 压缩(compress)。如果用户指定了对MapTask的结果进行压缩,那么数据在写出之后还
会进行压缩处理 - 记录(record)。将分区数据的元信息记录到内存索引数据结果 SpillRecord 中。元信息
中包含:每一个分区在每一个临时结果文件中的偏移量(offset),每一个分区压缩前的数据大小以及压缩后的数据大小。如果 SpillRecord 中记录的所有的元信息大小之和不超过1M,那么 SpillRecord 中的数据也会写到 output/spillN.out.index 中
- 排序(sort)。此时,是将毫无规律的数据整理成有序数据,采用的Quick Sort(快速排
- 当缓冲区使用达到指定阈值的时候,MapTask会将缓冲区中的数据冲刷(flush)到磁盘上,这
- Merge:合并阶段
- 当MapTask将所有的数据处理完成之后,会将所有的临时结果文件 spillN.out 合并(merge)
成一个大的结果文件 output/file.out ,同时会为这个文件生成索引文件 file.out.index - 在merge过程中,数据会再次进行分区。分区之后数据会再次进行排序。注意,此次排序是
将局部有序的数据整理成整体有序,采用的是Merge Sort(归并排序) - 在merge过程中,如果指定了Combiner,那么数据会进行combine操作
- merge的时候,默认情况下,是每10个(可以通过属性 mapreduce.task.io.sort.factor 来调节)小文件合并成1个大文件,通过多次合并,最后会产生一个结果文件 file.out 。这样子,能够有效的避免同时打开大量文件带来的开销
- 注意:无论是否会进行Spill过程,最后都会产生一个 file.out 文件!!!
ReduceTask执行流程
- 当MapTask将所有的数据处理完成之后,会将所有的临时结果文件 spillN.out 合并(merge)
- 当达到启动阈值的时候,ReduceTask就会启动。默认情况下,启动阈值是0.05,即5%的MapTask结束ReduceTask就会启动。可以通过属性mapreduce.job.reduce.slowstart.completedmaps来调节
- ReduceTask启动之后,会启动fetch线程来抓取数据。默认情况下,每一个ReduceTask最多可以启动5个fetch线程来抓取数据。可以通过属性 mapreduce.reduce.shuffle.parrellelcopies来调节
- fetch启动之后,会通过http请求的get请求来获取数据,在请求的时候,会携带参数表示当前的分
区号。MapTask在收到请求之后,根据参数(分区号)来解析 file.out.index 文件,从中获取指定分区的位置,然后才会读取 file.out ,将对应分区的数据返回 - fetch线程在抓取到数据之后,会先对数据进行大小判断。如果数据超过了ReduceTask的缓冲区阈值,那么会将数据直接以文件形式写到磁盘上;如果没有超过ReduceTask的缓冲区阈值,那么就
先放到缓冲区中
1. 缓冲区的大小由属性 mapreduce.reduce.shuffle.input.buffer.percent 来决定,默认
是0.7,是ReduceTask执行过程中允许占用内存的70%
2. 缓冲区的阈值由属性 mapreduce.reduce.shuffle.merge.percent 来决定,默认是0.66,
即缓冲区大小的66% - fetch线程在抓取数据的同时,ReduceTask会启动两个后台线程将抓取来的数据进行merge,以防内存以及磁盘占用过多
- 所有的fetch线程完成之后,ReduceTask会将抓取来的所有的数据进行排序。同样,此时也是将局部有序的数据整理成整体有序的数据,所以依然采用的是归并排序(Merge Sort)
- 经过排序和merge,最终产生了一个大的临时文件来交给ReduceTask来处理。此时ReduceTask
会将相同的键对应的值放到一起,形成一个伪迭代器(本质上就是一个流,来读取刚刚产生的临时
文件),这个过程称之为分组(group)。也正因为是伪迭代器(将流包装成了迭代器,临时文件读取完
之后就会被销毁),所以只能遍历一次! - 分组完成之后,每一个键调用一次 reduce 方法,按照指定的规则来对数据进行聚合
- reduce 处理完之后,会将结果传递给 OutputFormat ,按照指定规则将数据以指定形式写出到指
定位置
流程图
常见优化方案 - 减少MapTask的溢写次数。溢写是将数据写到磁盘上,程序和磁盘交互次数越多,效率越低;此
时,如果需要提高效率,就可以考虑减少MapTask和磁盘的交互次数
1. 调节缓冲区的大小。通过 mapreduce.task.io.sort.mb 来调节,实际过程中,一般会将这
个值调节为250M~400M
2. 调节缓冲区阈值的大小。通过属性 mapreduce.map.sort.spill.percent 来调节 - 减少Merge次数。通过属性 mapreduce.task.io.sort.factor 来调节
- 增加Combiner。如果计算可以传递,那么建议在程序中使用Combiner。根据经验,使用
Combiner,大约可以提升40%的效率 - 减少Reduce。如果MapTask处理完成之后,不需要使用Reduce聚合,那么此时可以直接省略
Reduce - 合理设置ReduceTask的执行内存。默认情况下,每一个ReduceTask最多占用1G内存,如果试图
超过1G内存,就会被kill掉- 调大ReducedTask的执行内存。通过 mapreduce.reduce.memory.mb 属性来调节,单位是
MB,默认值是1024 - 调大缓冲区的占比。通过 mapreduce.reduce.shuffle.input.buffer.percent 属性来调
节 - 调大缓冲区的阈值。通过 mapreduce.reduce.shuffle.merge.percent 属性来调节
- 调大ReducedTask的执行内存。通过 mapreduce.reduce.memory.mb 属性来调节,单位是
- 增加fetch线程的数量。通过 mapreduce.reduce.shuffle.parrellelcopies 属性来调节
- 可以考虑对数据进行压缩。即将MapTask产生的结果进行压缩之后传递给ReduceTask,但是这种方案是在网络和解压效率之间进行了平衡/对比