理解 Spark 的任务执行层级(Job -> Stage -> Task)以及 DAGScheduler
和 TaskScheduler
如何划分 Stage 和调度 Task,是掌握 Spark 核心调度机制的关键。
任务执行层级:Job, Stage, Task
-
Job (作业):
- 定义: 由 Spark Action 操作(如
collect()
,count()
,saveAsTextFile()
,foreach()
)触发的顶层工作单元。一个 Action 对应一个 Job。 - 触发: 用户代码中的 Action 被调用时,SparkContext 会向 DAGScheduler 提交一个 Job。
- 数量: 一个 Spark 应用(Application)可以包含多个 Job,它们按 Action 被调用的顺序依次执行(默认 FIFO 调度)。
- 定义: 由 Spark Action 操作(如
-
Stage (阶段):
- 定义: Job 被划分成的更小的、可并行执行的计算阶段。划分 Stage 的核心依据是 Shuffle 依赖。
- 划分原理:
- 窄依赖 (Narrow Dependency): 父 RDD 的每个分区最多被一个子 RDD 分区使用(如
map
,filter
,union
)。窄依赖允许在同一个 Stage 内进行流水线式连续计算(Pipeline)。 - 宽依赖 (Shuffle Dependency): 父 RDD 的每个分区可能被多个子 RDD 分区使用(如
groupByKey
,reduceByKey
,join
)。宽依赖要求所有父分区数据都准备好,并经过 Shuffle Write(分区、排序、写入磁盘/内存)和 Shuffle Read(从不同节点拉取数据)的过程。宽依赖是 Stage 的边界。
- 窄依赖 (Narrow Dependency): 父 RDD 的每个分区最多被一个子 RDD 分区使用(如
- 类型:
- Result Stage: 负责执行 Job 中的最后一个操作(通常是 Action 本身的计算),最终产生结果。一个 Job 有且只有一个 Result Stage。
- Shuffle Map Stage: 负责为后续的 Stage(可以是另一个 Shuffle Map Stage 或 Result Stage)准备 Shuffle 数据(执行 Shuffle Write)。一个 Job 可以有零个或多个 Shuffle Map Stage。
- 并行度: 一个 Stage 包含多个 Task,其数量由该 Stage 的 RDD 分区数决定。同一个 Stage 内的所有 Task 执行相同的计算逻辑,但处理不同的数据分区。
-
Task (任务):
- 定义: Stage 中被分配到单个 Executor Core 上执行的最小工作单元。一个 Task 负责计算一个 RDD 分区。
- 类型:
- ShuffleMapTask: 属于 Shuffle Map Stage。负责读取输入数据,应用用户定义的函数,并将计算结果分区、排序、写入到本地存储(Shuffle 文件),以供下游 Stage 的 Task 读取。
- ResultTask: 属于 Result Stage。负责应用用户定义的函数到其分配的分区上,计算结果并将结果返回给 Driver 或写入外部存储。
- 位置: Task 在 Executor 的 线程池 中执行。一个 Executor 可以同时运行多个 Task(通常等于其分配的核心数)。
层级关系总结:
一个 Application (应用)
-> 包含多个 Job (作业) [由 Action 触发]
-> 每个 Job 被划分为多个 Stage (阶段) [由 Shuffle 依赖划分]
-> 每个 Stage 包含多个 Task (任务) [数量 = RDD 分区数]
-> 每个 Task 在一个 Executor Core 上执行
DAGScheduler:Stage 划分与调度
- 角色: 高级调度器。运行在 Driver 进程中。
- 核心职责:
- 构建 DAG (有向无环图): 根据用户代码中的 RDD 转换操作序列,构建 RDD 的依赖关系图(Lineage)。
- 划分 Stage:
- 从代表最终结果的 RDD(即 Action 操作的 RDD)开始,反向递归遍历 DAG。
- 遇到 Shuffle 依赖 (宽依赖) 时,就在该依赖处切断。切断点之前的 RDD 操作属于当前 Stage,切断点之后的 RDD 操作属于下一个 Stage。
- 继续反向遍历下一个 Stage,直到遍历完所有依赖。
- 最终将 Job 划分为一个 Result Stage 和 0 个或多个 Shuffle Map Stage。这些 Stage 形成一个或多个依赖链(Stage DAG)。
- 提交 Stage:
- 按照 Stage 的依赖关系(Stage DAG)顺序提交。优先提交没有父 Stage 或所有父 Stage 都已成功完成的 Stage。
- 在提交一个 Stage 之前,DAGScheduler 会检查该 Stage 需要计算的 RDD 分区是否已有缓存数据可用(避免重复计算)。
- 生成 TaskSet: 对于要提交的 Stage,DAGScheduler 会为该 Stage 创建一个 TaskSet。TaskSet 包含该 Stage 需要执行的所有 Task(ShuffleMapTask 或 ResultTask)。每个 Task 对应 Stage 中最终 RDD 的一个分区。
- 提交 TaskSet 给 TaskScheduler: 将生成的 TaskSet 交给 TaskScheduler 去执行。
- 处理 Stage 失败与容错:
- 监控 Task 执行状态(通过 TaskScheduler 回调)。
- 如果某个 Stage 失败(例如,Task 重试次数耗尽),DAGScheduler 会重新提交该 Stage 及其所有依赖的 Stage。
- 如果某个 Task 失败,会先由 TaskScheduler 尝试重试(在同一个 Executor 或另一个 Executor 上)。如果 TaskScheduler 重试失败,DAGScheduler 会收到通知并决定是否重新提交整个 Stage。
Stage 划分图示 (简化示例):
textFile = sc.textFile("hdfs://...") // RDD0 (Partitions: P1, P2, P3)
words = textFile.flatMap(_.split(" ")) // RDD1 (窄依赖 -> Stage0)
mapped = words.map(word => (word, 1)) // RDD2 (窄依赖 -> Stage0)
reduced = mapped.reduceByKey(_ + _) // RDD3 (宽依赖! Stage边界 -> Stage1)
filtered = reduced.filter(_._2 > 10) // RDD4 (窄依赖 -> Stage1)
result = filtered.collect() // Action! (Job, ResultStage = Stage1)
- Stage 划分过程:
- 从最终 RDD4 (Action 的 RDD) 开始反向遍历。
- RDD4 依赖 RDD3 (
filter
是窄依赖),继续。 - RDD3 依赖 RDD2 (
reduceByKey
是宽依赖!) -> 切断!- RDD3 和 RDD4 属于 Stage1 (Result Stage)。
- 切断点之前的 RDD0, RDD1, RDD2 属于 Stage0 (Shuffle Map Stage)。
- 继续反向遍历 Stage0:RDD2 依赖 RDD1 (
map
窄),RDD1 依赖 RDD0 (flatMap
窄),没有更多宽依赖,Stage0 结束。
- Stage DAG: Stage0 -> (Shuffle) -> Stage1
- Task:
- Stage0: 包含 3 个 ShuffleMapTask (因为 RDD0 有 3 个分区 P1, P2, P3)。
- Stage1: 包含
reduced.getNumPartitions
个 ResultTask (默认分区数通常继承父 RDD 或由spark.default.parallelism
控制)。
TaskScheduler:Task 调度与执行
- 角色: 低级调度器。运行在 Driver 进程中。
- 核心职责:
- 接收 TaskSet: 接收来自 DAGScheduler 的 TaskSet。
- 资源感知调度:
- 与 Cluster Manager 交互,为 TaskSet 中的 Task 申请 Executor 资源(如果 Executor 尚未启动或资源不足)。
- 跟踪集群中可用的 Executor 及其资源(CPU 核心、内存)。
- 调度策略 (FIFO/FAIR):
- FIFO (默认): 先提交的 TaskSet 优先获得资源。
- FAIR: 根据配置的池(Pool)和权重(Weight)进行公平调度,允许多个 Job 的 Task 共享资源。
- 任务分配与分发 (任务本地性 - Task Locality):
- 目标: 尽可能将 Task 调度到靠近其所需数据的 Executor 上运行,减少网络传输。
- 本地性级别 (优先级从高到低):
PROCESS_LOCAL
:数据在同一个 Executor 进程的内存中。NODE_LOCAL
:数据在同一节点(可能是另一个 Executor 或磁盘)。RACK_LOCAL
:数据在同一机架上的某个节点。ANY
:数据在网络上的任意地方。
- 调度过程:
- TaskScheduler 为每个 Task 计算其偏好位置(Preferred Locations),通常由 RDD 的分区位置信息(如 HDFS 块位置)决定。
- 遍历 TaskSet 中所有未调度的 Task。
- 对于当前 Executor 上可用的资源(Slot),尝试分配一个满足其最高本地性级别(或降级后可接受级别)的 Task。
- 如果等待一段时间后仍然无法满足较高本地性要求,会降低要求(如从 NODE_LOCAL 降到 RACK_LOCAL 或 ANY)以避免饥饿。这是 延迟调度(Delay Scheduling) 策略。
- 发送 Task 到 Executor: 通过 SchedulerBackend(与具体 Cluster Manager 交互的组件)将序列化后的 Task 代码和所需数据发送给选定的 Executor。
- 监控 Task 执行:
- 接收 Executor 发送的 Task 状态更新(如开始、运行中、完成、失败)。
- 处理 Task 失败:
- 重新提交失败的 Task(在同一个 Executor 或其他 Executor 上)。
- 如果 Task 失败次数超过
spark.task.maxFailures
,标记整个 TaskSet 失败并通知 DAGScheduler。
- 处理 Executor 丢失:通知 DAGScheduler 该 Executor 上所有正在运行和未完成的 Task 失败。
- 结果处理: 对于 ResultTask,收集计算结果并返回给 DAGScheduler(最终给用户)。对于 ShuffleMapTask,主要是确认其完成状态和 Shuffle 输出位置(供下游 Stage 读取)。
DAGScheduler 与 TaskScheduler 协作流程总结
- 用户触发 Action:
filtered.collect()
被调用。 - DAGScheduler 工作:
- DAGScheduler 接收到 Job 提交。
- 根据 RDD 依赖图,识别出需要 Stage1 (Result Stage),发现它依赖 Stage0 (Shuffle Map Stage)。
- 检查 Stage0 是否已计算过(缓存)或其父 RDD 是否可用。如果没有,则 Stage0 需要先执行。
- DAGScheduler 先提交 Stage0(因为它是 Stage1 的父 Stage)。
- 为 Stage0 创建包含 3 个 ShuffleMapTask 的 TaskSet。
- 将 TaskSet(Stage0) 提交给 TaskScheduler。
- TaskScheduler 工作:
- 接收 TaskSet(Stage0)。
- 向 Cluster Manager 申请资源启动 Executor(如果尚未启动)。
- 根据 Task Locality 策略(考虑 HDFS 块位置),将 3 个 ShuffleMapTask 调度到合适的 Executor 上执行。
- 监控 Task 执行状态。
- Executor 执行:
- 接收到 Task 代码和数据。
- 执行 ShuffleMapTask:读取 HDFS 文件分区 ->
flatMap
->map
->reduceByKey
的 Map 端操作(分区、聚合) -> 将 Shuffle 输出写入本地磁盘/内存。 - 向 TaskScheduler 报告 Task 完成状态和 Shuffle 输出位置信息(
MapStatus
)。
- Stage0 完成:
- TaskScheduler 通知 DAGScheduler:Stage0 的所有 Task 成功完成,并提供了 Shuffle 输出位置。
- DAGScheduler 提交 Stage1:
- DAGScheduler 得知 Stage0 完成后,知道 Stage1 所需的数据(Shuffle 输出)已就绪。
- 为 Stage1 创建包含 N 个 ResultTask 的 TaskSet(N 取决于
reduced
RDD 的分区数)。 - 将 TaskSet(Stage1) 提交给 TaskScheduler。
- TaskScheduler 调度 Stage1 Tasks:
- 接收 TaskSet(Stage1)。
- 根据 Shuffle 输出位置(
MapStatus
)计算每个 ResultTask 的偏好位置(靠近其要读取的 Shuffle 数据块)。 - 将 ResultTask 调度到满足位置偏好的 Executor 上。
- Executor 执行 & 返回结果:
- Executor 执行 ResultTask:读取对应的 Shuffle 数据 ->
filter
-> 计算结果。 - 将每个 ResultTask 的计算结果 发送回 Driver。
- Executor 执行 ResultTask:读取对应的 Shuffle 数据 ->
- 结果收集 & Job 完成:
- TaskScheduler 收集所有 ResultTask 的结果。
- TaskScheduler 通知 DAGScheduler Job 成功完成。
- DAGScheduler 通知用户程序(
collect()
返回结果数组)。 - SparkContext 清理该 Job 相关的临时状态。
关键点:
- Stage 划分由 DAGScheduler 基于 Shuffle 依赖完成。 宽依赖是 Stage 的边界。
- TaskScheduler 负责具体的 Task 资源分配、调度(考虑本地性)和监控执行。
- Stage 按依赖顺序执行。 父 Stage 完成才能提交子 Stage。
- 一个 Stage 内的 Task 是并行执行的, 处理不同的数据分区。
- 任务本地性优化是减少网络传输、提升性能的核心策略。
理解这个流程对于分析 Spark 应用性能瓶颈(如数据倾斜、Shuffle 过慢、调度延迟)、进行调优(分区数、本地性等待时间)和排查错误至关重要。