Spark 核心调度机制

理解 Spark 的任务执行层级(Job -> Stage -> Task)以及 DAGSchedulerTaskScheduler 如何划分 Stage 和调度 Task,是掌握 Spark 核心调度机制的关键。

任务执行层级:Job, Stage, Task

  1. Job (作业):

    • 定义: 由 Spark Action 操作(如 collect(), count(), saveAsTextFile(), foreach())触发的顶层工作单元。一个 Action 对应一个 Job。
    • 触发: 用户代码中的 Action 被调用时,SparkContext 会向 DAGScheduler 提交一个 Job。
    • 数量: 一个 Spark 应用(Application)可以包含多个 Job,它们按 Action 被调用的顺序依次执行(默认 FIFO 调度)。
  2. 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 的边界
    • 类型:
      • 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 执行相同的计算逻辑,但处理不同的数据分区。
  3. 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 进程中。
  • 核心职责:
    1. 构建 DAG (有向无环图): 根据用户代码中的 RDD 转换操作序列,构建 RDD 的依赖关系图(Lineage)。
    2. 划分 Stage:
      • 从代表最终结果的 RDD(即 Action 操作的 RDD)开始,反向递归遍历 DAG。
      • 遇到 Shuffle 依赖 (宽依赖) 时,就在该依赖处切断。切断点之前的 RDD 操作属于当前 Stage,切断点之后的 RDD 操作属于下一个 Stage
      • 继续反向遍历下一个 Stage,直到遍历完所有依赖。
      • 最终将 Job 划分为一个 Result Stage 和 0 个或多个 Shuffle Map Stage。这些 Stage 形成一个或多个依赖链(Stage DAG)。
    3. 提交 Stage:
      • 按照 Stage 的依赖关系(Stage DAG)顺序提交。优先提交没有父 Stage所有父 Stage 都已成功完成的 Stage。
      • 在提交一个 Stage 之前,DAGScheduler 会检查该 Stage 需要计算的 RDD 分区是否已有缓存数据可用(避免重复计算)。
    4. 生成 TaskSet: 对于要提交的 Stage,DAGScheduler 会为该 Stage 创建一个 TaskSet。TaskSet 包含该 Stage 需要执行的所有 Task(ShuffleMapTask 或 ResultTask)。每个 Task 对应 Stage 中最终 RDD 的一个分区。
    5. 提交 TaskSet 给 TaskScheduler: 将生成的 TaskSet 交给 TaskScheduler 去执行。
    6. 处理 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 划分过程:
    1. 从最终 RDD4 (Action 的 RDD) 开始反向遍历。
    2. RDD4 依赖 RDD3 (filter 是窄依赖),继续。
    3. RDD3 依赖 RDD2 (reduceByKey 是宽依赖!) -> 切断!
      • RDD3 和 RDD4 属于 Stage1 (Result Stage)
      • 切断点之前的 RDD0, RDD1, RDD2 属于 Stage0 (Shuffle Map Stage)
    4. 继续反向遍历 Stage0:RDD2 依赖 RDD1 (map 窄),RDD1 依赖 RDD0 (flatMap 窄),没有更多宽依赖,Stage0 结束。
  • Stage DAG: Stage0 -> (Shuffle) -> Stage1
  • Task:
    • Stage0: 包含 3 个 ShuffleMapTask (因为 RDD0 有 3 个分区 P1, P2, P3)。
    • Stage1: 包含 reduced.getNumPartitionsResultTask (默认分区数通常继承父 RDD 或由 spark.default.parallelism 控制)。

TaskScheduler:Task 调度与执行

  • 角色: 低级调度器。运行在 Driver 进程中。
  • 核心职责:
    1. 接收 TaskSet: 接收来自 DAGScheduler 的 TaskSet。
    2. 资源感知调度:
      • Cluster Manager 交互,为 TaskSet 中的 Task 申请 Executor 资源(如果 Executor 尚未启动或资源不足)。
      • 跟踪集群中可用的 Executor 及其资源(CPU 核心、内存)。
    3. 调度策略 (FIFO/FAIR):
      • FIFO (默认): 先提交的 TaskSet 优先获得资源。
      • FAIR: 根据配置的池(Pool)和权重(Weight)进行公平调度,允许多个 Job 的 Task 共享资源。
    4. 任务分配与分发 (任务本地性 - 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) 策略。
    5. 发送 Task 到 Executor: 通过 SchedulerBackend(与具体 Cluster Manager 交互的组件)将序列化后的 Task 代码和所需数据发送给选定的 Executor。
    6. 监控 Task 执行:
      • 接收 Executor 发送的 Task 状态更新(如开始、运行中、完成、失败)。
      • 处理 Task 失败
        • 重新提交失败的 Task(在同一个 Executor 或其他 Executor 上)。
        • 如果 Task 失败次数超过 spark.task.maxFailures,标记整个 TaskSet 失败并通知 DAGScheduler。
      • 处理 Executor 丢失:通知 DAGScheduler 该 Executor 上所有正在运行和未完成的 Task 失败。
    7. 结果处理: 对于 ResultTask,收集计算结果并返回给 DAGScheduler(最终给用户)。对于 ShuffleMapTask,主要是确认其完成状态和 Shuffle 输出位置(供下游 Stage 读取)。

DAGScheduler 与 TaskScheduler 协作流程总结

  1. 用户触发 Action: filtered.collect() 被调用。
  2. 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
  3. TaskScheduler 工作:
    • 接收 TaskSet(Stage0)。
    • 向 Cluster Manager 申请资源启动 Executor(如果尚未启动)。
    • 根据 Task Locality 策略(考虑 HDFS 块位置),将 3 个 ShuffleMapTask 调度到合适的 Executor 上执行。
    • 监控 Task 执行状态。
  4. Executor 执行:
    • 接收到 Task 代码和数据。
    • 执行 ShuffleMapTask:读取 HDFS 文件分区 -> flatMap -> map -> reduceByKey 的 Map 端操作(分区、聚合) -> 将 Shuffle 输出写入本地磁盘/内存。
    • 向 TaskScheduler 报告 Task 完成状态和 Shuffle 输出位置信息(MapStatus)。
  5. Stage0 完成:
    • TaskScheduler 通知 DAGScheduler:Stage0 的所有 Task 成功完成,并提供了 Shuffle 输出位置。
  6. DAGScheduler 提交 Stage1:
    • DAGScheduler 得知 Stage0 完成后,知道 Stage1 所需的数据(Shuffle 输出)已就绪。
    • 为 Stage1 创建包含 N 个 ResultTask 的 TaskSet(N 取决于 reduced RDD 的分区数)。
    • 将 TaskSet(Stage1) 提交给 TaskScheduler
  7. TaskScheduler 调度 Stage1 Tasks:
    • 接收 TaskSet(Stage1)。
    • 根据 Shuffle 输出位置(MapStatus)计算每个 ResultTask 的偏好位置(靠近其要读取的 Shuffle 数据块)。
    • 将 ResultTask 调度到满足位置偏好的 Executor 上。
  8. Executor 执行 & 返回结果:
    • Executor 执行 ResultTask:读取对应的 Shuffle 数据 -> filter -> 计算结果。
    • 将每个 ResultTask 的计算结果 发送回 Driver
  9. 结果收集 & Job 完成:
    • TaskScheduler 收集所有 ResultTask 的结果。
    • TaskScheduler 通知 DAGScheduler Job 成功完成。
    • DAGScheduler 通知用户程序(collect() 返回结果数组)。
    • SparkContext 清理该 Job 相关的临时状态。

关键点:

  • Stage 划分由 DAGScheduler 基于 Shuffle 依赖完成。 宽依赖是 Stage 的边界。
  • TaskScheduler 负责具体的 Task 资源分配、调度(考虑本地性)和监控执行。
  • Stage 按依赖顺序执行。 父 Stage 完成才能提交子 Stage。
  • 一个 Stage 内的 Task 是并行执行的, 处理不同的数据分区。
  • 任务本地性优化是减少网络传输、提升性能的核心策略。

理解这个流程对于分析 Spark 应用性能瓶颈(如数据倾斜、Shuffle 过慢、调度延迟)、进行调优(分区数、本地性等待时间)和排查错误至关重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值