ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spark Job执行流程与调度机制深度解析

Spark Job执行流程与调度机制深度解析

1. Spark Job 执行流程全景透视

当我们在Spark应用中调用一个Action算子时,背后会触发一系列复杂的调度与执行过程。这个看似简单的操作实际上经历了DAG构建、Stage划分、Task调度等多个关键阶段。理解这个完整流程对于性能调优和故障排查至关重要。

以最常见的collect()操作为例,当这个Action被调用时,Spark会立即启动以下机制:

  1. 首先在Driver端生成一个Job实例
  2. 根据RDD的血缘关系(Lineage)构建DAG图
  3. DAGScheduler将DAG拆分为多个Stage
  4. TaskScheduler将Stage转化为具体的TaskSet
  5. Executor上的线程池最终执行这些Task

关键提示:每个Action算子都会生成独立的Job,而一个Job可能包含多个Stage,这取决于RDD之间的依赖关系是窄依赖(Narrow Dependency)还是宽依赖(Wide Dependency)。

2. 核心调度组件深度解析

2.1 DAGScheduler 的工作机制

DAGScheduler是Spark调度体系的核心大脑,主要负责以下职能:

  • 将逻辑执行计划(DAG)转化为物理执行计划(Stage)
  • 处理Shuffle依赖关系
  • 决定Stage的最佳执行顺序
  • 处理故障恢复和推测执行

其内部维护着几个关键数据结构:

// 伪代码表示关键数据结构 class DAGScheduler { private[spark] val jobIdToActiveJob = new HashMap[Int, ActiveJob] private[spark] val stageIdToStage = new HashMap[Int, Stage] private[spark] val shuffleIdToMapStage = new HashMap[Int, ShuffleMapStage] }

2.2 TaskScheduler 的实现细节

TaskScheduler是实际任务调度的执行者,主要职责包括:

  • 按照调度策略(FIFO/FAIR)分配Task
  • 处理任务本地性(Locality)优化
  • 处理任务失败和重试
  • 与集群管理器(Cluster Manager)交互

Spark提供了多种调度策略实现:

  1. FIFO调度:默认策略,先进先出
  2. FAIR调度:支持权重分配和资源池
  3. 自定义调度:通过实现TaskScheduler接口扩展

3. 从Action到Task的完整链路分析

3.1 Action算子触发的Job提交

当调用如count()、saveAsTextFile()等Action算子时,SparkContext会通过runJob方法启动整个流程:

def runJob[T, U]( rdd: RDD[T], func: (TaskContext, Iterator[T]) => U, partitions: Seq[Int], resultHandler: (Int, U) => Unit): Unit = { // 触发DAGScheduler的job提交 dagScheduler.runJob(rdd, cleanedFunc, partitions, callSite, resultHandler, localProperties.get) }

这个调用会经历以下关键步骤:

  1. 创建JobSubmitted事件
  2. 放入DAGScheduler的事件队列
  3. 由事件循环线程处理

3.2 Stage划分算法详解

Stage划分的核心依据是RDD的依赖关系类型:

  • 窄依赖(Narrow Dependency):父RDD的每个分区最多被子RDD的一个分区使用
  • 宽依赖(Wide Dependency):父RDD的分区可能被子RDD的多个分区使用

划分算法伪代码:

1. 从最终的RDD开始反向遍历 2. 遇到宽依赖就断开,形成新的Stage 3. 继续向上遍历直到所有RDD处理完毕

3.3 Task生成与调度过程

每个Stage会被转化为一组Task:

  • ShuffleMapStage生成ShuffleMapTask
  • ResultStage生成ResultTask

Task调度考虑以下因素:

  1. 数据本地性级别:
    • PROCESS_LOCAL > NODE_LOCAL > RACK_LOCAL > ANY
  2. 资源可用性
  3. 推测执行需求

4. 性能优化实战技巧

4.1 调度延迟优化方案

通过Spark UI观察调度延迟时,可以关注以下指标:

  • Scheduler Delay:任务排队等待时间
  • Task Deserialization Time:任务反序列化耗时
  • GC Time:垃圾收集停顿时间

优化建议:

  1. 增加spark.scheduler.mode配置为FAIR
  2. 调整spark.locality.wait参数(默认3s)
  3. 减少序列化开销,使用Kryo序列化

4.2 数据倾斜处理策略

常见数据倾斜表现:

  • 少数Task执行时间远长于其他Task
  • Stage卡在99%进度无法完成

解决方案:

// 方法1:加盐处理 val skewedRDD = rdd.map{ case (key, value) => val salt = random.nextInt(10) (s"${key}_$salt", value) } // 方法2:两阶段聚合 val partialAgg = rdd.map{ case (key, value) => val partitionKey = getPartition(key) (partitionKey, (key, value)) }.aggregateByKey(...)

5. 高级特性与内部机制

5.1 动态资源分配机制

Spark支持根据负载动态调整Executor数量,相关配置:

spark.dynamicAllocation.enabled=true spark.dynamicAllocation.minExecutors=1 spark.dynamicAllocation.maxExecutors=100 spark.dynamicAllocation.initialExecutors=2

工作原理:

  1. 周期性检查任务队列积压情况
  2. 根据spark.dynamicAllocation.schedulerBacklogTimeout触发扩容
  3. 根据spark.dynamicAllocation.executorIdleTimeout触发缩容

5.2 推测执行(Speculative Execution)

当某些Task明显慢于其他Task时,Spark会启动备份任务:

spark.speculation=true spark.speculation.interval=100ms // 检查间隔 spark.speculation.multiplier=1.5 // 慢于中位数的倍数 spark.speculation.quantile=0.75 // 触发比例

实现要点:

  1. 定期统计已完成Task的运行时间
  2. 计算中位数和指定分位数
  3. 对落后Task启动备份执行

6. 生产环境问题排查指南

6.1 常见调度问题诊断

问题现象1:Job长时间处于ACCEPTED状态 可能原因:

  • 集群资源不足
  • 最大并行度(spark.scheduler.maxRegisteredResourcesWaitingTime)设置过小

问题现象2:Stage反复重试 排查步骤:

  1. 检查Executor日志中的错误信息
  2. 确认网络连接稳定性
  3. 验证数据完整性

6.2 关键指标监控方案

建议监控以下核心指标:

指标名称监控阈值应对措施
任务等待时间>1s调整调度策略
任务执行时间方差>30%检查数据倾斜
失败任务比例>5%检查资源稳定性

实现方式:

# 示例:通过REST API获取指标 import requests metrics = requests.get("http://driver:4040/api/v1/applications/[appId]/stages")

7. 调度系统调优实战

7.1 资源分配黄金法则

Executor配置经验公式:

executor_cores = min(5, total_cores_per_node - 1) executor_memory = (node_memory - 1GB) / executors_per_node

典型配置示例:

spark-submit --executor-cores 4 \ --executor-memory 12g \ --num-executors 20

7.2 并行度优化策略

合理设置并行度的原则:

  1. 至少是集群总核数的2-3倍
  2. 对于shuffle操作,分区数应大于集群总核数
  3. 避免产生过多小文件

调整方法:

// 方法1:设置全局默认并行度 spark.conf.set("spark.default.parallelism", 200) // 方法2:针对特定RDD调整 val rdd = inputRDD.repartition(100)

8. 深入Task执行引擎

8.1 Task运行生命周期

单个Task的执行流程:

  1. 序列化任务代码和闭包
  2. 传输到Executor
  3. 反序列化并加载依赖
  4. 执行计算逻辑
  5. 返回结果或shuffle数据

关键性能影响因素:

  • 序列化效率
  • 网络带宽
  • 内存压力
  • JVM优化

8.2 内存管理机制

Spark内存分为几个区域:

  1. Execution Memory:shuffle/join/sort使用
  2. Storage Memory:缓存数据使用
  3. User Memory:用户数据结构
  4. Reserved Memory:系统保留

配置建议:

spark.memory.fraction=0.6 // JVM堆中用于执行和存储的比例 spark.memory.storageFraction=0.5 // 存储内存占比

9. 调度系统高级特性

9.1 黑名单机制

当某些节点频繁失败时,Spark会将其加入黑名单:

spark.blacklist.enabled=true spark.blacklist.timeout=1h

实现原理:

  1. 记录Task失败次数
  2. 超过spark.blacklist.task.maxTaskAttempts后屏蔽节点
  3. 超时后自动恢复

9.2 屏障调度模式

用于支持需要全局同步的算法:

rdd.barrier().mapPartitions { iter => // 所有分区同时执行此代码 }

特性:

  1. 确保所有Task同时启动
  2. 失败时整个Stage重试
  3. 不支持动态资源分配

10. 未来演进方向

Spark调度系统正在向以下方向发展:

  1. 更细粒度的资源调度(GPU/FPGA)
  2. 与Kubernetes深度集成
  3. 自适应查询执行优化
  4. 混合调度策略支持

实际使用中发现,合理配置调度参数可以提升30%以上的集群利用率。对于复杂的批处理作业,建议采用FAIR调度策略并设置合理的资源池权重。在遇到数据倾斜问题时,两阶段聚合通常是最高效的解决方案。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进