ARTICLE · 997892
【极速学习spark源码】Task 在 Executor 上的完整生命周期
基于 Spark 4.2,分支
branch-4.2阅读时长约 12 分钟 · 进阶
背景
前面我们已经看到 Driver 端如何创建 TaskDescription,并通过 SchedulerBackend 发给 Executor。
那 Executor 收到 Task 后发生了什么?
这篇文章沿着 Executor 端代码走一遍:
LaunchTask
→ Executor.launchTask
→ TaskRunner.run
→ Task.run
→ ResultTask.runTask / ShuffleMapTask.runTask
→ 状态回传 Driver
一、入口:LaunchTask 消息
Driver 发给 Executor 的 RPC 消息定义在 CoarseGrainedClusterMessage.scala:45:
case class LaunchTask(data: SerializableBuffer) extends CoarseGrainedClusterMessageExecutor 端收到消息的位置在 CoarseGrainedExecutorBackend.scala:181-187:
case LaunchTask(data) =>val taskDesc = TaskDescription.decode(data.value)executor.launchTask(this, taskDesc)
从这里开始,任务进入真正的 Executor 执行流程。
二、Executor.launchTask:创建 TaskRunner
Executor.scala:249-257 是 Executor 类定义和说明。
入口方法在 Executor.scala:551-563:
def launchTask(context: ExecutorBackend, taskDescription: TaskDescription): Unit = {val tr = createTaskRunner(context, taskDescription)runningTasks.put(taskId, tr)val killMark = killMarks.get(taskId)if (killMark != null) {tr.kill(killMark._1, killMark._2)killMarks.remove(taskId)}threadPool.execute(tr)}
这一步只做三件事:
创建
TaskRunner放入
runningTasks丢进线程池执行
三、TaskRunner:Executor 端的任务线程
Executor.scala:687-691:
class TaskRunner(execBackend: ExecutorBackend,val taskDescription: TaskDescription,private val plugins: Option[PluginContainer]) extends Runnable
真正入口在 Executor.scala:806:
override def run(): Unit = {// 初始化上下文、反序列化 task、调用 task.run、处理结果或异常}
TaskRunner.run 做的事情很多,可以分成 6 段:
初始化线程名、classloader、serializer
回传
TaskState.RUNNING(Executor.scala:830)反序列化
taskDescription.serializedTask(Executor.scala:841-850)调用
task.run(...)(Executor.scala:887-895)序列化结果并决定直接/间接回传(
Executor.scala:940-1023)回传
TaskState.FINISHED(Executor.scala:1029)
四、Task.run:创建 TaskContext
Task.scala:61-74:
private[spark] abstract class Task[T](...)Task.run 入口在 Task.scala:87-93:
final def run(taskAttemptId: Long,attemptNumber: Int,metricsSystem: MetricsSystem,cpus: Int,resources: Map[String, ResourceInformation],plugins: Option[PluginContainer]): T = {
它会创建 TaskContextImpl。Task.scala:101-117:
val taskContext = new TaskContextImpl(stageId,stageAttemptId,partitionId,taskAttemptId,attemptNumber,numPartitions,taskMemoryManager,localProperties,metricsSystem,metrics,cpus,resources)
如果是 barrier stage,会包装成 BarrierTaskContext(Task.scala:119-123)。
最后,Task.scala:146-148 调用:
context.runTaskWithListeners(this)真正执行的抽象方法是 Task.scala:178:
def runTask(context: TaskContext): T五、ResultTask:执行 action 的 func
ResultTask.scala:55-71 定义 ResultTask。
入口在 ResultTask.scala:78:
override def runTask(context: TaskContext): U = {val ser = SparkEnv.get.closureSerializer.newInstance()val (rdd, func) = ser.deserialize[(RDD[T], (TaskContext, Iterator[T]) => U)](ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)func(context, rdd.iterator(partition, context))}
这段特别重要:
Driver 端序列化的是
(RDD, func)Executor 端反序列化出来
调用
rdd.iterator(partition, context)真正计算该分区把 iterator 交给 action 的 func
collect、count、take 这类最终返回结果给 Driver 的任务,都走 ResultTask。
六、ShuffleMapTask:写 shuffle 输出
ShuffleMapTask.scala:54-70 定义 ShuffleMapTask。
入口在 ShuffleMapTask.scala:82:
override def runTask(context: TaskContext): MapStatus = {val ser = SparkEnv.get.closureSerializer.newInstance()val rddAndDep = ser.deserialize[(RDD[_], ShuffleDependency[_, _, _])](ByteBuffer.wrap(taskBinary.value), Thread.currentThread.getContextClassLoader)val rdd = rddAndDep._1val dep = rddAndDep._2dep.shuffleWriterProcessor.write(rdd.iterator(partition, context), dep, mapId, partitionId, context)}
它不把用户结果回传给 Driver,而是:
计算当前 map partition
写 shuffle 文件
返回
MapStatus
MapStatus 告诉 Driver:这个 map 输出在哪里、每个 reduce 分区多大。
七、结果回传:DirectTaskResult / IndirectTaskResult
Task 执行完后,Executor 要把结果传回 Driver。
Executor.scala:940-944:先用 serializer 序列化任务返回值。
Executor.scala:988-992:构造 DirectTaskResult。
TaskResult.scala:33-44 定义了结果类型:
sealed trait TaskResult[T]case class DirectTaskResult[T](...) extends TaskResult[T]case class IndirectTaskResult[T](blockId: BlockId, size: Long) extends TaskResult[T]
如果结果较小,直接通过 RPC 回传;如果较大,写入 BlockManager,再回传一个 IndirectTaskResult(Executor.scala:1001-1023)。
成功回传在 Executor.scala:1029:
execBackend.statusUpdate(taskId, TaskState.FINISHED, serializedResult)八、Driver 端怎么接收结果
Executor backend 把状态发给 Driver:
ExecutorBackend.scala:27-28:statusUpdate(taskId, state, data)接口CoarseGrainedExecutorBackend.scala:267-279:把状态包装成StatusUpdate发给 DriverCoarseGrainedSchedulerBackend.scala:168-179:Driver 收到后调用scheduler.statusUpdate(...)
成功结果由 TaskResultGetter 处理:
TaskResultGetter.scala:65-78:反序列化DirectTaskResultTaskResultGetter.scala:78-105:遇到IndirectTaskResult,从 BlockManager 拉取真实结果TaskResultGetter.scala:111-122:交给scheduler.handleSuccessfulTask
然后 TaskScheduler 通知 DAGScheduler,最终走到 DAGScheduler.handleTaskCompletion。
九、失败和 kill 路径
Task 不是总能成功。
kill
CoarseGrainedClusterMessage.scala:47-48:KillTask消息CoarseGrainedExecutorBackend.scala:190-194:收到 kill 后调用executor.killTask(...)Executor.scala:603-607:查找正在运行的 TaskRunnerExecutor.scala:721-733:TaskRunner.killTask.scala:231-249:Task.killTaskContextImpl.scala:326-335:标记 interruptedTaskContextImpl.scala:339-343:已中断时抛TaskKilledException
普通失败
TaskContext.scala:199-207:runTaskWithListeners先检查 kill,再执行runTaskTaskContext.scala:208-222:异常时触发 failure/completion listenersExecutor.scala:1094-1112:普通异常转成ExceptionFailure,回传TaskState.FAILED
十、metrics 更新
Executor 端会记录大量指标:
Executor.scala:947-963:task deserialize time、run time、CPU time、GC time、result serialization、peak memoryExecutor.scala:966-982:把 task metrics 累加到 executor metrics sourceExecutor.scala:1139-1184:shuffle read/write metricsExecutor.scala:1122-1129:TaskRunner finally 清理 runningTasks,并通知 metricsPoller task completion
所以 Spark UI 里看到的 task time、shuffle read/write、spill、GC 等,大多来自这里。
十一、实战:观察 Task 级指标
val rdd = sc.parallelize(1 to 1000000, 4).map(_ + 1).filter(_ % 2 == 0)println(rdd.count())
打开 Spark UI 的 Stage 页面:
http://localhost:4040/stages/结果:
每个 partition 对应一个 Task
可以看到 Executor Run Time、Executor CPU Time、GC Time
如果涉及 shuffle,还能看到 Shuffle Read / Write
本地模式下 executor 可能显示为 driver 或 local executor
十二、总结
Executor 端 Task 生命周期:
Driver
→ LaunchTask(TaskDescription)
ExecutorBackend
→ Executor.launchTask
→ TaskRunner
→ statusUpdate(RUNNING)
→ deserialize Task
→ task.run
→ TaskContextImpl
→ ResultTask.runTask / ShuffleMapTask.runTask
→ serialize result
→ DirectTaskResult / IndirectTaskResult
→ statusUpdate(FINISHED / FAILED / KILLED)
Driver
→ TaskResultGetter
→ TaskScheduler.statusUpdate
→ DAGScheduler.handleTaskCompletion
三个认知:
Executor 运行的是 TaskRunner,不是直接运行 Task。 TaskRunner 负责反序列化、状态回传、metrics、异常处理。
ResultTask 和 ShuffleMapTask 的 runTask 完全不同。 一个返回用户结果,一个写 shuffle 并返回 MapStatus。
Task 结果可能直接回传,也可能间接回传。 大结果会先写 BlockManager,再让 Driver 通过
IndirectTaskResult拉取。
每天花费5分钟学习spark,让你技术之路走得更稳、更快。
喜欢的点个关注。