MapReduce的shuffle

一、MapReduce计算模型

maprecude主要三个阶段组成:Map,shuffle,Reduce.

image.png 如图所示: Map是映射,负责数据的过滤分法,将原始数据转为键值对;redcue是合并,将具有相同的key值的value进行处理后在输出新的值作为最终结果。为了让Reduce可以并行处理map的结果,必须对Map的输出进行一定的处理分割,然后交给reduce才能并行处理。这个处理过程就是Shuffle。

整个MR的大致过程如下:

image.png

Shuffle过程包含在Map和Reduce两端,即Map shuffleReduce shuffle

Map shuffle

对于Map输出的结果进行分区(按照Reduce的并行度进行分区,也就是key值对reduce进行hash计算)、排序(按照分区Id 排序,同一分区按照key值排序 默认升序)、分割,然后将属于同一分区的输出合并在一起并写在磁盘上,最终生成一个有序的分区文件。大致流程如下:

image.png

主要经过 Partition,Collector,Sort,Spill,Merge几个阶段。

Partition

对于Map输出的每一个键值对数据,系统都会给定一个PartitonId,其值为对reduce Task的并行度取hash值得到的。用于后续reduce Task的拉取。

Collector

将带有partition信息的map端输出数据写入collector进行处理,每个Map任务不断地将键值对输出到在内存中构造的一个环形数据结构中。使用环形数据结构是为了更有效地使用内存空间,在内存中放置尽可能多的数据。

这个数据结构其实就是个字节数组,叫Kvbuffer,其里面除了有数据,还有索引数据,也就是图中的kvmeta。数据区域和索引数据区域在Kvbuffer中是相邻不重叠的两个区域,用一个分界点来划分两者,分界点不是亘古不变的,而是每次Spill之后都会更新一次。初始的分界点是0,数据的存储方向是向上增长,索引数据的存储方向是向下增长,如图所示: image.png

扫描二维码关注公众号,回复: 14267958 查看本文章

image.png 其索引指针和数据指针的跟新方式为,bufindex数据指针的更新方式是向上增长,例如bufindex初始值为0,一个Int型的key写完之后,bufindex增长为4,一个Int型的value写完之后,bufindex增长为8。

索引指针kvmeta由于其是反向的,其中包括value的起始位置、key的起始位置、partition值、value的长度,占用四个Int长度,Kvmeta的存放指针Kvindex每次都是向下跳四个“格子”,然后再向上一个格子一个格子地填充四元组的数据。比如Kvindex初始位置是-4,当第一个键值对写完之后,(Kvindex+0)的位置存放value的起始位置、(Kvindex+1)的位置存放key的起始位置、(Kvindex+2)的位置存放partition的值、(Kvindex+3)的位置存放value的长度,然后Kvindex跳到-8位置,等第二个键值对和索引写完之后,Kvindex跳到-12位置。

Kvbuffer的大小可以通过io.sort.mb设置,默认大小为100M。

这种数据结构还有一个设计就是,内存多会开始将数据写入磁盘,也就是溢写,也就是Spill(溢写)的出发条件,默认为80%(这里的80%是索引数据和数据整体占总kvbuffer的0.8),这么设计是,在Spil的同时,Map端还可以往其中写数据,进行反向写(如果反写到20%,原数据还没溢写完,就等待。)。可以通过io.sort.spill.percent调节,默认是0.8。

在溢写阶段主要的任务其实是 SortAndSpill,就是在溢写之前还有个排序。

Sort

当初发Spill这个操作时,其先要把KvBuffer中的数据按照按照升序排序(快排),移动的只是索引数据,排序结果只是kvmeta中的数据按照partition聚集在一起,并且区内按照key有序。

Spill

由于要将缓冲区的数据写出,Spill线程为会为此次溢写过程创建一个磁盘文件,(mapreduce.cluster.local.dir属性指定的目录中。),创建一个类似于"spill12.out"的文件。Spill线程根据排过序的Kvmeta挨个partition的把数据吐到这个文件中,一个partition对应的数据吐完之后顺序地吐下个partition,直到把所有的partition遍历完。一个partition在文件中对应的数据也叫段(segment)。

虽然数据按照Partition的顺序放好了,但是还需要类似索引的东西去知道某个partition的起始位置。所以有一个三元组记录某个partition对应的数据在这个文件中的索引;[起始位置,原始数据长度,压缩之后的数据长度],一个partition对应一个三元组。这些索引信息存放在内存中,如果内存放不下了,后续的信息会存放到磁盘中。其文件类似"spill12.out.index",存储中不光有索引数据,还有crc32的校验数据。

所以每一次Spill都会至少生成一个out文件,有时还会生成index文件(先存在内存中)。其文件名是按照spill次数命名的。

索引文件和数据文件的对应关系如图:

image.png

上面提到数据达到80%以后开始溢写,而Map依然可以把数据写入KVbuffer中,那么是继续写呢,还是怎么样,如果继续写,那么bufindex和kvmeta很快就会碰头,之后要么重新开始,要么移动内存都比较麻烦,所以kvBuffer达到80%以后开始反向写入数据。

具体做法为如图,Map取kvbuffer中剩余空间的中间值,作为新一批数据的索引数据和数据的分界点,继续按照既定的方式去写入数据,这样当Spill溢写完之后,腾出的空间可以接着写,而不需要做任何改动操作。

image.png

Map端的数据最终都会写入磁盘,即使缓冲能放下,最终也会被刷写到磁盘上。

Combiner(可选)

Combiner在MapReduce中是可选的,如果客户端定义了Combiner(相当于的map阶段的reduce),则会在分区排序后到溢写出前自动调用Combiner,将相同的key的value进行reduce的聚合操作,这样的好处就是减少溢写到磁盘的数据量。这个过程叫“合并”。

在两个地方进行调用:如图

image.png

1.当为作业设置Combiner类后,缓存溢出线程将缓存存放到磁盘时,就会调用; 
2.缓存溢出的数量超过mapreduce.map.combine.minspills(默认3)时,在缓存溢出文件合并的时候会调用

Merge

假如Map输出的数据很大,可能会进行多次溢写,产生多个溢写文件,分布在不同的磁盘上。merge最后将这些文件合并。

首先,merge扫描磁盘文件,获取所有的spill文件和index文件路径存储在数组中,获取index索引信息存储在列表里。(为什么在spill前把这些信息写入内存中呢,又得重复进行两次扫描。)尤其是spill的索引的数据,超出内存的写入磁盘,现在又要都回来,不是多此一举吗? 这样做的原因,原来kvBuffer占用了大量的内存,导致剩余内存容量变小,现在spill结束,kvbuffer不再使用并进行回收,有内存空间来存储这些数据。

接着,为merge这个过程创建两个文件file.out和file.out.index存储最终的数据和索引。合并是一个partition一个partition进行的合并输出。具体来说,就是对于某个Partition而言,从索引列表中查询这个partition对应的所有索引信息,每个索引信息都对应一个段(某个spill文件对应的某个分区的数据)插入到段列表中,其实也就是这个分区的索引信息查出,放到segment列表中,这个列表记录着所有的spill文件对应这个partition的数据的文件名,起始位置,长度等等。

最后,对这个partition对应的所有的segment进行合并,目标合并成一个segment。最终的索引文件任然输出到index文件中。

Reduce shuffle

当mapreduce任务提交后,reduce task就不断通过RPC从JobTracker那里获取map task是否完成的信息,如果获知某台TaskTracker上的map task执行完成,会通知父TaskTracker状态已经更新,TaskTracker进而通知JobTracker(这些通知在心跳机制中进行),Shuffle的后半段过程就开始启动。

主要经过 Copy,SortMergr几个阶段。

Copy

Reduce任务通过HTTP向各个MapTask所在的TaskTracker拖取它所需要的数据。Map任务成功完成后,会通知父TaskTracker状态已经更新,TaskTracker进而通知JobTracker(这些通知在心跳机制中进行)。所以,对于指定作业来说,JobTracker能记录Map输出和TaskTracker的映射关系。Reduce会定期向JobTracker获取Map的输出位置,一旦拿到输出位置,Reduce任务就会从此输出对应的TaskTracker上复制输出到本地,而不会等到所有的Map任务结束。

Merge Sort

这里的merge和map端的一样,只是数组中存放的不同map端copy过来的数值。Copy过来的数据会先放入内存缓冲区中,这里缓冲区的大小要比map端的更为灵活,它是基于JVM的heap size设置,因为shuffler阶段reducer不运行,所以应该把绝大部分的内存都给shuffle用。

merge总共有三种形式: -内存到内存、内存到磁盘、磁盘到磁盘

默认情况下,第一种形式不启用,其意思是如果内存缓冲区中能放得下这次数据的话就直接把数据写到内存中,即内存到内存merge

第二种merge方式是一直运行的,直到没有map端的数据时才结束。具体来说就是,Reduce要向每个Map去拖取数据,在内存中每个Map对应一块数据,当内存缓存区中存储的Map数据占用空间达到一定阈值时,开始启用内存中merge,把内存中的merge输出到磁盘文件上(解释一下这里的merge,因为针对同一个Reduce Task来说,会有不同的key值,即每个Map的数据块中存放着不同的key数据,这里merge将相同key进行merge)。在将buffer中多个map输出合并写入磁盘之前,如果设置了Combiner,则会化简压缩合并的map输出。

相关配置:

Reduce的内存缓冲区可通过mapred.job.shuffle.input.buffer.percent配置,默认是JVM的heap size的70%。内存到磁盘merge的启动门限可以通过mapred.job.shuffle.merge.percent配置,默认是66%。

第二种形式结束完成后,启动第三种磁盘到磁盘的形式。具体来说,当属于该reducer的map输出全部拷贝完成,则会在reducer上生成多个文件(如果拖取的所有map数据总量都没有内存缓冲区,则数据就只存在于内存中),这时开始执行合并操作,即磁盘到磁盘merge,即归并排序,也就是reduce的sort过程。一般Reduce是一边copy一边sort,即copy和sort两个阶段是重叠而不是完全分开的。最终Reduce shuffle过程会输出一个整体有序的数据块。

经过上面的理解之后,就可以很容易的理解下面这张图了。

shuffle流程图。 image-20220320204438216.png

注意:图里关于Combiner少了一处,在reduce shuffle阶段,缓存溢写磁盘时,可有进行combiner。

猜你喜欢

转载自juejin.im/post/7108367423111168014