1、shuffle的概念
Shuffle
就是对数据进行重组,由于分布式计算的特性和要求,在实现细节上更加繁琐和复杂。
在MapReduce
框架,Shuffle
是连接Map
和Reduce
之间的桥梁,Map
阶段通过shuffle
读取数据并输出到对应的Reduce
;而Reduce
阶段负责从Map
端拉取数据并进行计算。在整个shuffle
过程中,往往伴随着大量的磁盘和网络I/O
。所以shuffle
性能的高低也直接决定了整个程序的性能高低。Spark也会有自己的shuffle
实现过程。
2、HashShuffle机制
2.1、HashShuffle概述
Spark-1.6
版本之前,一直使用HashShuffle
,在Spark-1.6
版本之后使用Sort-Base Shuffle
,因为HashShuffle
存在的不足所以就替换了HashShuffle
。
Spark的运行主要分为2部分:一部分是驱动程序,其核心是 SparkContext
;另一部分是 Worker
节点上 Task
,它是运行实际任务的。程序运行的时候,Driver
和Executor
进程相互交互:运行什么任务,即 Driver
会分配 Task
到 Executor
,Driver
跟 Executor
进行网络传输; 任务数据从哪儿获取,即Task
要从Driver
抓取其他上游的 Task
的数据结果,所以有这个过程中就不断的产生网络结果。其中,下一个 Stage
向上一个 Stage
要数据这个过程,我们就称之为Shuffle
。
2.2、优化之前的HashShuffle机制
在HashShuffle
没有优化之前,每一个ShufflleMapTask
会为每一个 ReduceTask
创建一个 bucket
缓存,并且会为每一个 bucket
创建一个文件。这个bucket
存放的数据就是经过Partitioner
操作(默认是HashPartitioner
)之后找到对应的bucket
然后放进去,最后将数据刷新bucket
缓存的数据到磁盘上,即对应的block file
。
然后ShuffleMapTask
将输出作为MapStatus
发送到DAGScheduler
的MapOutputTrackerMaster
,每一个MapStatus
包含了每一个ResultTask
要拉取的数据的位置和大小。
ResultTask
然后去利用BlockStoreShuffleFetcher
向MapOutputTrackerMaster
获取MapStatus
,看哪一份数据是属于自己的,然后底层通过BlockManager
将数据拉取过来
拉取过来的数据会组成一个内部的ShuffleRDD
,优先放入内存,内存不够用则放入磁盘,然后ResulTask
开始进行聚合,最后生成我们希望获取的那个MapPartitionRDD
。
缺点
在这里有1个worker
,2个executor
,每一个executor
运行2个ShuffleMapTask
,有三个ReduceTask
,所以总共就有4 * 3=12个bucket
和12个block file
。
- 如果数据量较大,将会生成
MR
个小文件,比如ShuffleMapTask
有100个,ResultTask
有100个,这就会产生100*100=10000
个小文件; bucket
缓存很重要,需要将ShuffleMapTask
所有数据都写入bucket
,才会刷到磁盘,那么如果Map
端数据过多,这就很容易造成内存溢出,尽管后面有优化,bucket
写入的数据达到刷新到磁盘的阀值之后,就会将数据一点一点的刷新到磁盘,但是这样磁盘I/O
就多了。
2.3、优化后的HashShuffle
每一个 Executor
进程根据核数,决定 Task
的并发数量,比如Executor
核数是2,就是可以并发运行两个Task
,如果是一个则只能运行一个Task
;
假设Executor
核数是1,ShuffleMapTask
数量是M,那么它依然会根据ResultTask
的数量R,创建R个bucket
缓存,然后对key
进行hash
,数据进入不同的bucket
中,每一个 bucket
对应着一个block file
,用于刷新bucket
缓存里的数据;
然后下一个Task
运行的时候,那么不会再创建新的bucket
和block file
,而是复用之前的Task
已经创建好的bucket
和block file
。即所谓同一个Executor
进程里所有Task
都会把相同的key放入相同的bucket
缓冲区中;
这样的话,生成文件的数量就是(本地 worker
的 Executor
数量 Executor
的 coresResultTask
数量)如上图所示,即2*1*3=6
个文件,每一个Executor
的ShuffleMapTask
数量100,ResultTask
数量为100,那么未优化的HashShuffle
的文件数是2 * 1 * 100 * 100=20000
,优化之后的数量是21 * 100=200
文件,相当于少了100倍。
缺点
如果 Reducer
端的并行任务或者是数据分片过多的话则 Core * Reducer Task
依旧过大,也会产生很多小文件。
3、Sort-Based Shuffle
2.1、HashShuffle回顾
HashShuffle
写数据的时候,内存有一个bucket
缓冲区,同时在本地磁盘有对应的本地文件,如果本地有文件,那么在内存应该也有文件句柄也是需要耗费内存的。也就是说,从内存的角度考虑,即有一部分存储数据,一部分管理文件句柄。如果Mapper
分片数量为1000,Reduce
分片数量为1000,那么总共就需要1000000个小文件。所以就会有很多内存消耗,频繁 IO
以及 GC
频繁或者出现内存溢出。
而且Reducer
端读取Map
端数据时,Mapper
有这么多小文件,就需要打开很多网络通道读取,很容易造成Reducer
(下一个stage
)通过driver
去拉取上一个stage
数据的时候,说文件找不到,其实不是文件找不到而是程序不响应,因为正在GC
。
2.2、Sorted-Based Shuffle介绍
为了缓解Shuffle
过程产生文件数过多和Writer
缓存开销过大的问题,Spark引入了类似于hadoop Map-Reduce
的shuffle
机制。该机制每一个ShuffleMapTask
不会为后续的任务创建单独的文件,而是会将所有的 Task
结果写入同一个文件,并且对应生成一个索引文件。以前的数据是放在内存缓存中,等到数据完了再刷到磁盘,现在为了减少内存的使用,在内存不够用的时候,可以将输出溢写到磁盘,结束的时候,再将这些不同的文件联合内存的数据一起进行归并,从而减少内存的使用量。一方面文件数量显著减少,另一方面减少 Writer
缓存所占用的内存大小,而且同时避免 GC
的风险和频率。
Sort-Based Shuffle
有几种不同的策略:BypassMergeSortShuffleWriter
、SortShuffleWriter
和UnasfeSortShuffleWriter
。
对于BypassMergeSortShuffleWriter
,使用这个模式特点:
- 主要用于处理不需要排序和聚合的
Shuffle
操作,所以数据是直接写入文件,数据量较大的时候,网络I/O
和内存负担较重; - 要适合处理
Reducer
任务数量比较少的情况下; - 将每一个分区写入一个单独的文件,最后将这些文件合并,减少文件数量;但是这种方式需要并发打开多个文件,对内存消耗比较大。
因为BypassMergeSortShuffleWriter
这种方式比SortShuffleWriter
更快,所以如果在Reducer
数量不大,又不需要在map
端聚合和排序,而且Reducer
的数目<spark.shuffle.sort.bypassMergeThrshold
指定的阀值,就是用的是这种方式。
对于SortShuffleWriter
,使用这个模式特点:
- 比较适合数据量很大的场景或者集群规模很大;
- 引入了外部外部排序器,可以支持在
Map
端进行本地聚合或者不聚合; - 如果外部排序器
enable
了spill
功能,如果内存不够,可以先将输出溢写到本地磁盘,最后将内存结果和本地磁盘的溢写文件进行合并。
另外这个
Sort-Based Shuffle
跟Executor
核数没有关系,即跟并发度没有关系,它是每一个ShuffleMapTask
都会产生一个data
文件和index
文件,所谓合并也只是将该ShuffleMapTask
的各个partition
对应的分区文件合并到data
文件而已。所以这个就需要个Hash-BasedShuffle
的consolidation
机制区别开来。