在Spark 1.2.0中,Spark Core的一个重要的升级就是将默认的Hash Based Shuffle换成了Sort Based Shuffle,即spark.shuffle.manager 从hash换成了sort,对应的实现类分别是org.apache.spark.shuffle.hash.HashShuffleManager和org.apache.spark.shuffle.sort.SortShuffleManager。
这个方式的选择是在org.apache.spark.SparkEnv完成的:
// Let the user specify short names forshuffle managers
val shortShuffleMgrNames = Map(
"hash" ->"org.apache.spark.shuffle.hash.HashShuffleManager",
"sort" ->"org.apache.spark.shuffle.sort.SortShuffleManager")
val shuffleMgrName =conf.get("spark.shuffle.manager", "sort") //获得Shuffle Manager的type,sort为默认
val shuffleMgrClass =shortShuffleMgrNames.getOrElse(shuffleMgrName.toLowerCase, shuffleMgrName)
val shuffleManager =instantiateClass[ShuffleManager](shuffleMgrClass)
那么Sort BasedShuffle“取代”Hash BasedShuffle作为默认选项的原因是什么?
正如前面提到的,Hashbased shuffle的每个mapper都需要为每个reducer写一个文件,供reducer读取,即需要产生M*R个数量的文件,如果mapper和reducer的数量比较大,产生的文件数会非常多。Hash based shuffle设计的目标之一就是避免不需要的排序(Hadoop Map Reduce被人诟病的地方,很多不需要sort的地方的sort导致了不必要的开销)。但是它在处理超大规模数据集的时候,产生了大量的DiskIO和内存的消耗,这无疑很影响性能。Hash based shuffle也在不断的优化中,正如前面讲到的Spark 0.8.1引入的file consolidation在一定程度上解决了这个问题。为了更好的解决这个问题,Spark 1.1 引入了Sort based shuffle。首先,每个Shuffle Map Task不会为每个Reducer生成一个单独的文件;相反,它会将所有的结果写到一个文件里,同时会生成一个index文件,Reducer可以通过这个index文件取得它需要处理的数据。避免产生大量的文件的直接收益就是节省了内存的使用和顺序Disk IO带来的低延时。节省内存的使用可以减少GC的风险和频率。而减少文件的数量可以避免同时写多个文件对系统带来的压力。
Shuffle Map Task会按照key相对应的partition ID进行sort,其中属于同一个partition的key不会sort。因为对于不需要sort的操作来说,这个sort是负收益的;要知道之前Spark刚开始使用Hash based的shuffle而不是sort based就是为了避免Hadoop Map Reduce对于所有计算都会sort的性能损耗。对于那些需要sort的运算,比如sortByKey,这个sort在Spark 1.2.0里还是由reducer完成的。
如果这个过程内存不够用了,那么这些已经sort的内容会被spill到外部存储。然后在结束的时候将这些不同的文件进行merge sort。
为了便于下游的Taskfetch到其需要的partition,这里会生成一个index文件,去记录不同的partition的位置信息。当然了org.apache.spark.storage.BlockManager需要也有响应的实现以实现这种新的寻址方式。
核心实现的逻辑都在类org.apache.spark.shuffle.sort.SortShuffleWriter。下面简要分析一下它的实现:
1) 对于每个partition,创建一个scala.Array存储它所包含的key,value对。每个待处理的key,value对都会插入相应的scala.Array。
2) 如果scala.Array的大小超过阈值,那么需要将这个in memory的数据spill到外部存储。这个文件的开始部分会记录这个partition的ID,这个文件保存了多少个pair等信息。
3)最后需要将所有spill到外部存储的文件进行mergesort。同时打开的文件不能过多,过多的话会消耗大量的内存,增加OOM或者GC的风险;也不能过少,过少的话就会影响性能,增大计算的延时。一般的话推荐每次同时打开10 – 100个文件。
4)在生成最后的数据文件时,需要同时生成index索引文件。正如前面提到的,这个索引文件将记录不同partition的range。
前文也多次提到过,Spark1.2.0官方支持两种方式的Shuffle,即Hash Based Shuffle和Sort Based Shuffle。其中在Spark 1.0之前仅支持Hash Based Shuffle。Spark 1.1的时候引入了Sort Based Shuffle。Spark 1.2的默认Shuffle机制从Hash变成了Sort。如果需要Hash Based Shuffle,可以将spark.shuffle.manager设置成“hash”即可。
如果对性能有比较苛刻的要求,那么就要理解这两种不同的Shuffle机制的原理,结合具体的应用场景进行选择。
Hash Based Shuffle,就是将数据根据Hash的结果,将各个Reducer partition的数据写到单独的文件中去,写数据时不会有排序的操作。这个问题就是如果Reducer的partition比较多的时候,会产生大量的磁盘文件。这会带来两个问题:
1) 同时打开的文件比较多,那么大量的文件句柄和写操作分配的临时内存会非常大,对于内存的使用和GC带来很多的压力。尤其是在Sparkon YARN的模式下,Executor分配的内存普遍比较小的时候,这个问题会更严重。
2) 从整体来看,这些文件带来大量的随机读,读性能可能会遇到瓶颈。
更加细节的讨论可以参见7.1节和7.6.6(尝试去解决写的文件太多的问题)。
Sort Based Shuffle会根据实际情况对数据采用不同的方式进行Sort。这个排序可能仅仅是按照Reducer的partition进行排序,保证同一个Shuffle Map Task的对应于不同的Reducer的partition的数据都可以写到同一个数据文件,通过一个Offset来标记不同的Reducer partition的分界。因此一个Shuffle Map Task仅仅会生成一个数据文件(还有一个index索引文件),从而避免了Hash Based Shuffle文件数量过多的问题。
选择Hash还是Sort,取决于内存,排序和文件操作等因素的综合影响。
对于不需要进行排序的Shuffle而且Shuffle产生的文件数量不是特别多,Hash Based Shuffle可能是个更好的选择;毕竟Sort Based Shuffle至少会按照Reducer的partition进行排序。
而Sort BasedShuffle的优势就在于Scalability,它的出现实际上很大程度上是解决Hash Based Shuffle的Scalability的问题。由于Sort Based Shuffle还在不断的演进中,因此Sort Based Shuffle的性能会得到不断的改善。
对选择那种Shuffle,如果对于性能要求苛刻,最好还是通过实际的场景中测试后再决定。不过选择默认的Sort,可以满足大部分的场景需要。