通用的Shuffle Write框架, 框架的计算顺序为“map() 输出→数据聚合→排序→分区”输出。

Spark的Shuffle Write框架设计和实现_数据


(1)不需要map() 端聚合(combine) 和排序。

这种情况最简单, 只需要实现分区功能。 如图6.5所示, map() 依次输出<; K, V>; record, 并计算其partitionId(PID) , Spark根据partitionId, 将record依次输出到不同的buffer中, 每当buffer填满就将record溢写到磁盘上的分区文件中。 分配buffer的原因是map() 输出record的速度很快, 需要进行缓冲来减少磁盘I/O。 在实现代码中, Spark将这种Shuffle Write方式称为BypassMergeSortShuffleWriter, 即不需要进行排序的Shuffle Write方式;

Spark的Shuffle Write框架设计和实现_数据_02

该模式的优缺点: 优点是速度快, 直接将record输出到不同的分区文件中。 缺点是资源消耗过高, 每个分区都需要一个buffer(大小由spark.Shuffle.file.buffer控制, 默认为32KB) , 且同时需要建立多个分区文件进行溢写。 当分区个数太大, 如10 000时, 每个map task需要约320MB的内存, 会造成内存消耗过大, 而且每个task需要同时建立和打开10 000个文件, 造成资源不足。 因此, 该Shuffle方案适合分区个数较少的情况(<; 200) 。该模式适用的操作类型: map() 端不需要聚合(combine) 、 Key不需要排序且分区个数较少(< =spark.Shuffle.sort.bypassMergeThreshold, 默认值为200) 。 例如, groupByKey(100) , partitionBy(100), sortByKey(100) 等。

(2) 不需要map() 端聚合(combine) , 但需要排序。

在这种情况下需要按照partitionId+Key进行排序。 Spark采用的实现方法是建立一个Array(图6.6中的PartitionedPairBuffer) 来存放map() 输出的record, 并对Array中元素的Key进行精心设计, 将每个< K, V> record转化为<(PID, K) , V> record存储; 然后按照partitionId+Key对record进行排序; 最后将所有record写入一个文件中, 通过建立索引来标示每个分区。

Spark的Shuffle Write框架设计和实现_数据_03

如果Array存放不下, 则会先扩容, 如果还存放不下, 就将Array中的record排序后spill到磁盘上, 等待map() 输出完以后, 再将Array中的record与磁盘上已排序的record进行全局排序, 得到最终有序的record, 并写入文件中。该Shuffle模式被命名为SortShuffleWriter(KeyOrdering=true) , 使用的Array被命名为PartitionedPairBuffer。

该Shuffle模式的优缺点:

优点是只需要一个Array结构就可以支持按照partitionId+Key进行排序, Array大小可控, 而且具有扩容和spill到磁盘上的功能, 支持从小规模到大规模数据的排序。 同时, 输出的数据已经按照partitionId进行排序, 因此只需要一个分区文件存储, 即可标示不同的分区数据, 克服了BypassMergeSortShuffleWriter中建立文件数过多的问题, 适用于分区个数很大的情况。

缺点是排序增加计算时延。

该Shuffle模式适用的操作: map() 端不需要聚合(combine) 、 Key需要排序、 分区个数无限制。 目前, Spark本身没有提供这种排序类型的数据操作, 但不排除用户会自定义, 或者系统未来会提供这种类型的操作。 sortByKey() 操作虽然需要按Key进行排序, 但这个排序过程在Shuffle Read端完成即可, 不需要在Shuffle Write端进行排序。

另外, 回想上一个BypassMergeSortShuffleWriter模式的缺点是, 分区个数一旦过多(>; 200) , 就会出现buffer过大、 建立和打开的文件数过多的问题。 在这种情况下, 应该采用什么样的Shuffle模式呢?

我们刚才分析了SortShuffleWriter的优点是只需要分配一个Array,大小可控, 同时只输出一个文件就可以标示出不同的分区, 可以用于解决BypassMergeSortShuffleWriter存在的buffer分配过多的问题。 唯一缺点是, 需要按PartitionId+Key进行排序, 而BypassMergeSortShuffleWriter面向的操作不需要按Key进行排序。 因此, 我们只需要将“按PartitionId+Key排序”改为“只按PartitionId排序”, 就可以支持“不需要map() 端combine、 不需要按照Key进行排序, 分区个数过大”的操作。 例如, groupByKey(300) 、 partitionBy(300) 、 sortByKey(300) 。

(3) 需要map() 端聚合(combine) , 需要或者不需要按Key进行排序。

Spark采用的实现方法是建立一个类似HashMap的数据结构对map() 输出的record进行聚合。 HashMap中的Key是“partitionId+Key”, HashMap中的Value是经过相同combine的聚合结果。

combine() 是sum() 函数, 那么Value中存放的是多个record对应的Value相加的结果。 聚合完成后, Spark对HashMap中的record进行排序。如果不需要按Key进行排序,

如图6.7的上图所示, 那么只按partitionId进行排序;

Spark的Shuffle Write框架设计和实现_spark_04

如果需要按Key进行排序, 如图6.7的下图所示, 那么按partitionId+Key进行排序。 最后, 将排序后的record写入一个分区文件中。

Spark的Shuffle Write框架设计和实现_spark_05

如果HashMap存放不下, 则会先扩容为两倍大小, 如果还存放不下, 就将HashMap中的record排序后spill到磁盘上。 此时, HashMap被清空, 可以继续对map() 输出的record进行聚合, 如果内存再次不够用,那么继续spill到磁盘上, 此过程可以重复多次。 当map() 输出完成以后, 将此时HashMap中的reocrd与磁盘上已排序的record进行再次聚合(merge) , 得到最终的record, 输出到分区文件中。

该Shuffle模式的优缺点: 优点是只需要一个HashMap结构就可以支持map() 端的combine功能, HashMap具有扩容和spill到磁盘上的功能, 支持小规模到大规模数据的聚合, 也适用于分区个数很大的情况。 在聚合后使用Array排序, 可以灵活支持不同的排序需求。

缺点是在内存中进行聚合, 内存消耗较大, 需要额外的数组进行排序, 而且如果有数据spill到磁盘上, 还需要再次进行聚合。 在实现中, Spark在Shuffle Write端使用一个经过特殊设计和优化的HashMap, 命名为PartitionedAppendOnlyMap, 可以同时支持聚合和排序操作, 相当于HashMap和Array的合体;

该Shuffle模式适用的操作: 适合map() 端聚合(combine) 、 需要或者不需要按Key进行排序、 分区个数无限制的应用, 如reduceByKey() 、 aggregateByKey() 等。


Shuffle Write框架需要执行的3个步骤是“数据聚合→排序→分区”。

如果应用中的数据操作不需要聚合, 也不需要排序, 而且分区个数很少, 那么可以采用直接输出模式, 即BypassMergeSortShuffleWriter。

为了克服BypassMergeSortShuffleWriter打开文件过多、 buffer分配过多的缺点, 也为了支持需要按Key进行排序的操作, Spark提供了SortShuffleWriter, 使用基于Array的方法来按partitionId或partitionId+Key进行排序, 只输出单一的分区文件即可。

最后, 为了支持map() 端combine操作,

Spark提供了基于HashMap的SortShuffleWriter, 将Array替换为类似HashMap的操作来支持聚合操作, 在聚合后根据partitionId或partitionId+Key对record进行排序, 并输出分区文件。 因为SortShuffleWriter按partitionId进行了排序, 所以被称为sort-based Shuffle Write。