MapReduce
执行流程
MapTask执行流程
- Read:读取阶段
- MapTask会调用InputFormat中的
getSplits
方法来对文件进行切片 - 切片之后,针对每一个Split,产生一个
RecordReader
流用于读取数据 - 数据是以
Key-Value
形式来产生,交给map
方法来处理。每一个键值对触发调用一次map
方法
- MapTask会调用InputFormat中的
- 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
中
- 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
文件!!!
- 当MapTask将所有的数据处理完成之后,会将所有的临时结果文件
ReduceTask执行流程
- 当达到启动阈值的时候,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的缓冲区阈值,那么就先放到缓冲区中
- 缓冲区的大小由属性
mapreduce.reduce.shuffle.input.buffer.percent
来决定,默认是0.7,是ReduceTask执行过程中允许占用内存的70% - 缓冲区的阈值由属性
mapreduce.reduce.shuffle.merge.percent
来决定,默认是0.66,即缓冲区大小的66%
- 缓冲区的大小由属性
- fetch线程在抓取数据的同时,ReduceTask会启动两个后台线程将抓取来的数据进行merge,以防内存以及磁盘占用过多
- 所有的fetch线程完成之后,ReduceTask会将抓取来的所有的数据进行排序。同样,此时也是将局部有序的数据整理成整体有序的数据,所以依然采用的是归并排序(Merge Sort)
- 经过排序和merge,最终产生了一个大的临时文件来交给ReduceTask来处理。此时,ReduceTask会将相同的键对应的值放到一起,形成一个伪迭代器(本质上就是一个流,来读取刚刚产生的临时文件),这个过程称之为分组(group)。也正因为是伪迭代器(将流包装成了迭代器,临时文件读取完之后就会被销毁),所以只能遍历一次!
- 分组完成之后,每一个键调用一次
reduce
方法,按照指定的规则来对数据进行聚合 reduce
处理完之后,会将结果传递给OutputFormat
,按照指定规则将数据以指定形式写出到指定位置
流程图
常见优化方案
- 减少MapTask的溢写次数。溢写是将数据写到磁盘上,程序和磁盘交互次数越多,效率越低;此时,如果需要提高效率,就可以考虑减少MapTask和磁盘的交互次数
- 调节缓冲区的大小。通过
mapreduce.task.io.sort.mb
来调节,实际过程中,一般会将这个值调节为250M~400M - 调节缓冲区阈值的大小。通过属性
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的执行内存。通过
- 增加fetch线程的数量。通过
mapreduce.reduce.shuffle.parrellelcopies
属性来调节 - 可以考虑对数据进行压缩。即将MapTask产生的结果进行压缩之后传递给ReduceTask,但是这种方案是在网络和解压效率之间进行了平衡/对比
其他
小文件问题
-
在大数据开发环境中,虽然实际处理的文件大部分都是大文件,但是依然无法避免产生小文件
-
一般而言,如果文件大小≤Block*0.8,那么此时就认为这是一个小文件。实际过程中,一般认为不超过100M的文件就是小文件
-
小文件在分布式环境下的问题
- 存储:在HDFS中,每一个小文件对应一条元数据,如果存储大量的小文件,那么会产生大量的元数据,此时会导致占用较多的内存,同时导致元数据的读写效率降低
- 计算:在MapReduce中,每一个小文件对应一个切片,每一个切片对应一个MapTask。如果需要对大量的小文件进行处理,那么就意味着需要产生大量的MapTask,导致集群的资源被同时大量的占用和释放
-
目前对小文件的处理方案无非两种:合并(merge)和打包
-
MapReduce提供了一种原生的打包方案:
Hadoop Archive
,将多个小文件打成一个.har
包# 将/txt/打成txt.har包,放到/result下 hadoop archive -archiveName txt.har -p /txt /result
数据倾斜
- 在集群中,因为处理的数据量不均等导致任务执行时间不一致而产生的等待,称之为数据倾斜
- 数据倾斜可能发生在Map端,也可能会发生在Reduce端
- Map端产生数据倾斜的直接原因:需要同时处理多个文件,且文件大小不均等,文件不可切
- Reduce产生数据倾斜的直接原因:对数据进行分类
- 数据倾斜无法避免,因为数据本身就有倾斜特性
- 理论上来说,数据倾斜产生之后可以解决,但是实际过程中,不太好解决;对于Reduce端的数据倾斜,实际过程中,可能会采用二阶段聚合方式来处理
推测执行机制
-
推测执行机制本质是MapReduce针对慢任务的一种优化。当出现慢任务的时候,MapReduce会将这个任务拷贝一份到其他节点上,两个节点同时执行相同的任务,谁先执行完谁的结果就作为最终结果,另一个没有执行完的就会被kill掉
-
慢任务的出现场景
- 任务分配不均匀:每一个节点被分配到的任务数量不同
- 节点性能不一致:每一台服务器的配置不同
- 数据倾斜:任务之间处理的数据量不均等
-
实际工作中,因为数据倾斜而导致的慢任务出现的概率更高,此时推测执行机制除了会占用更多的集群资源以外,并不能提高执行效率。因此,实际过程中,一般会考虑关闭推测执行机制
<!-- MapTask的推测执行机制 --> <property><name>mapreduce.map.speculative</name><value>true</value> </property> <!-- ReduceTask的推测执行机制 --> <property><name>mapreduce.reduce.speculative</name><value>true</value> </property>
join
- 案例:统计每一天的利润(文件:order.txt和product.txt)
- 需要同时处理多个文件,且信息来源不是只依靠单个文件就能处理,此时需要确定一个主文件,将其他的文件作为缓存文件进行处理,这个过程称之为join