流程原理
1 Map
1.1 map
根据输入,形成切片,并行运行MapTask。(MapTask并行度由切片数决定,每个切片对应一个MapTask)
1.2 partition
map()函数处理后得到的键值对在写入环形缓冲区前,需要先进行分区操作。(MR默认提供的分区类是HashPartitioner,可以通过继承Partitioner类并重新getPartition()方法来自定义分区)
分区数应该和ReduceTask数量一致。(可以通过job.setNumReduceTasks()设置,不设置的话默认只有一个ReduceTask,最终产生一个分区)
1.3 写入环形缓冲区
每个MapTask都会分配一个环形缓冲区,用来暂时存储map任务输出的键值对以及相应的partition信息。通过将键值对结果先写入缓冲区,避免频繁产生磁盘IO降低效率。
1.4 spill
当缓冲区内容达到阈值时(默认80%),就会锁定这80%的内存,并先根据分区排序,再在每个分区内对其中的键值对按key进行排序(快速排序)。(排序结果为缓冲区内的数据按partition为单位聚集在一起,同一partition内数据按key有序)
排序完成后会创建并写入一个临时的溢写文件。(如果自定义了Combiner,则会在分区排序后溢写前调用Combiner的reduce()函数)
缓冲区中剩余的20%内存在此阶段可以继续写入MapTask输出的键值对,写满时MapTask输出阻塞。
1.5 merge
当MapTask处理的数据量很大时会产生多个溢写文件,此时需要将同一个MapTask产生的多个溢写文件合并(归并排序),以最终形成一个已分区已排序的大文件。(如果自定义了Combiner,这个过程也会合并相同的键值的键值对)(在写磁盘的时候也可以采用压缩的方式将MapTask的输出结果进行压缩来减少网络开销,Reduce阶段在合并时会进行解压缩操作)
溢出文件合并完毕后,MapTask将删除所有的临时溢写文件,并告知NodeManager任务已完成,只要其中一个MapTask完成,ReduceTask就开始复制它的输出。
2 Reduce
2.1 copy
ReduceTask启动一些数据复制线程,通过HTTP方式请求MapTask所在的NodeManager以获取输出文件。ReduceTask通常需要集群上若干个MapTask的输出作为其接受分区的分区文件。而每个map任务的完成时间可能不同,因此只要有一个任务完成,ReduceTask就开始复制其输出。(ReduceTask的复制线程数默认为5,并行去取MapTask的输出)
2.2 merge
复制过来的数据会先放入内存缓冲区中,如果内存缓冲区中能放得下这次数据的话就直接把数据写到内存中。当内存缓存区中存储的Map数据占用空间达到一定程度的时候,则会把内存中的数据合并输出到磁盘文件中。与Map端的溢写类似,在将缓冲区中多个Map输出合并写入磁盘之前,如果设置了Combiner,则会通过reduce()函数合并Map输出。(Reduce的内存缓冲区可通过mapred.job.shuffle.input.buffer.percent配置,默认是JVM的heap size的70%)(将内存缓冲区的数据merge到磁盘文件的过程和copy过程同时运行,直到Map端的数据全部拷贝完成才结束)
当属于该Reducer的Map输出全部拷贝完成,则会在Reducer上生成多个文件(如果拉取复制的所有Map数据总量没有超过内存缓冲区大小,则数据只存在于内存中),然后执行合并操作(将已经各自排好序的Map输出文件根据键值进行归并排序),最终会输出一个整体有序的数据块。
2.3 reduce
当一个ReduceTask完成复制和排序后,就会针对已根据键值排序好的key构造对应的value迭代器。这时就要用到分组,默认根据键分组,也可以自定义分组函数类,并通过job.setGroupingComparatorClass()方法设置。对于默认分组来说,只要比较器比较的两个key值相等,它们就属于同一组,它们的value就会放在一个value迭代器,而这个迭代器的key使用属于同一个组的所有key的第一个key。
reduce()方法的输入是key和它的value迭代器。此阶段的输出直接写到输出文件系统,如果采用的文件系统是HDFS,由于NodeManager通常也运行数据节点,所以第一个块副本将被写到本地磁盘。ReduceTask为每个key和它的value迭代器 <key, (list of values)>对调用一次 reduce()方法,且最终的输出是没有排序的。
优化
其他
1 MR的shuffle和Spark的shuffle:
1.1 Spark的shuffle
shuffle的本质是将各个节点的同一类数据汇集到某一个节点进行计算。Spark中的shuffle操作
有hash shuffle和sort shuffle两种,而两种模式的主要差异是在shuffle write阶段(map端shuffle)
1.1.1 hash shuffle
MR的shuffle将排序作为固定步骤,有许多并不需要排序的任务,MR也会对其进行排序,造成了不必要的开销。Spark基于hash的shuffle实现方式中,MapTask会为每个ReduceTask生成一个文件,这样容易产生大量文件。(即对应M*R个中间文件,其中, M表示Mapper阶段的Task个数,R表示 Reducer阶段的Task个数,伴随大量的随机磁盘I/O与内存开销)
为缓解该问题,Spark 0.8.1为hash shufflr引入了shuffle consolidate机制,合并Mapper端生成的中间文件。
1.1.2 sort shuffle
基于sort的shuffle中,每个MapTask不会为每个ReduceTask生成一个单独的文件,而是全部写到一个数据文件中,同时生成一个索引文件, ReduceTask通过索引文件获取相关的数据。最终生成的文件个数减少到 2*M。
- 普通模式:
- bypass模式:Reducer 端任务数比较少的情况下,hash shuffle的实现机制明显比sort shuffle 实现机制快,因此sort shuffle提供了一个hash风格的机制,就是 bypass模式。Reducer端任务数少于配置属性spark.shuffle.sort.bypassMergeThreshold设置的个数时,使用该模式。此时,每个task会为每个下游task都创建一个临时磁盘文件,并将数据按key进行hash然后根据 key的hash值,将key写入对应的磁盘文件中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。该过程的磁盘写机制和未经优化的hash shuffle一模一样,都要创建大量磁盘文件,只是最后会做一个磁盘文件的合并。因此少量的最终磁盘文件,让该机制相对未经优化的hash shuffle来说,shuffle read的性能更好。而该模式与普通模式的不同在于:一是磁盘写机制不同;二是shuffle write过程中不会进行数据的排序操作,节省了排序的性能开销。
-
Tungsten Sort模式: