摘要:DAGScheduler 拿到 finalRDD 后,如何一步步回溯出完整的 Stage DAG?为什么 Stage 内部可以 Pipeline 执行而 Stage 之间必须串行?数据本地性如何影响 Task 分发?本文从四步切分算法、Pipeline 函数组合原理、递归提交顺序、数据本地性调度、Stage 间串行 vs Stage 内并行五个维度,配合 1 张原创深色架构图 + 完整源码分析,带你彻底理解 Spark Stage 的切分逻辑与计算模式。
关键词:Spark Stage, DAGScheduler, Pipeline, 递归提交, 数据本地性, ShuffleMapStage, ResultStage, TaskSet
一、开篇:一个 collect() 如何变成两个 Stage?
scala
sc.textFile("hdfs:///data")
.flatMap(_.split(" "))
.map((_, 1))
.reduceByKey(_ + _)
.filter(_._2 > 10)
.collect()
这段代码会产生 2 个 Stage 。textFile→flatMap→map 是 Stage 0(3 个 NarrowDep),reduceByKey→filter→collect 是 Stage 1(1 个 WideDep 切分点)。
Stage 不是"写出来的",而是 DAGScheduler "算出来的"。
二、Stage 切分算法全景图

三、切分算法四步走
Step 1: DAGScheduler 从 finalRDD 出发
scala
// 源码:DAGScheduler.scala
private def createResultStage(
finalRDD: RDD[_], func: ..., jobId: Int, ...): ResultStage = {
val parents = getOrCreateParentStages(finalRDD, jobId) // 回溯
val id = nextStageId.getAndIncrement()
new ResultStage(id, finalRDD, func, partitions, parents, jobId, ...)
}
Step 2-3: 递归回溯 RDD.dependencies → 遇 Shuffle 则切分
scala
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
rdd.dependencies.flatMap {
case shufDep: ShuffleDependency[_, _, _] =>
getOrCreateShuffleMapStage(shufDep, firstJobId) :: Nil // ← 切分点
case _: NarrowDependency[_] =>
Nil // ← 同 Stage,继续往前找
}.toList
}
Step 4: submitStage() 递归提交------先父后子
scala
private def submitStage(stage: Stage): Unit = {
val missing = getMissingParentStages(stage).sortBy(_.id)
if (missing.isEmpty) {
submitMissingTasks(stage, jobId.get) // 无父 → 直接执行
} else {
for (parent <- missing) submitStage(parent) // 有父 → 先执行父
}
}
提交顺序:submitStage(Stage1) → 发现父 Stage0 未完成 → submitStage(Stage0) → Stage0 全部 Task 完成 → 回到 submitStage(Stage1) → 执行。
四、Pipeline 计算模式
Stage 内所有 NarrowDep 的算子不是依次执行 ,而是被组合成一个迭代器流水线:
scala
// RDD.iterator() 的嵌套调用链
// textFile → flatMap → map 被编译为:
rdd1.iterator(partition, context) // textFile
.flatMap(f1) // flatMap
.map(f2) // map
// 等价于 f2(f1(read(partition))) → 逐条流水线,不落盘
Pipeline 优化效果:数据从 HDFS 读出后直接流经 flatMap → map,中途不落盘、不跨网络,极大减少 I/O。
五、数据本地性调度
TaskScheduler 分发 Task 时遵循延迟调度策略:
scss
PROCESS_LOCAL (最优) → NODE_LOCAL → RACK_LOCAL → ANY
0ms 立即调度 → 等待 3s → 等待 6s → 强制调度
bash
# 调整本地性等待时间
--conf spark.locality.wait.process=3s # PROCESS_LOCAL 等待
--conf spark.locality.wait.node=3s # NODE_LOCAL 等待
--conf spark.locality.wait.rack=3s # RACK_LOCAL 等待
六、Stage 间串行 vs Stage 内并行
| 维度 | Stage 间 | Stage 内 |
|---|---|---|
| 执行模式 | 串行(父子依赖) | 并行(Partition 级) |
| 桥接 | MapOutputTracker | --- |
| 最大并行度 | --- | Partition 数 = Task 数 |
七、总结
| 要点 | 总结 |
|---|---|
| 切分算法 | finalRDD 回溯 → 遇 ShuffleDep 切分 → 递归构建 Stage DAG |
| Pipeline | Narrow 算子函数组合成迭代器链,逐条流水线执行 |
| 提交顺序 | submitStage 递归 → 先父后子 → MapOutputTracker 桥接 |
| 并行度 | Stage 间串行,Stage 内并行(Partition 数 = Task 数) |
金句:Stage 切分不是"人工标注"的------它是 DAGScheduler 从 RDD 血缘中自动推断出的最优执行计划。Narrow 是一气呵成的流水线,Wide 是必须交接的中转站。
作者:starzy | AI Data Engineer / 大数据技术实践者
博客:blog.starzy.cn | GitHub:starzy1990.github.io
专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践