Spark 核心之 Stage 和 Task 原理剖析

简介: Spark中,Job由Action触发,DAGScheduler依Shuffle依赖切分为Stage:前序为ShuffleMapStage(生成中间文件),末尾为ResultStage(产出结果);每个Stage按分区数生成对应ShuffleMapTask或ResultTask。本文深度解析Stage划分、Task生成与序列化机制。(239字)

摘要:如果说 Job 是 Spark 的"任务单",Stage 就是"施工阶段",Task 就是每个工人的"具体活"。一个 Job 被 DAGScheduler 沿 Shuffle 边界切分为多个 Stage——前面的全是 ShuffleMapStage,最后一个必须是 ResultStage。每个 Stage 的 Partition 数决定了 Task 数量,ShuffleMapStage 产生 ShuffleMapTask(写 Shuffle 文件),ResultStage 产生 ResultTask(直接返回结果)。本文从 Stage 类型体系、DAG → Stage 切分源码、Task 生成与序列化、两种 Task 执行差异四个维度,配合 1 张原创深色架构图 + 完整源码分析,带你彻底看懂 Spark 最核心的执行引擎。

关键词:Spark Stage, ShuffleMapStage, ResultStage, ShuffleMapTask, ResultTask, DAGScheduler, Task 序列化, MapOutputTracker


一、开篇:Stage 和 Task 是什么关系?

先说结论:

Job = 用户的一个 Action 操作
  ├── Stage 0: ShuffleMapStage → 2 个 ShuffleMapTask
  └── Stage 1: ResultStage     → 3 个 ResultTask
概念 定义 数量
Stage Shuffle 边界切分的计算阶段 每个 Job 可有多个
Task 处理一个 Partition 的最小计算单元 每个 Stage 可有多个
ShuffleMapStage 输出 Shuffle 中间文件的 Stage Job 中除最后一个外的所有
ResultStage 输出最终结果的 Stage 每个 Job 有且仅有一个

二、Stage 与 Task 全景图

在这里插入图片描述

三、Stage 切分:从 RDD DAG 到 Stage

3.1 核心源码

// 源码:DAGScheduler.scala - 创建 ResultStage
private def createResultStage(finalRDD: RDD[_], func: (TaskContext, Iterator[_]) => _,
    partitions: Array[Int], jobId: Int, callSite: CallSite): ResultStage = {
  // 从 finalRDD 回溯 → 遇到 ShuffleDep → 创建 ShuffleMapStage
  val parents = getOrCreateParentStages(finalRDD, jobId)
  val id = nextStageId.getAndIncrement()
  new ResultStage(id, finalRDD, func, partitions, parents, jobId, callSite)
}

// 递归获取父 Stage
private def getOrCreateParentStages(rdd: RDD[_], firstJobId: Int): List[Stage] = {
  rdd.dependencies.flatMap {
    case shufDep: ShuffleDependency[_, _, _] =>
      getOrCreateShuffleMapStage(shufDep, firstJobId) :: Nil
    case _ => Nil  // NarrowDep 不切分
  }.toList
}

3.2 Stage 提交顺序

// 递归提交:先父后子
private def submitStage(stage: Stage): Unit = {
  val missing = getMissingParentStages(stage).sortBy(_.id)
  if (missing.isEmpty) {
    submitMissingTasks(stage, jobId.get)  // 无缺失父 Stage → 执行
  } else {
    for (parent <- missing) submitStage(parent)  // 递归提交父 Stage
  }
}

四、Task 生成:从 Stage 到 TaskSet

// 源码:DAGScheduler.scala - submitMissingTasks()
private def submitMissingTasks(stage: Stage, jobId: Int): Unit = {
  // 计算需要计算的 Partition(跳过已完成的)
  val partitionsToCompute = stage.findMissingPartitions()

  // 为每个 Partition 创建一个 Task
  val tasks: Seq[Task[_]] = stage match {
    case stage: ShuffleMapStage =>
      partitionsToCompute.map { id =>
        new ShuffleMapTask(stage.id, stage.rdd, stage.shuffleDep, ...)
      }
    case stage: ResultStage =>
      partitionsToCompute.map { id =>
        new ResultTask(stage.id, stage.rdd, stage.func, id, ...)
      }
  }

  // 封装为 TaskSet,提交给 TaskScheduler
  taskScheduler.submitTasks(new TaskSet(tasks.toArray, stage.id, ...))
}

Task 数量 = Stage 最后一个 RDD 的 Partition 数量。


五、两种 Stage 与两种 Task 对比

5.1 ShuffleMapStage + ShuffleMapTask

// ShuffleMapTask.runTask() — 执行逻辑
override def runTask(context: TaskContext): MapStatus = {
  val writer = new ShuffleWriter(partition, shuffleDep)
  // ① 执行 RDD 算子链(map/flatMap/filter...)
  val iter = rdd.iterator(partition, context)
  // ② 将结果写入 Shuffle 文件
  writer.write(iter)
  // ③ 返回 MapStatus(文件位置 + 分区长度)
  writer.stop(success = true).get
}

5.2 ResultStage + ResultTask

// ResultTask.runTask() — 执行逻辑
override def runTask(context: TaskContext): U = {
  // ① 执行 RDD 算子链
  val iter = rdd.iterator(partition, context)
  // ② 将最终结果应用 func(如 collect 的收集逻辑)
  func(context, iter)
  // ③ 序列化结果 → StatusUpdate → Driver
}

5.3 对比表

维度 ShuffleMapStage ResultStage
Task 类型 ShuffleMapTask ResultTask
输出 Shuffle 中间文件 最终计算结果
返回类型 MapStatus U (泛型)
一个 Job 中的数量 0~N 1(唯一)

六、Task 序列化

# 推荐 Kryo 序列化(比 Java 快 10 倍)
--conf spark.serializer=org.apache.spark.serializer.KryoSerializer
--conf spark.kryo.registrationRequired=true  # 强制注册
// 代码中注册 Kryo 类
val conf = new SparkConf()
  .set("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
  .registerKryoClasses(Array(classOf[MyDataClass], classOf[MyModel]))

为什么需要序列化? Driver 端的 Task 对象(包含 RDD 算子闭包)需要跨网络发送到 Executor,必须序列化为字节流。


七、总结

要点 总结
Stage 切分 遇到 ShuffleDependency 即切分,递归提交(先父后子)
Task 生成 每个 Partition → 一个 Task,类型由 Stage 决定
两种 Stage ShuffleMapStage(写 Shuffle) + ResultStage(返回结果)
序列化 Task 闭包必须可序列化,推荐 Kryo

金句:Stage 是 Spark 的"流水线工位",Task 是每个工位上的"工人"。Shuffle 就是工位之间的传送带——上一个工位写完,下一个工位才能开始。



作者:starzy | AI Data Engineer / 大数据技术实践者

博客:blog.starzy.cn | GitHub:starzy1990.github.io

专注 AI Agent · LangGraph · RAG · 大数据架构 · 数据工程实践

目录
相关文章
|
运维 网络安全 数据安全/隐私保护
Nextcloud一键部署:快速搭建如企业钉钉或飞书的团队协作平台
Nextcloud Hub 是一款来自德国的完全开源的云上托管内容协作平台。团队内成员可以通过移动、桌面和 Web 界面访问、共享和编辑文档,聊天和参与视频通话以及管理邮件、日历和项目。目前,Nextcloud的产品定位是在保障数据安全下的完整协作平台,功能类似国内的企业钉钉或飞书。Nextcloud官网:[https://nextcloud.com/](https://nextcloud.com/) 。
Nextcloud一键部署:快速搭建如企业钉钉或飞书的团队协作平台
|
1天前
|
缓存 安全 前端开发
基于 YOLO11 的番茄九类病害智能识别系统 | 源码识别系统分享
本项目基于YOLO11n构建番茄九类病害智能识别系统,支持单图/批量/视频/摄像头输入,精准定位病斑并输出“病害类型+严重等级(健康/轻/中/重)+图文防治建议”,实现从图像检测到农技服务的闭环。模型mAP@0.5达0.944,仅5MB,可部署于田间PAD。
|
开发者 黑灰产治理
阿里云开发者社区积分细则
阿里云开发者社区,积分规则、领取、过期等相关说明
4160 20
|
负载均衡 网络协议 数据库
【华为数通HCIP | 网络工程师】821-IGP高频题、易错题之OSPF(6)
【华为数通HCIP | 网络工程师】821-IGP高频题、易错题之OSPF(6)
1679 0
|
数据可视化 搜索推荐 Linux
SourceTree的基本使用(上)
今日目标 能够掌握SourceTree 安装 能够掌握SourceTree 连接远程仓库 能够掌握SourceTree克隆,拉取,提交,推送 能够掌握SourceTree 新建/切换/合并分支 能够掌握SourceTree 冲突解决 能够掌握SourceTree 提交PR(Pull Request)
1459 0
|
云安全 监控 负载均衡
游戏运行只会占用到服务器里面一个核心使用,其他核心不工作,是什么问题
游戏运行只占用服务器的一个核心,而其他核心不工作,可能有多种原因。以下分享一些常见的原因和处理的方案
|
存储 弹性计算 运维
大型企业自购服务器还是使用阿里云服务器更合适?
大型企业是选择自建机房还是选择上云?大型企业使用阿里云服务器省钱吗?
1232 0
大型企业自购服务器还是使用阿里云服务器更合适?
|
存储 NoSQL 关系型数据库
PostgreSQL中的变长数据结构Varlena
在本文是PostgreSQL学习系列---基础数据类型的第二篇,对PG中的Varlena类型进行了介绍,内容还会随着学习进行不断补充~
2155 0
|
JavaScript C++
VS2017使用utf-8+BOM的编码格式
VS2017使用utf-8+BOM的编码格式
1129 0
VS2017使用utf-8+BOM的编码格式
|
安全 Windows
内置管理员无法激活此应用
内置管理员无法激活此应用
591 3