目录

Shuffle 基本认识

shulle 过程简述

shuffle 过程详解:

Map端 shuffle

1.Map端选择输出主类(构建环形缓冲区,初始化缓冲区及定义分区)

2.往环形缓冲区中写入数据

3.触发溢写

4.Spill 过程

 排序

溢写

flush

5.Merge

Reduce  shuffle

6.reduceShuffle 启动

7.reduce copy

8. reduce merge


Shuffle 基本认识

何为shuffle:map端的数据传递给reduce端的流程。总体来说即为将map端的输出数据进行分区,排序,缓存然后分发给reduce端,然在reduce端进行归并,分组

shulle 过程简述

官网图:

hadoop shuffle的概念 hadoop中shuffle过程_大数据

流程简述:
1.map task 根据分片信息从文件中读入数据

2.maptask 调用map 方法进行业务逻辑运算

3.一个maptask 对应一个缓存区,在缓存过程中会进行分区、排序、溢写、归并排序(多个溢写文件变成一个)

4.如果存在combiner,combiner会在本地对归并排序后的文件进行合并,减少传输压力 。

5.当一个maptask执行完成之后,对应分区的reducetask就会从maptask 获取溢写文件,并把溢写文件放入内存缓冲中,如果内存放不下,溢写到磁盘。对数据进行归并排序和分组。每一组数据传递给一个reduce方法

 

shuffle 过程详解:

Map端 shuffle

1.Map端选择输出主类(构建环形缓冲区,初始化缓冲区及定义分区)

yarnchild进程调用runNewMapper方法,根据ReduceTask数目选择创建Mapper端的输出主类。即OutputCollectoer类。通过outputCollectoer类中的构造方法先会执行collector = createSortingCollector(job, reporter),在createSortingCollector类的构造方法中执行collector.init(context);该方法会对缓冲区进行初始化。当初始化完成后会在OutputCollectoer构造方法中设置分区数目。

在collector.init方法中定义了一个kvbuffer。kvbuffer的环形缓冲功能由MapOutputBuffer内部类BlockingBuffer实现。

在BlockingBuffer中定义了内部类Buffer,Buffer的write()方法,write()方法中的主要实现为根据缓冲区中插入前的位置+插入数据的长度大于缓冲区总长度时,将多余部分移到buffer开始的位置。为了保证key连续,在BlockingBuffer类中实现了对buffer的reset操作。

定义了serializationFactory类,通过执行serializationFactory.getSerializer()分别对 kv序列化后,通过keySerializer.open(bb)和valSerializer.open(bb),把kv通过Buffer的write()方法最终写入kvbuffer。

初始化包括设置缓冲区溢写阈值,缓冲区大小,从缓冲区往磁盘写文件时所用的排序方法(定义的排序方法为快速排序),排序时用的比较器,combine的combineCollector 初始化(不存在时设置为null),溢写小文件合并数量阈值(设置的是当溢写小文件大于三时会发生合并)。

设置溢写阈值代码:job.getFloat(JobContext.MAP_SORT_SPILL_PERCENT, (float)0.8);

可以通过mapred-site.xml:mapreduce.task.io.sort.mb 改变其值

设置缓冲区大小:final int sortmb = job.getInt(JobContext.IO_SORT_MB, 100);

可以通过mapred-site.xml:mapreduce.map.sort.spill.percent改变其值

设置溢写文件时用的排序算法:sorter = ReflectionUtils.newInstance(job.getClass("map.sort.class", QuickSort.class, IndexedSorter.class), job)

溢写文件阈值:minSpillsForCombine = job.getInt(JobContext.MAP_COMBINE_MIN_SPILLS, 3)

2.往环形缓冲区中写入数据

之后在runNewMapper方法中会执行mapper.run(mapperContext)调用到我们自定义的mapper类。在我们自定义的mapper类中调用map方法执行业务逻辑,最后执行context.write()。通过context.write()会转到TaskInputOutputContext类中的write方法执行output.write()。

通过output.write()方法转到OutputCollectoer类中的write()方法。在OutputCollectoer的write方法中会执行collector.collect(key, value, this.partitioner.getPartition(key, value, this.partitions))。该方法会往对应的分区中写入kv数据。(这一步主要是把k,v变成了k,v,p)

collector 对象是MapOutputCollector类型的,而MapOutputBuffer实现了MapOutputCollector接口,所以实际OutputCollectoer的write方法会调用MapOutputBuffer的collect()方法。

环形缓冲区中不光放置了数据,还有一些索引数据(Kvmeta)。数据区域和索引区域在kvbuffer中是两个互不重叠的区域,两者通过一个分界点来划分(equator),每次spill后分界点都会更新。初始的分界点是0,数据的存储方向是向上增长,索引数据的存储方向是向下增长。如下图所示:

hadoop shuffle的概念 hadoop中shuffle过程_大数据_02

 

3.触发溢写

在MapOutputBuffer的collect方法中根据索引区来检查是否需要触发spill。

当kvoffsets满了(kvnext==kvstart),或者kvoffsets的使用达到上限(kvstart==kvend)时在执行startSpill()方法告诉SpillThread开始溢出。

 

当触发溢写操作之后,Map取kvbuffer中剩余空间的中间位置,用这个位置设置为新的分界点,bufindex指针移动到这个分界点,Kvindex移动到这个分界点的-16位置。分界点的转换如下图:

hadoop shuffle的概念 hadoop中shuffle过程_mapreduce_03

4.Spill 过程

SpillThread的run方法中会执行sortAndSpill()方法,sortAndSpill()方法会执行new SpillRecord(partitions)创建一个溢写对象spillRec。每隔溢写文件会对应一个spillRec对象。

SpillRecord里面缓存的是一个一个的记录,所以并不是一整块无结构字节流,而是以IndexRecord为基本单位组织起来的IndexRecord记录是分区的位子信息,描述了一个记录在缓存中的起始偏移、原始长度、实际长度(可能压缩)等信息

 排序

在本地磁盘创建一个spill文件(文件名为:TaskTracker.OUTPUT + "/spill" + spillNumber + ".out"),排序。排序中获取分区索引值,如果分区不同,先按分区排序。如果分区相同,按照key进行排序。排序方法默认快排,另外,排序的规则是对Key进行比较,这里采用的比较对象就是RawComparator<K> comparator。排序仅仅移动kvoffsets。具体实现见下图:

MapOutputBuffer类中是定义了一个三个缓冲区,分别是:  int [] kvoffsets, int[] kvindices, byte[] kvbuffer。

kvoffsets是索引缓冲区,它的作用是用来记录kv键值对在kvindices中的偏移位置信息。

kvindices也是一个索引缓冲区,索引区的每个单元包含了分区号,k,v在kvbuffer中的偏移位置信息。

kvbuffer 是环形缓冲区

hadoop shuffle的概念 hadoop中shuffle过程_大数据_04

 

kvoffsets作为一级索引,一个用途是用来表示每个k,v在kvindices中的位置,另一个是用来统计当前索引的缓存的占用比,当超过设定的阀值(也是0.8),就会触发spill动作,将已写入的数据区间spill出去,新写入的时候持续向后写入,当写到尾部后,回过头继续写入。

 kvindices这样结构表示是为了在指定了多个reducetask的时候,maptask的输出需要进行分区,比如有2个reducetask,那么需要将maptask的输出数据均衡的分布到2个reducetask上,因此在索引里引入了分区信息,另外一个是为了每个分区的key有序,避免直接在比较后直接拷贝key,而只要相互交换一下整形变量(kvoffsets值)即可。

 kvbuffer存储了实际的k,v,为了保证k,v的键值成对的出现,引入了mark标记上一个完成的k,v的位置。同时类似kvoffset一样也加入了表示缓冲区是否满足溢出的一些标志。还有一点就是,k,v的大小不向索引区一样明确的是一对占一个int,可能会出现尾部的一个key被拆分两部分,一步存在尾部,一部分存在头部,但是key为保证有序会交给RawComparator进行比较,而comparator对传入的key是需要有连续的,那么由此可以引出key在尾部剩余空间存不下时,如何处理。处理方法是,当尾部存不下,先存尾部,剩余的存头部,同时在copy key存到接下来的位置,但是当头部开始,存不下一个完整的key,会溢出flush到磁盘。当碰到整个buffer都存储不下key,那么会抛出异常MapBufferTooSmallException表示buffer太小容纳不小。

溢写

执行new IndexRecord 创建一个IndexRecord对象。

执行for ( int i = 0 ; i < partitions ; ++ i )。

如果设置了combineclass则调用combinerRunner.combine中调用sortAndCombine处理区分partition处理kvs。

如果没有,获取分区偏移量segmentStart。segment对象中就是一个分区在文件中对应的数据段。

最后通过执行writer.close(); 将key和value对应的两个buffer写到溢写文件中。

执行spillRec.putIndex(rec, i)把溢写的索引信息,封装到spillRec对象里。

写完了Spill文件后,还会把SpillRec的内容写入成一个Spill索引文件,不过这个写不是一个Spill文件就对应于一个索引文件,而是根据累计的index信息数据量超过了一个界限(mapreduce.task.index.cache.limit.bytes=1MB)再来决定是否新生成一个index文件

执行  ++numSpills;为下一次溢写进行统计数字,这个数字与生成的溢写文件名称有关系。

flush

当Map输入数据处理完毕之后,在OutputCollectoer里面执行collector的flush方法。flush方法主要是对kvbuffer内剩余还没有Spill的数据进行Spill,全部刷写到磁盘后,给SpillThread线程发送暂停信号,等待SpillThread关闭(SpillThread.join)。之后,我们得到了N个Spill文件以及多个索引文件,于是需要按照分区归并成分区数量个文件,调用mergeParts方法.

5.Merge

在mergeParts里,首先获得这些Spill文件的文件名,如果numSpills=1,那么Spill文件相当于就是要Map输出的文件,因为在Spill内部已经进行了排序。而且因为没有多余的Spill文件需要归并,所以重命名文件名即可。

Map输出文件名为output/file.out和output/file.out.index

如果多于一个Spill文件,则需要进行归并处理。

首先将全部索引数据从文件中读出来,加入到indexCacheList数组里,一个partition一个partition的进行合并输出。

indexCacheList数组里存放spillRec对象的数组,可以方便地找到某个分区在各个Spill文件中的位置,以便进行归并处理

归并完毕后,将其写入文件。如果设置了combiner,会在这里执行combinerRunner.combine(kvIter, combineCollector)对数据再进行一次合并再写入文件。最后输出一个file.out的文件和一个叫file.out.Index的文件用来存储最终的输出和索引。

归并流程具体实现见下图:

对于某个partition来说,从索引列表中查询这个partition对应的所有索引信息,每个对应一个段插入到段列表中。也就是这个partition对应一个段列表,记录所有的Spill文件中对应的这个partition那段数据的文件名、起始位置、长度等等。然后对这个partition对应的所有的segment进行合并,目标是合并成一个segment。当这个partition对应很多个segment时,会分批地进行合并:先从segment列表中把第一批取出来,以key为关键字放置成最小堆,然后从最小堆中每次取出最小的输出到一个临时文件中,这样就把这一批段合并成一个临时的段,把它加回到segment列表中;再从segment列表中把第二批取出来合并输出到一个临时segment,把其加入到列表中;这样往复执行,直到剩下的段是一批,输出到最终的文件中。最终的索引数据仍然输出到Index文件中。下图为partition合并过程:

hadoop shuffle的概念 hadoop中shuffle过程_数据_05

以上为map阶段shuffle。在Reduce端,shuffle主要分为复制Map输出、排序合并两个阶段。

Reduce  shuffle

6.reduceShuffle 启动

当map task 完成比例大于0.05(该参数存在于MRJobConfig类中,由mapreduce.job.reduce.slowstart.completedmaps指定)时且Reduce Task可允许占用的资源能够折合成整数个任务时,ContainerAllocator类会为Reduce Task申请资源。(资源的数目由MRJobConfig类中的yarn.app.mapreduce.am.job.reduce.rampup.limit指定),启动比率为0.5。

7.reduce copy

当启动reducetask之后,reducetask会执行run方法,如果不是本地模式,会构造一个ReduceCopier对象,调用fetchOutputs方法来拷贝map端输出。拷贝数据的线程叫fetchers。mapreduce.reduce.shuffle.parallelcopies参数决定了fetchers的个数,默认5个。

Fetcher线程先连接运行Map服务器的Shuffle监听程序,获取ShuffleHead。其中包括Map 编号,解压后数据大小,压缩后数据大下,对应Reduce编号。

Fetcher获取到信息后执行mapOutput = merger.reserve(mapId, decompressedLength, id);,在merger.reserve方法中会canShuffleToMemory方法,判断拿到的map结果是否可以放入内存。

private boolean canShuffleToMemory(long requestedSize) {
     return (requestedSize < maxSingleShuffleLimit); 
   }

requestedSize 是pull的map结果大小

maxSingleShuffleLimit:单个shuffle任务能使用的内存限额 其值为:Shuffle内存为总内存 * 0.7*0.25

1)当pull的map结果大小>maxSingleShuffleLimit,数据放入磁盘。

2)当pul的map结果大小<maxSingleShuffleLimit,当前已用的内存是否超过了内存限制,没超过,放内存。

2)当pul的map结果大小<maxSingleShuffleLimit,当前已用的内存是否超过了内存限制,超过了,等待

maxSingleShuffleLimit计算逻辑如下:
this.maxSingleShuffleLimit = (long)(memoryLimit * singleShuffleMemoryLimitPercent);

singleShuffleMemoryLimitPercent取决于参数mapreduce.reduce.shuffle.memory.limit.percent,默认为0.25。

memoryLimit:shuffle 可以使用的总内存大小  其值=Shuffle内存为总内存 * 0.7

memoryLimit计算逻辑如下:
this.memoryLimit = (long)(jobConf.getLong(MRJobConfig.REDUCE_MEMORY_TOTAL_BYTES,Runtime.getRuntime().maxMemory()) * maxInMemCopyUse);
maxInMemCopyUse取决于参数mapreduce.reduce.shuffle.input.buffer.percent,默认0.70f。
MRJobConfig.REDUCE_MEMORY_TOTAL_BYTES取决于参数mapreduce.reduce.memory.totalbytes,默认值其实是reduce进程当前最大可用内存。

merge返回InMemoryMapOutput或者是OnDiskMapOutput对象, 如果返回null,则再从ShuffleScheduler取任务。据拷贝完成后,copySucceeded的output.commit()时进行判断是否需要进行merge。

ShuffleScheduler结构中内容Map<String, MapHost> mapLocations告诉我们MapTask的分布情况,Set<MapHost> pendingHosts则是告诉fetchers处理的host和mapid。MapHost 结构中存储的信息为host、url和一堆mapid。

8. reduce merge


Merge主要分为三类:


IntermediateMemoryToMemoryMerger      inMemoryMergedMapOutputs    内存到内存


InMemoryMerger                                        inMemoryMapOutputs                内存到磁盘


OnDiskMerger                                            onDiskMapOutputs                     磁盘到磁盘


 

Copy过来的数据会先放入内存缓冲区中,如果内存中的map输出文件个数达到阈值,出现内存到内存merge,这种合并将内存中的map输出合并,然后再写入内存。


首先mapreduce.reduce.merge.memtomem.enabled控制是否开启内存到内存merge功能,默认关闭,


其次mapreduce.reduce.merge.memtomem.threshold控制内存中map输出文件个数阈值,默认1000


 


当内存缓存区中存储的Map数据占用空间达到一定程度的时候,开始把内存中的数据merge输出到磁盘上一个文件中,即内存到磁盘merge。在将buffer中多个map输出合并写入磁盘,如果设置了Combiner,则会化简压缩合并的map输出。

Reduce的内存缓冲区大小可通过mapred.job.shuffle.input.buffer.percent配置,默认是JVM的heap size的70%。

heap size 通过mapreduce.admin.reduce.child.java.opts来设置,通常为Xmx1024m

内存到磁盘merge的启动门限:

通过mapred.job.shuffle.merge.percent配置,默认是66%

或达到Map任务在缓存溢出前能够保留在内存中的输出个数的阈值(由mapreduce.reduce.merge.inmem.threshold控制,默认1000)

当属于该reducer的map输出全部拷贝完成,则会在reducer上生成多个文件,这时开始执行合并操作,即磁盘到磁盘merge。

如果拖取的所有map数据总量都没有触发阈值,则数据就只存在于内存中

当文件数量以(2*task.io.sort.factor-1)上升 , 但是合并不超过mapreduce.task.io.sort.factor 的文件数量时会触发磁盘到磁盘溢写。合并因子:mapreduce.task.io.sort.factor设置 默认10

Map的输出数据已经是有序的,Merge进行一次合并排序,所谓Reduce端的sort过程就是这个合并的过程。

如果map的输出结果进行了压缩,则在合并过程中,需要在内存中解压后才能给进行合并

Reduce是一边copy一边sort,即copy和sort两个阶段是重叠而不是完全分开的。

当所有map输出都拷贝完毕之后,所有数据被最后合并成一个整体有序的文件,作为reduce任务的输入。

这个合并过程是一轮一轮进行的,最后一轮的合并结果直接推送给reduce作为输入,节省了磁盘操作的一个来回。最后(map输出都拷贝到reduce之后)进行合并的map输出可能来自合并后写入磁盘的文件,也可能来及内存缓冲,在最后写入内存的map输出可能没有达到阈值触发合并,所以还留在内存中。

每一轮合并不一定合并平均数量的文件数,指导原则是使用整个合并过程中写入磁盘的数据量最小,为了达到这个目的,则需要最终的一轮合并中合并尽可能多的数据,因为最后一轮的数据直接作为reduce的输入,无需写入磁盘再读出。因此我们让最终的一轮合并的文件数达到最大。

比如,如果有40个map输出,而合并因子是10(10为默认设置,由mapreduce.task.io.sort.factor属性设置,与map的合并类似),合并将进行4趟,最后有4个中间文件。其中每趟合并的文件数实际上并不是每次合并10个文件。这40个文件并不会在四趟中每趟合并10个文件从而得到4个文件。相反,第一趟只合并4个文件,随后的三趟合并完整的10个文件。在最后一趟中,4个已合并的文件和余下的6个(未合并)文件总共10个文件。目标是合并最小数量的文件来满足最后一趟的合并系数。

这并没有改变合并次数,它只是一个优化措施,目的是尽量减少写到磁盘的数据量,因为最后一趟总是直接将数据输入reduce函数中,从而省略了一次磁盘往返行程。

 

 

参考链接:

Mapper shuffle

(适合初学者有个深入了解框架)

(对mapper端的源码分析写的还不错)

(绝了,强烈建议认真阅读,多看几遍不为过)

reduce shuffle

(比较详细带shuffle 过程内存溢出调优)

https://forum.huawei.com/enterprise/zh/thread-372243.html

(这篇讲的最细)