Spark源码系列之Spark内核——Shuffle

在Hadoop中有一个阶段——Shuffle,Shuffle存在于Map和Reduce之间。同样在Spark中也存在Shuffle,而且Shuffle影响着Job的性能。尽管Spark尽可能的减少Shuffle,但是操作却需要Shuffle来完成(如,groupByKey、sortByKey、reduceByKey、distinct等)。

假设有Map Tasks=M,Reduce Tasks=R,则Spark会根据Map Tasks和Reduce Tasks数量生成ShuffleFile=M*R:

Spark源码系列之Spark内核——Shuffle_第1张图片

Spark1.2以后就有实现了两种Shuffle:Hash Shuffle和Sort Shuffle。


1.Hash Shuffle

1.1Basic Hash Shuffle

Hash Shuffle是不用对数据进行排序的。因此Shuffle Write的职责很简单:将数据 partition 好,并持久化。之所以要持久化,一方面是要减少内存存储空间压力,另一方面也是为了 fault-tolerance。
Shuffle Write的任务很简单,那么实现也很简单:将 Shuffle Write的处理逻辑加入到ShuffleMapStage(ShuffleMapTask所在的Stage)的最后,该Stage的 final RDD每输出一个record就将其partition并持久化。
Spark源码系列之Spark内核——Shuffle_第2张图片

ShuffleMapTask的执行过程:先利用pipeline计算得到final RDD中对应的partition的records。没得到一个record就将其送到对应的bucket里,具体哪个bucket由partitioner.getPartition(record._1)决定。每个bucket里面的数据会不断被写到本地磁盘上,形成一个ShuffleBlockFile,或者简称FileSegment。之后的Reducer会去fetch属于自己的FileSegment,从而进入Shuffle Read阶段。
这是最基本的Hash Shuffle。每个Map Task都要将输出写到多个文件中。
假设有Map Tasks=M,Reduce Tasks=R,则Spark会根据Map Tasks和Reduce Tasks数量生成ShuffleFile=M*R。
这种实现就导致两个问题:

  1. 产生的FileSegment过多。一般Spark Job的M和R都很大,因此磁盘上会存在大量的数据文件。
  2. 缓冲区占用内存空间大。假设每个buffer有100Kb,且有1000个Reducers和10个Mappers/Executor。那么buffer占用的内存总量是:100Kb*10000*10=10GB/Executor。10GB/Executor对于buffer,是不能接受的。最简单直接的解决方法是减小buffer的大小,但也不能太小。

1.2Consolidate Hash Shuffle

对于第一个问题,Spark给出了一种加强版的Hash Shuffle。每个Executor的每个Bucket映射到一个文件分段。
Spark源码系列之Spark内核——Shuffle_第3张图片

可以明显看出,在一个core上连续执行的ShuffleMapTask可以共用一个输出文件ShuffleFile。先执行完的ShuffleMapTask形成ShuffleBlock i(即bucket),后执行的ShuffleMapTask可以将输出数据直接追加到ShuffleBlock i后面,形成ShuffleBlock i',每个ShuffleBlock被称为FileSegment。下一个 stage 的 reducer 只需要 fetch 整个 ShuffleFile 就行了。这样,每个 worker 持有的文件数降为 cores * R。FileConsolidation 功能可以通过spark.shuffle.consolidateFiles=true来开启。


2.Sort Shuffle

Spark源码系列之Spark内核——Shuffle_第4张图片

在Hadoop中,Shuffle阶段是要对数据进行排序的,即Reducer接收到的数据都是已经排好序的。在Spark1.2之后,也提供了Sort Shuffle。
在Sort Shuffle中,每个MapTask会生成一个ShuffleFile和一个IndexFile。

val partitionLengths = sorter.writePartitionedFile(blockId, context, outputFile)
shuffleBlockResolver.writeIndexFile(dep.shuffleId, mapId, partitionLengths)

3.Shuffle的选择

在SparkEnv中,我们可以看到:
// Let the user specify short names for shuffle managers
val shortShuffleMgrNames = Map(
  "hash" -> "org.apache.spark.shuffle.hash.HashShuffleManager",
  "sort" -> "org.apache.spark.shuffle.sort.SortShuffleManager",
  "tungsten-sort" -> "org.apache.spark.shuffle.unsafe.UnsafeShuffleManager")
val shuffleMgrName = conf.get("spark.shuffle.manager", "sort")
val shuffleMgrClass = shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase, shuffleMgrName)
val shuffleManager = instantiateClass[ShuffleManager](shuffleMgrClass)

Spark是允许我们自己实现Shuffle的。从代码中可以发现,Hash Shuffle对应的是org.apache.spark.shuffle.hash.HashShuffleManager类,Sort Shuffle对应的是org.apache.spark.shuffle.sort.SortShuffleManager类。而Spark默认采用的是Sort Shuffle,“sort”大小写没关系。如果用户配置的spark.shuffle.manager在shortShuffleMgrNames中没有查到,则选用用户自定义的Shuffle。用户自定义的Shuffle必须继承ShuffleManager类,重写里面的一些方法。


你可能感兴趣的:(源码,spark,内核,shuffle)