概述
什么是Shuffle?
在讲到Spark Shuffle实现机制之前,需要了解下什么是Shuffle。Shuffle按字面意思,也就是洗牌,把牌视作数据,那么洗的过程也就是按照某种规则改变数据的次序,接着发牌员发牌给玩家,发牌的过程就对应着通过网络I/O分发数据,接着玩家读牌。Shuffle正好对应了这三个过程。
假设在一场牌局中,有一位发牌员和两位玩家。一般情况下,发牌员在开始游戏之前,需要将牌随机打乱,再按序发给两位玩家,接着两位玩家开始处理牌。发牌员洗牌、发牌、玩家拿牌这三个过程中,洗牌和拿牌在Spark中分别对应Shuffle Write, Shuffle Read,文章关注的则是Shuffle Write。
Shuffle发生在哪些实例上?
在Spark中,driver是负责切分task并序列化task,协调资源,并调度派发到executor上,driver不负责处理数据,具体数据处理是由executor完成的,也就是说Shuffle是发生在executor上的。回到上边的牌局,这时候executor兼具发牌员和玩家两个职能,一般大家和朋友打扑克,总不能有人专门洗牌发牌但不玩吧~在Spark中稍复杂些,并不是一个executor在Shuffle Write,而是每个executor都在做,直到所有executor都完成了Shuffle Write,都通知driver已完成,才会进入到下一个Stage,进行Shuffle Read。
为什么需要Shuffle?
在数据量小时,一般一个单体进程即可完成加工处理,但面对海量数据处理,一台单体进程是难以胜任的。随着互联网发展,许多分布式计算框架被提出,这些框架总的来说,都是在多个分布式进程中处理不同的数据。在对数据处理过程中,时常需要:
- group, join数据:比如根据相同key聚类,每个分布式数据处理进程处理后,将特定key的数据发往特定的分布式进程上进行聚类;
- 数据倾斜时重分布:数据倾斜在少数分布式进程,导致其他进程空跑等待,既是浪费资源,也会影响整体处理效率,因此需要将数据发往其他分布式进程进行处理。
这两种场景就涉及到如何“洗牌”,将数据按某种规则分布到其他进程中。在Spark中有哪些操作会触发到Shuffle呢?
有哪些对RDD/DF的操作会触发到Shuffle呢?
主要是这四类:
- .*ByKey: groupByKey, countByKey, reduceByKey等聚类算法
- .*By: distributeBy, clusterBy等聚类算法
- repartition: round robin重分布数据
- join: 可能触发,当需要连接的数据广播到各个executor时,就不会触发shuffle,直接在内存中进行join
Spark中对Shuffle实现的演进历史
这部分我倒是没有细看,我开始接触时Spark就已经迭代到3.2的版本了,只是了解到在2.0之前,Shuffle的实现变化很多,主要是为了解决非功能问题,有兴趣可以了解一下,对解决日常非功能问题也有一定启发。在2.0及之后版本,Shuffle Write的实现就已经稳定了,只有以下三种:
- UnsafeShuffleWriter: 对序列化数据直接排序(对partitionId排序),减少反序列化后排序再序列化的开销,每个分区一个FileSegment,最终所有的FileSegment合并到一份文件中。
- BypassMergeSortShuffleWriter: bypass通过,数据怎样来就怎样写,当然会按parititionId写到不同的FileSegment中,最终所有的FileSegment会整合到同个文件中。
- SortShuffleWriter: 对数据进行mapSideCombine(可选的),启用后可选对特定key进行聚合和排序(先排partitionId,再排key);如果不启用,只会对partitionId排序。它没有按照一个分区一个FileSegment的方式,而是在将数据插入到排序器的过程中达到一定阈值,触发排序并写入文件,再回到将数据插入排序器的过程,直至没有数据了,最终合并所有文件和可能排序器中残留的数据到一份文件。
那么Spark是如何决定使用哪种实现的呢?
使用各个Shuffle Writer的条件
源码分析
在Spark driver构建RDD之间的血缘依赖时,便根据以下条件选择构建具体的Shuffle依赖:
// SortShuffleManager
override def registerShuffle[K, V, C](
shuffleId: Int,
dependency: ShuffleDependency[K, V, C]): ShuffleHandle = {
if (SortShuffleWriter.shouldBypassMergeSort