摘要:Shuffle 是 Spark 作业中最昂贵的操作,也是性能瓶颈的高发区。Spark 2.0 起 SortShuffleManager 成为唯一的默认实现,通过 ExternalSorter 分区排序 + 溢写归并 + Index File 精确索引三大机制,彻底解决了 HashShuffle 的 M×R 文件爆炸问题。本文从 SortShuffleManager 的三种 ShuffleHandle 选择策略、ExternalSorter 溢写与归并流程、Index File 索引机制、Tungsten 堆外排序优化、HashShuffle → SortShuffle 演进对比五个维度,配合 2 张架构图 + 源码追踪,完整拆解 SortShuffle 原理。
关键词:SortShuffleManager, ExternalSorter, Index File, BypassMerge, Tungsten, HashShuffle, Shuffle 优化
一、开篇:Shuffle 为何是 Spark 的「阿喀琉斯之踵」
凡是涉及 reduceByKey / groupByKey / join / sortBy 等宽依赖算子,都会触发一次 Shuffle。Shuffle 涉及磁盘 I/O、网络传输和序列化反序列化,是 Spark 性能调优的核心战场。
Spark Shuffle 演进时间线
Spark 0.x-1.x: HashShuffleManager (M×R 文件,已弃用)
Spark 1.2+: SortShuffleManager 引入(可选)
Spark 2.0+: SortShuffleManager 成为唯一默认
Spark 2.x+: Tungsten-sort (UnsafeShuffleWriter)
二、SortShuffle 架构全景

2.1 三种 ShuffleHandle 选择策略
scala
// SortShuffleManager.registerShuffle() 决策逻辑
if (依赖不需要 map-side combine
&& 不要求排序
&& 分区数 <= bypassMergeThreshold) {
→ BypassMergeSortShuffleHandle
(无排序,每个分区独立文件,最后合并为一个 data file)
}
else if (记录可序列化
&& 不需要 aggregation
&& 不需要排序
&& 分区数 < 16777216) {
→ SerializedShuffleHandle
(UnsafeShuffleWriter --- Tungsten 堆外排序)
}
else {
→ BaseShuffleHandle
(SortShuffleWriter + ExternalSorter 通用路径)
}
2.2 ExternalSorter 溢写与归并
ExternalSorter.insertAll(records):
① 数据先写入内存中的 PartitionedAppendOnlyMap
② 内存超过阈值 → spill() 溢写磁盘
- 按 (partitionId, key) 排序
- 写入临时文件(受 spark.shuffle.spill.compress 控制)
③ 所有数据处理完毕 → 多路归并
- 将所有溢写文件 + 内存中剩余数据
- 按 (partitionId, key) 归并排序
④ 输出到最终 data file + index file
2.3 Index File --- SortShuffle 的关键创新
Index File 结构(每分区 16 字节)
[p0_offset(8B)|p0_length(8B)|p1_offset|p1_length|...]
价值:
├── Reduce 端可精确定位每个分区的数据块
├── 无需全量扫描 data file
├── 支持并行拉取不同分区
└── Netty 零拷贝传输(sendfile)
三、BypassMerge 机制
当分区数较少(≤200)且不需要排序时,BypassMerge 直接为每个分区写入独立临时文件,最后合并为一个 data file + 一个 index file。省去了排序开销,适合 groupByKey / join 等不需要 map-side combine 的场景。
properties
spark.shuffle.sort.bypassMergeThreshold=200 # 默认值
# 如果期望的分区数 > 200,使用 sort-based 路径
# 如果较小且无排序需求,走 bypass 路径更快
四、Tungsten-Sort:二进制堆外排序
当满足 SerializedShuffleHandle 条件时,SortShuffleManager 启用 UnsafeShuffleWriter 。核心机制:将序列化后的二进制数据放在堆外内存 中,直接按分区 ID 排序数据指针------全程不反序列化,零GC压力。
Tungsten-sort 启用条件
├── spark.shuffle.manager=sort
├── 无 map-side aggregation
├── 无排序要求
├── 分区数 < 16777216 (2^24)
├── 序列化器支持对象重定位(Kryo / Java)
└── spark.sql.tungsten.enable=true(默认)
五、HashShuffle → SortShuffle 演进

核心对比
| HashShuffle 缺陷 | SortShuffle 解决方案 |
|---|---|
| M×R 个文件 | 2M 个文件(data+index) |
| 无排序,Reduce 全量扫描 | 分区排序 + Index File 精确定位 |
| 无溢写,内存可能 OOM | ExternalSorter 自动溢写+归并 |
| 文件句柄耗尽 | BypassMerge 合并为单文件 |
| 无堆外优化 | Tungsten UnsafeShuffleWriter |
六、核心源码追踪
scala
// SortShuffleManager.write() --- Shuffle Write 入口
override def getWriter[K, V](handle: ShuffleHandle, ...): ShuffleWriter[K, V] = {
handle match {
case unsafe: SerializedShuffleHandle[K @unchecked, V @unchecked] =>
new UnsafeShuffleWriter(env, blockManager, ...)
case bypass: BypassMergeSortShuffleHandle[K @unchecked, V @unchecked] =>
new BypassMergeSortShuffleWriter(blockManager, ...)
case other: BaseShuffleHandle[K @unchecked, V @unchecked, _] =>
new SortShuffleWriter(shuffleBlockResolver, handle, mapId, context)
}
}
// SortShuffleWriter.write() → ExternalSorter
sorter = new ExternalSorter(context, aggregator, ordering, serializer)
sorter.insertAll(records) // ① 插入+排序+溢写
sorter.writePartitionedFile(outputWriter) // ② 输出 data+index
七、关键配置速查
properties
# SortShuffle 核心参数
spark.shuffle.manager=sort # 默认
spark.shuffle.sort.bypassMergeThreshold=200 # bypass 阈值
spark.shuffle.spill.compress=true # 溢写压缩
spark.shuffle.compress=true # shuffle 输出压缩
spark.shuffle.file.buffer=32k # 写缓冲
spark.reducer.maxSizeInFlight=48m # reduce 拉取缓冲
spark.shuffle.sort.io.plugin.class # 自定义 IO 插件
# Tungsten
spark.sql.tungsten.enabled=true # 默认启用
八、总结
-
SortShuffleManager:Spark 2.0 起唯一默认实现,三种 ShuffleHandle 覆盖所有场景。BypassMerge 处理小分区直写,Serialized 启用 Tungsten 堆外排序,Base 走 ExternalSorter 通用路径。
-
ExternalSorter:内存中 PartitionedAppendOnlyMap → 超阈值自动溢写 → 多路归并排序 → 输出 data + index 文件。Index File(16B/分区)实现精准分区定位。
-
演进路径:HashShuffle(M×R 文件爆炸)→ SortShuffle(2M 文件 + 排序 + Index)→ Tungsten-Sort(堆外二进制排序,零GC)。
作者 :starzy
博客 :blog.starzy.cn
GitHub :starzy1990.github.io
专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践