easy-algorithm-interview-an.../bigdata/hadoop/MapReduce Shuffle详解.md

10 KiB
Raw Blame History

Shuffle过程是MapReduce的核心也被称为奇迹发生的地方。要想理解MapReduceShuffle是必须要了解的。我看过很多相关的资料但每次看完都云里雾里的绕着很难理清大致的逻辑反而越搅越混。前段时间在做MapReduce job 性能调优的工作需要深入代码研究MapReduce的运行机制这才对Shuffle探了个究竟。考虑到之前我在看相关资料而看不懂时很恼火所以在这里我尽最大的可能试着把Shuffle说清楚让每一位想了解它原理的朋友都能有所收获。如果你对这篇文章有任何疑问或建议请留言到后面谢谢

Shuffle的正常意思是洗牌或弄乱可能大家更熟悉的是Java API里的Collections.shuffle(List)方法它会随机地打乱参数list里的元素顺序。

在Hadoop这样的集群环境中大部分map task与reduce task的执行是在不同的节点上。当然很多情况下Reduce执行时需要跨节点去拉取其它节点上的map task结果。如果集群正在运行的job有很多那么task的正常执行对集群内部的网络资源消耗会很严重。这种网络消耗是正常的我们不能限制能做的就是最大化地减少不必要的消耗。还有在节点内相比于内存磁盘IO对job完成时间的影响也是可观的。从最基本的要求来说我们对Shuffle过程的期望可以有
1.完整地从map task端拉取数据到reduce 端。
2.在跨节点拉取数据时,尽可能地减少对带宽的不必要消耗。
3.减少磁盘IO对task执行的影响。

这里写图片描述
这里写图片描述

Map端

整个流程我分了四步。简单些可以这样说每个maptask都有一个内存缓冲区存储着map的输出结果当缓冲区快满的时候需要将缓冲区的数据以一个临时文件的方式存放到磁盘当整个map task结束后再对磁盘中这个map task产生的所有临时文件做合并生成最终的正式输出文件然后等待reduce task来拉数据。

当然这里的每一步都可能包含着多个步骤与细节,下面我对细节来一一说明:

1.在map task执行时它的输入数据来源于HDFS的block当然在MapReduce概念中map task只读取split。Split与block的对应关系可能是多对一默认是一对一。在WordCount例子里假设map的输入数据都是像“aaa”这样的字符串。

2.在经过mapper的运行后我们得知mapper的输出是这样一个key/value对 key是“aaa” value是数值1。因为当前map端只做加1的操作在reduce task里才去合并结果集。前面我们知道这个job有3个reduce task到底当前的“aaa”应该交由哪个reduce去做呢是需要现在决定的。

MapReduce提供Partitioner接口它的作用就是根据key或value及reduce的数量来决定当前的这对输出数据最终应该交由哪个reduce task处理。默认对key hash后再以reduce task数量取模。默认的取模方式只是为了平均reduce的处理能力如果用户自己对Partitioner有需求可以订制并设置到job上。

在我们的例子中“aaa”经过Partitioner后返回0也就是这对值应当交由第一个reducer来处理。接下来需要将数据写入内存缓冲区中缓冲区的作用是批量收集map结果减少磁盘IO的影响。我们的key/value对以及Partition的结果都会被写入缓冲区。当然写入之前key与value值都会被序列化成字节数组。

整个内存缓冲区就是一个字节数组它的字节索引及key/value存储结构我没有研究过。如果有朋友对它有研究那么请大致描述下它的细节吧。

3.这个内存缓冲区是有大小限制的默认是100MB。当map task的输出结果很多时就可能会撑爆内存所以需要在一定条件下将缓冲区中的数据临时写入磁盘然后重新利用这块缓冲区。这个从内存往磁盘写数据的过程被称为Spill中文可译为溢写字面意思很直观。这个溢写是由单独线程来完成不影响往缓冲区写map结果的线程。溢写线程启动时不应该阻止map的结果输出所以整个缓冲区有个溢写的比例spill.percent。这个比例默认是0.8也就是当缓冲区的数据已经达到阈值buffer size * spill percent = 100MB * 0.8 = 80MB溢写线程启动锁定这80MB的内存执行溢写过程。Map task的输出结果还可以往剩下的20MB内存中写互不影响。

当溢写线程启动后需要对这80MB空间内的key做排序(Sort)。排序是MapReduce模型默认的行为这里的排序也是对序列化的字节做的排序。

在这里我们可以想想因为map task的输出是需要发送到不同的reduce端去而内存缓冲区没有对将发送到相同reduce端的数据做合并那么这种合并应该是体现是磁盘文件中的。从官方图上也可以看到写到磁盘中的溢写文件是对不同的reduce端的数值做过合并。所以溢写过程一个很重要的细节在于如果有很多个key/value对需要发送到某个reduce端去那么需要将这些key/value值拼接到一块减少与partition相关的索引记录。

在针对每个reduce端而合并数据时有些数据可能像这样“aaa”/1 “aaa”/1。对于WordCount例子就是简单地统计单词出现的次数如果在同一个map task的结果中有很多个像“aaa”一样出现多次的key我们就应该把它们的值合并到一块这个过程叫reduce也叫combine。但MapReduce的术语中reduce只指reduce端执行从多个map task取数据做计算的过程。除reduce外非正式地合并数据只能算做combine了。其实大家知道的MapReduce中将Combiner等同于Reducer。

如果client设置过Combiner那么现在就是使用Combiner的时候了。将有相同key的key/value对的value加起来减少溢写到磁盘的数据量。Combiner会优化MapReduce的中间结果所以它在整个模型中会多次使用。那哪些场景才能使用Combiner呢从这里分析Combiner的输出是Reducer的输入Combiner绝不能改变最终的计算结果。所以从我的想法来看Combiner只应该用于那种Reduce的输入key/value与输出key/value类型完全一致且不影响最终结果的场景。比如累加最大值等。Combiner的使用一定得慎重如果用好它对job执行效率有帮助反之会影响reduce的最终结果。

4.每次溢写会在磁盘上生成一个溢写文件如果map的输出结果真的很大有多次这样的溢写发生磁盘上相应的就会有多个溢写文件存在。当map task真正完成时内存缓冲区中的数据也全部溢写到磁盘中形成一个溢写文件。最终磁盘中会至少有一个这样的溢写文件存在(如果map的输出结果很少当map执行完成时只会产生一个溢写文件)因为最终的文件只有一个所以需要将这些溢写文件归并到一起这个过程就叫做Merge。Merge是怎样的如前面的例子“aaa”从某个map task读取过来时值是5从另外一个map 读取时值是8因为它们有相同的key所以得merge成group。什么是group。对于“aaa”就是像这样的{“aaa”, [5, 8, 2, …]}数组中的值就是从不同溢写文件中读取出来的然后再把这些值加起来。请注意因为merge是将多个溢写文件合并到一个文件所以可能也有相同的key存在在这个过程中如果client设置过Combiner也会使用Combiner来合并相同的key。

至此map端的所有工作都已结束最终生成的这个文件也存放在TaskTracker够得着的某个本地目录内。每个reduce task不断地通过RPC从JobTracker那里获取map task是否完成的信息如果reduce task得到通知获知某台TaskTracker上的map task执行完成Shuffle的后半段过程开始启动。

简单地说reduce task在执行之前的工作就是不断地拉取当前job里每个map task的最终结果然后对从不同地方拉取过来的数据不断地做merge也最终形成一个文件作为reduce task的输入文件。

这里写图片描述
这里写图片描述

Reduce端

如map 端的细节图Shuffle在reduce端的过程也能用图上标明的三点来概括。当前reduce copy数据的前提是它要从JobTracker获得有哪些map task已执行结束这段过程不表有兴趣的朋友可以关注下。Reducer真正运行之前所有的时间都是在拉取数据做merge且不断重复地在做。如前面的方式一样下面我也分段地描述reduce 端的Shuffle细节

1.Copy过程简单地拉取数据。Reduce进程启动一些数据copy线程(Fetcher)通过HTTP方式请求map task所在的TaskTracker获取map task的输出文件。因为map task早已结束这些文件就归TaskTracker管理在本地磁盘中。

2.Merge阶段。这里的merge如map端的merge动作只是数组中存放的是不同map端copy来的数值。Copy过来的数据会先放入内存缓冲区中这里的缓冲区大小要比map端的更为灵活它基于JVM的heap size设置因为Shuffle阶段Reducer不运行所以应该把绝大部分的内存都给Shuffle用。这里需要强调的是merge有三种形式1)内存到内存 2)内存到磁盘 3)磁盘到磁盘。默认情况下第一种形式不启用让人比较困惑是吧。当内存中的数据量到达一定阈值就启动内存到磁盘的merge。与map 端类似这也是溢写的过程这个过程中如果你设置有Combiner也是会启用的然后在磁盘中生成了众多的溢写文件。第二种merge方式一直在运行直到没有map端的数据时才结束然后启动第三种磁盘到磁盘的merge方式生成最终的那个文件。

3.Reducer的输入文件。不断地merge后最后会生成一个“最终文件”。为什么加引号因为这个文件可能存在于磁盘上也可能存在于内存中。对我们来说当然希望它存放于内存中直接作为Reducer的输入但默认情况下这个文件是存放于磁盘中的。至于怎样才能让这个文件出现在内存中之后的性能优化篇我再说。当Reducer的输入文件已定整个Shuffle才最终结束。然后就是Reducer执行把结果放到HDFS上。