Spark 中的 Shuffle 是分布式数据交换的核心流程,涉及多个组件的协同工作。为了深入理解其处理过程,我们可以从源码角度分析 Shuffle 的执行路径,分为 Shuffle Write 和 Shuffle Read 两个阶段。
1. Shuffle Write 阶段
Shuffle Write 的主要任务是将 Mapper 的数据按照分区规则(如 HashPartitioner)分割、排序并写入磁盘。
1.1 数据分区与序列化
-
入口方法 :
Mapper 阶段的
compute()
方法中调用了ShuffleDependency
的相关逻辑。scalaval partition = partitioner.getPartition(key)
数据会根据
partitioner
(如HashPartitioner
或自定义分区器)计算目标分区。 -
序列化 :
每条数据会通过
serializerInstance.serialize()
进行序列化,将数据转换成字节流以便后续写入。
1.2 数据排序与溢写(Spill)
-
排序 :
在
SortShuffleWriter
中,数据会被放入内存中的PartitionedAppendOnlyMap
或PartitionedPairBuffer
进行排序(根据键的自然顺序或用户指定的比较器)。 -
溢写到磁盘 :
当内存不足时,会触发溢写(spill)。溢写的数据会写到多个磁盘文件,每个文件对应多个分区。
1.3 合并分区数据
- 归并操作 :
在溢写文件较多时,Spark 会对这些临时文件执行归并排序,生成最终的分区文件。- 对于
BypassMergeSortShuffleWriter
,会直接将分区文件写出,无需排序。 - 对于
SortShuffleWriter
,归并排序确保每个分区的数据有序。
- 对于
1.4 写出索引文件
- 最后,Shuffle Write 阶段会生成一个索引文件(
shuffleId_0.index
)和数据文件(shuffleId_0.data
)。- 索引文件:记录每个分区在数据文件中的偏移量,用于快速定位分区数据。
- 数据文件:存储分区后的数据。
2. Shuffle Read 阶段
Shuffle Read 阶段由 Reducer 执行,任务是从分布式存储中拉取相应分区的数据。
2.1 拉取数据
-
入口方法 :
Reducer 的
compute()
方法会调用BlockStoreShuffleFetcher.fetch()
从MapOutputTracker
获取每个分区的数据位置。scalaval blocksByAddress = mapOutputTracker.getMapSizesByExecutorId(shuffleId, reduceId)
-
数据传输 :
数据通过 Spark 的 BlockManager 拉取。如果目标数据在同一节点上,可以通过本地文件系统读取;如果在远程节点上,则通过 Netty 或 HTTP 传输。
2.2 数据解压与反序列化
-
解压 :
如果数据经过压缩(如 LZ4、Snappy),在读取时会被解压缩。
- 压缩相关配置:
spark.shuffle.compress=true
,spark.shuffle.spill.compress=true
。
- 压缩相关配置:
-
反序列化 :
使用与 Shuffle Write 相同的序列化器(如 Kryo 或 JavaSerializer)将字节流转换回对象。
2.3 数据聚合与处理
- 拉取到的数据会根据 Reducer 的逻辑(如
reduceByKey
或aggregateByKey
)进行聚合或排序处理。
3. 关键组件的协作关系
-
ShuffleManager
:决定使用哪种类型的 Shuffle,如
SortShuffleManager
或HashShuffleManager
。 -
ShuffleWriter
:负责数据写入磁盘,主要实现类:
BypassMergeSortShuffleWriter
SortShuffleWriter
-
ShuffleReader
:负责从不同节点拉取数据,主要实现类:
BlockStoreShuffleReader
-
MapOutputTracker
:负责跟踪每个 Mapper 的输出分区位置,Reducer 会通过它获取分区数据的位置。
4. Shuffle 设计的优缺点
特性 | 优点 | 缺点 |
---|---|---|
分区文件索引 | 减少数据读取时的随机 I/O 开销 | 索引管理复杂度增加 |
排序优化 | 提高数据局部性和读取效率 | 需要更多的 CPU 和内存资源 |
溢写与归并 | 避免内存溢出,支持大规模数据处理 | 增加磁盘 I/O 开销 |
数据压缩 | 减少网络传输和存储空间 | 压缩和解压缩会增加 CPU 开销 |
源码路径及关键类
-
Shuffle Write:
SortShuffleWriter
:org.apache.spark.shuffle.sort.SortShuffleWriter
BypassMergeSortShuffleWriter
:org.apache.spark.shuffle.sort.BypassMergeSortShuffleWriter
-
Shuffle Read:
BlockStoreShuffleReader
:org.apache.spark.shuffle.BlockStoreShuffleReader
-
Shuffle 依赖与管理:
ShuffleDependency
:org.apache.spark.shuffle.ShuffleDependency
ShuffleManager
:org.apache.spark.shuffle.ShuffleManager
5. 性能优化方向
-
调优分区数:
- 合理配置
spark.sql.shuffle.partitions
或spark.default.parallelism
,避免分区数过多或过少。
- 合理配置
-
压缩与序列化:
- 优化序列化器(推荐使用 Kryo),并启用压缩来减少网络开销。
-
内存管理:
- 调整
spark.memory.fraction
,确保 Shuffle 缓存有足够的内存。
- 调整
-
使用外部 Shuffle 服务:
- 启用
ExternalShuffleService
,减轻 Executor 的内存和磁盘压力。
- 启用
通过以上分析,可以从源码和优化角度全面理解 Spark Shuffle 的设计与工作原理。