一、Shuffle 简介
shuffle 的字面意思是洗牌、混洗的意思,就是把一组有规律的数据尽量打乱成无规律的数据。但在 MapReduce 中 Shuffle 更像是洗牌的逆过程,其将 Map 端输出的混乱数据按指定规则划分成有一定规律的数据,以方便 Reduce 端接收处理。MapReduce 的工作阶段主要可有分为 Map 端和 Reduce 端两个部分。
在 Map 阶段,MapReduce 会对要处理的数据进行分片(split)操作,为每一个分片分配一个 MapTask 任务,然后对每一个分片中的每一行数据进行处理得到键值对(key, value),这些键值对被称为"中间结果"。然后 Shuffle 阶段按一定规则将"中间结果"分发至对应的 Reduce 端。Reduce 端再对接收到的"中间结果"进行处理,再最后输出最终结果。
由于 Shuffle 涉及到磁盘 I/O 和网络传输,因此 Shuffle 性能的高低直接影响整个程序的运行效率。
二、MapReduce Shuffle
Hadoop 的核心思想是 MapReduce,而 shuffle 又是 MapReduce 的核心。shuffle 的主要工作是从 Map 结束到 Reduce 开始之间的过程。shuffle 阶段又可以分为 Map 端的 shuffle 和 Reduce 端的 shuffle。
2.1 Map 端 shuffle
下图是 Map 端 shuffle 的大致流程:
频繁的磁盘 I/O 会导致效率严重降低,因此"中间结果"不会立马写入磁盘,而是优先存储到 Map 节点的"环形内存缓冲区",在写入的过程中进行分区(partition),也就是对于每个键值对来说,都增加了一个 partition 属性值,然后连同键值对一起序列化成字节数组写入到缓冲区(缓冲区采用字节数组的方式进行存储,默认大小为 100M)。
当写入的数据量达到预先设置的阈值后,便会启动溢写出线程将缓冲区中的那部分数据溢写(spill)到磁盘的临时文件中,并在写入前根据 key 进行排序(sort)以及合并(combine,可选操作)。
溢出写过程按照轮询的方式将缓冲区中的内容写到 mapreduce.cluster.local.dir 属性指定的本地文件目录中。当整个 Map 任务完成后,会对这个 Map 任务在磁盘中产生的所有临时文件进行 merge 操作,生成最终的正式输出文件。这个 merge 是将所有的临时文件中相同 partition 合并到一起,并对各个 partition 中的数据再进行一次排序(sort) ,生成 key 和对应的 value-list。在 merge 时,如果溢写文件数量超过参数 min.num.spills.for.combine 的值(默认为 3)时,可以再次进行合并。
至此,Map 端的工作已经完成,最终生成的文件也会存储在 TaskTracker 能够访问的位置,每个 reduce task 不间断的通过 RPC 从 JobTracker 那里获取 map task 是否完成的信息,如果得到的信息是 map task 已经完成,那么 reduce 端的 shuffle 开始启动。
2.2 Reduce 端 shuffle
Reduce 端的 shuffle 主要包括三个阶段:copy、merge 和 reduce。

每个 reduce task 负载处理一个分区的文件,以下是 reduce task 的处理流程:
- reduce task 从每个 map task 的结果文件中拉取对应分区的数据。因为数据在 map 阶段已经分好区了,并且会有一个额外的索引文件记录每个分区的起始偏移量,所以 reduce task 取数的时候直接根据偏移量去拉取数据就可以;
- reduce task 从每个 map task 拉取分区数据的时候会进行再次合并、排序,按照自定义的 reducer 的逻辑代码去处理;
- 最后就是 reduce 过程,在这个过程中产生了最终的输出结果,并将其写到 HDFS 上;
2.3 MapReduce 过程中为什么要排序?
- key 存在 combine 操作,排序之后相同的 key 放在一起,方便合并操作;
- reduce task 是按照 key 去处理数据的,如果没有进行排序就需要扫描所有数据,再从中对相同 key 的 value 数据进行 reduce 逻辑处理,这很影响性能。排序可以很方便的得到一个 key 对应的 value 集合;
- reduce task 按照 key 去处理数据时,如果 key 是有序的,那么 reduce task 也就可以按照 key 去顺序读取,当读到的 key 是文件末尾的 key 时,就表示数据处理完毕。如果没有排序,那就还需要其他逻辑来记录哪些 key 处理完了,哪些 key 没有处理;
2.4 为什么要合并文件?
- 当写入内存的数据达到阈值时,就会溢写到文件,多次溢写就会形成很多小文件,HDFS 管理小文件会产生很多资源开销;
- 读取文件的数量增多,打开的文件句柄数也会增多;
- MapReduce 是全局有序的,单个文件有序不代表全局有序,所以需要把小文件合并在一起排序才会全局有序;
三、Spark Shuffle
Spark 的 shuffle 是在 MapReduce shuffle 的基础上进行了调优。主要是针对排序、合并逻辑做了一些优化。在 Spark 中 Shuffle write 相当于 MapReduce 的 Map,Shuffle read 相当于 MapReduce 的 reduce。
Spark 丰富了任务类型,有些任务之间数据流转不需要通过 shuffle,但是有些任务之间还是需要通过 shuffle 来传递数据,比如宽依赖的 group by key 以及各种 by key 算子。宽依赖之间会划分 stage ,stage 之间就会 shuffle,如下图中的 stage 1 和 stage 3 都发生了 shuffle。

在 Spark 中,由 ShuffleManager 负责 shuffle 过程的执行、计算和处理。随着 Spark 的发展,ShuffleManager 有两种实现方式,分别是 HashShuffleManager 和 SortShuffleManager,所以 Spark 的 shuffle 有 Hash shuffle 和 sort shuffle 两种。
3.1 Hash shuffle
HashShuffleManager 的运行机制主要分为两种,一种是普通运行机制,另一种是合并的运行机制。合并机制主要是通过复用 buffer 来优化 shuffle 过程中产生的小文件数量。Hash shuffle 是不具有排序的 shuffle。
3.1.1 普通机制
普通机制的 Hash shuffle 最开始使用的是 Hash Based Shuffle,每个 Mapper 会根据 Reducer 的数量创建对应的 bucket,bucket 的数量是 M * R,M 是 Map 的数量,R 是 Reduce 的数量。如下图所示:2 个 core ,4 个 map task,3 个 reduce task,会产生 4*3=12 个小文件。

3.1.2 优化后的机制
普通机制的 Hash shuffle 会产生大量小文件,对文件系统的压力很大,也不利于 I/O 的吞吐量,后面做了优化(设置spark.shuffle.consolidateFiles=true开启,默认false),优化后的 Hash shuffl 把在同一个 core 上的多个 Mapper 输出到同一个文件,这样文件数就变成了 core * R 个。如下图所示:2 个 core,4 个 map task,3 个 reduce task,会产生 2*3=6 个小文件。

Hash shuffle 合并机制的问题:
如果 Reduce 端的并行任务或者数据分片过多,Core * Reduce Task 依旧很大,也还是会产生很多小文件。所以在 Spark 1.2 之后的版本中,默认的 ShuffleManager 改成了 SortShuffleManager。
3.2 Sort shuffle
SortShuffleManager 的运行机制主要分为两种,一种是普通运行机制,一种是 bupass 运行机制。当 shuffle read task 的数量小于等于 spark.shuffle.sort.bypassMergeThreshold 参数的值时(默认为200),就会启用 bypass 机制。
3.2.1 普通机制
这种机制和 MapReduce 差不多,在该机制下,数据会先写入一个内存数据结构中,此时根据不同的 shuffle 算子,可能选用不同的数据结构,如果是 reduceByKey 这种聚合类 的 shuffle 算子,那么会选用 Map 结构,一边通过 Map 进行聚合,一边写入内存;如果是 join 这种普通的 shuffle 算子,则会选用 Array 结构,直接写入内存。接着,每写一条数据进入内存结构就会判断一下,是否达到阈值,如果达到阈值就尝试将内存数据结构中的数据溢写到磁盘,然后清空内存数据结构。

在溢写到磁盘之前,会根据 key 对内存数据结构中已有的数据进行排序,排序过后会分批将数据写入磁盘文件。默认的 batch 是 10000 条,即排序好的数据会以每批1万条数据的形式分批写入磁盘文件。
一个 task 将所有数据写入内存数据结构的过程中,会发生多次磁盘溢写操作 ,也会产生多个临时文件。最后会将之前所有的临时文件都进行合并,由于一个 task 只对应一个文件,因此还会单独写一份索引文件,记录下游各个 task 的数据在文件中的 start offset 和 end offset。
SortShuffleManager 由于有一个磁盘文件 merge 的过程,因此大大减少了文件数量,由于每个 task 最终只有一个磁盘文件,所以文件个数等于上游 shuffle write 个数。
3.2.2 bypass 机制的 Sort Shuffle
bypass 运行机制的触发需要以下两个条件:
- shuffle map task 数量小于 spark.shuffle.sort.bypassMergeThreshold 参数的值,默认值200;
- 不是聚合类的 shuffle 算子(比如 reduceByKey);
bypass 机制的 sort shuffle 流程如下图所示:

在该机制下,上游 task 会为每个下游 task 都创建一个临时文件,并将数据按 key 进行 hash,然后根据 key 的 hash 值,将数据写入对应的文件中。当然,写入磁盘文件时也是先写入内存缓冲,缓冲写满之后再溢写到磁盘文件的。最后,同样会将所有临时磁盘文件都合并成一个磁盘文件,并创建一个单独的索引文件。
该过程的磁盘写机制其实跟未经优化的 HashShuffleManager 是一样的,因为都要创建数量惊人的磁盘文件,只是在最后会做一个磁盘文件的合并而已。因此少量的最终磁盘文件,也让该机制相对未经优化的 HashShuffleManager 来说,shuffle read 的性能会更好。
bypass 机制与普通 SortShuffleManager 运行机制的不同在于:
- 磁盘写机制不同;
- 不会进行排序。也就是说,启用该机制的最大好处在于,shuffle write 过程中,不需要进行数据的排序操作,也就节省掉了这部分的性能开销。
3.3 Hash shuffle 和 Sort shuffle 的区别
在 Spark 1.2 以前,默认的 shuffle 计算引擎是 HashShuffleManager。但是 HashShuffleManager 有着一个非常严重的弊端,就是会产生大量的中间磁盘文件,进而有大量的磁盘 I/O 操作影响性能。因此在 Spark 1.2 以后的版本中,默认的 shuffle 方式从 ShuffleManager 改成了 SortShuffleManager。
SortShuffleManager 相较于 HashShuffleManager 来说,有了一定的改进。主要就在于,每个Task在进行shuffle操作时,虽然也会产生较多的临时磁盘文件,但是最后会将所有的临时文件合并(merge)成一个磁盘文件,因此每个 Task 就只有一个磁盘文件。在下一个 stage 的 shuffle read task 拉取自己的数据时,只要根据索引读取每个磁盘文件中的部分数据即可。
四、Spark Shuffle 和 MapReduce Shuffle 的异同
-
从整体功能上看,两者并没有大的差别。 都是将 mapper(Spark 里是 ShuffleMapTask)的输出进行 partition,不同的 partition 送到不同的 reducer(Spark 里 reducer 可能是下一个 stage 里的 ShuffleMapTask,也可能是 ResultTask)。Reducer 以内存作缓冲区,边 shuffle 边 aggregate 数据,等到数据 aggregate 好以后进行 reduce(Spark 里可能是后续的一系列操作)。
-
从流程的上看,两者差别不小。 Hadoop MapReduce 是 sort-based,进入 combine 和 reduce 的 records 必须先 sort。这样的好处在于 combine/reduce可以处理大规模的数据,因为其输入数据可以通过外排得到(mapper 对每段数据先做排序,reducer 的 shuffle 对排好序的每段数据做归并)。以前 Spark 默认选择的是 hash-based,通常使用 HashMap 来对 shuffle 来的数据进行合并,不会对数据进行提前排序。如果用户需要经过排序的数据,那么需要自己调用类似 sortByKey 的操作。在Spark 1.2之后,sort-based 变为默认的 Shuffle 实现。
-
从流程实现角度来看,两者也有不少差别。 Hadoop MapReduce 将处理流程划分出明显的几个阶段:map, spill, merge, shuffle, sort, reduce等。每个阶段各司其职,可以按照过程式的编程思想来逐一实现每个阶段的功能。在 Spark 中,没有这样功能明确的阶段,只有不同的 stage 和一系列的 transformation,所以 spill, merge, aggregate 等操作需要蕴含在 transformation中。