Spark Shuffle演化历程
- Spark 1.1 引入Sort Based Shuffle,但默认仍为Hash Based Shuffle
- Spark 1.6 Tungsten-sort并入Sort Based Shuffle
- Spark2.0 所有shuffle的方式全部统一到Sort Shuffle一个实现中
关于Spark的shuffle和Hadoop的shuffle是一致的,包括Shufflewrite, ShuffleRead。前者解决上游stage输出数据的分区问题,后者解决下游stage从上游stage获取数据,重新组织,并为后续操作提供数据的问题。
SortShuffleManager
在sparkEnv里面shuffleManager只有一种实现,SortShuffleManager。 在SortShuffleManager中,输入的数据基于分区id进行排序,写入到单独的mapoutput文件中,并且通过索引文件记录每个分区在文件中的位置,由于分区id是排序的,比较方便reducer的拉取。SortShuffleManager的实现如下:
- registerShuffle
- unregisterShuffle
- getReader
- getWriter
核心接口是reader和writer, reader只有一种实现BlockStoreShuffleReader, writer有三种实现UnsafeShuffleWriter,BypassMergeSortShuffleWriter, SortShuffleWriter
ShuffleWriter
UnsafeShuffleWriter
UnsafeShuffleWriter内部维护了一个外部排序的类ShuffleExternalSorter。将输出数据先在内存中通过ShuffleInMemorySorter根据分区id进行排序,排好序的数据经过序列化后输出的临时文件中的一段。并且记录每个分区段的位置。
整个过程就是不断地在 ShuffleInMemorySorter 插入数据,如果没有内存就申请内存,如果申请不到内存就 spill 到文件中,最终合并成一个 依据 partition id 全局有序 的大文件,整个排序只是分区级别的排序。
要求:没有聚合或者key排序
分区数目不能超过16777216
原始数据需要序列化器的支持(kryoSerializer)
需要Serializer支持relocation,在指定位置读取对应数据
BypassMergeSortShuffleWriter
每个输出分区维护一个文件句柄,直接将数据写入分区文件中,最后再讲所有分区文件合并成一个文件,并创建一个 index索引文件来标记不同分区的位置信息。类似于分桶的思想
要求:不需要map端的聚合操作,分区数目小于等于spark.shuffie.sort.bypassMergeThreshold(200)
缺点:如果有太多分区,就会有太多的临时文件,会对文件系统造成很大的压力
SortShuffleWriter
在不满足上述要求的情况下,返回的是 BaseShuffieHandle 对象,并且实例化一个SortShuffleWriter
SortShuffleWriter使用ExternalSorter进行外部排序,创建ExternalSorter对象,将全部数据插入到对象中。并且生成Shuffle数据文件和索引文件。创建MapStatus对象,将Shuffle数据文件和索引文件的信息进行传输。
ExternalSorter是整个SortShufflerWriter的核心,包含了两个存放数据的变量,PartitionedAppendOnlyMap, PartitionedPairBuffer。PartitionedPairBuffer类似于动态的数组,PartitionedAppendOnlyMap类似于动态的hashMap, 如果在map端有聚合操作就使用后者。支持按照分区或者分区key进行排序
整体流程
- 根据map端是否要聚合选择不同的容器,聚合选择hashMap, 不聚合选择动态数组。
- 数据进行分区,不同的分区创建不同的容器。
- 数据写入到对应的容器中,hashMap可以做聚合操作。
- 判断数据是否达到存储容量,达到就spill到磁盘中去。
- 将内存中和spill的数据进行归并,支持排序,并且添加一个索引文件。
ShuffleReader
BlockStoreShuffleReader
- 获取 ShuffleMapTask任务执行的状态信息, 通过 MapOutputTracker 的 getStatus 方法获 取 map任务执行的状态信息,得到所需要读取数据的位置
- 知道Shuffle结果的位置后,根据不同的位置选择不同的读取方式,本地获取通过BlockManager, 远端获取通过netty读取。
- 判断shuffleDependency是否定义聚合,如果需要就根据兼职调度Aggregator进行聚合,聚合完毕之后使用ExternalSorter进行外部排序,并且写入内存缓存去,达到阈值,spill到磁盘,最后进行归并。
内存的使用
Shuffle Write
在 map 端选择普通的排序方式,则会采用 ExternalSorter进行外排, 在内 存中存储数据时主要占用堆内执行空间 若在 map 端选择 Tungsten 的排序方式,则采用 ShuffleExternalSorter 直接对以序列 化形式存储的数据进行排序在内存中存储数据时可以
占用堆外或堆内执行空间,取决于用户是否开启了堆外内存,以及堆外执行内存是否足够。
Shuffle Read
在对 reduce 端的数据进行聚合时,要将数据交给 Aggregator 处理,在内存 中存储数据时占用堆内执行空间 。 如果需要进行 最终结果 排序,则要再次将数据交给 ExternalSorter处理,占用堆内执行空间
与Hadoop Shuffle机制对比
都是分为shuffle write, shuffle reade, hadoop的shuffle write过程比较明确,数据经过分区处理后输出到一个固定大小的spill buffer中,如果被填满就将spill buffer中的record按照key进行排序输出到磁盘中。这点类似于spark的动态数组PartitionedPairBuffer, 不同的是hadoop是严格按照key进行排序的,而PartitionedPairBuffer更加灵活(支持分区或者分区+key排序), 而在map端存在combine情况下,hadoop需要等待所有record都spill到磁盘后,启动专门的combine阶段,将所有spill文件的record进行全局聚合,得到最终的聚合结果,这个过程是多次的,因为每次支队某个分区的spill进行聚合。
参考
http://ixiaosi.art/2019/09/02/spark/spark-shuffle%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86/
https://www.cnblogs.com/itboys/p/9201750.html
https://www.jianshu.com/p/ca1f59429fce