当我们通过SparkContext
提交后,很多情况下,我们需要提前终止Job的执行,此时可以通过 SparkContext.cancelJobGroup()
方法完成终止 Job / Stage / Task 的需求。
先说一下什么是 JobGroup
, 一个SQL可能被分割为多个Job同时去执行,此时,这些Job属于同一个 JobGroup 。groupId 会保存在 Job 的 properties 中的 SPARK_JOB_GROUP_ID = "spark.jobGroup.id"
属性中
KillTask 消息从提交到执行的传递流程
DAGScheduler 提交 cancel job请求
SparkContext.cancelJobGroup(groupId: String) // SparkContext 本身只是作为对 User提供 action的接口,实际将任务转给 DAGScheduler
DAGScheduler.cancelJobGroup(groupId: String) // 此过程很重,通过 JobGroupCancelled(groupId) 消息实现自身的异步处理
// 进入 DAGScheduler 真正的事件处理
handleJobGroupCancelled(groupId, reason) // 根据 groupId 过滤出该group 中目前还在运行的 jobs,依次进行 cancel
handleJobCancellation(jobId, reason)
failJobAndIndependentStages(job, failureReason, exception)
cancelRunningIndependentStages(job, failureReason) // 根据job 过滤出正在运行的stage,依次进行cancel
taskScheduler.cancelTasks(stageId, shouldInterruptTaskThread(job))
// TaskSchedulerImpl
cancelTasks(stageId: Int, interruptThread: Boolean)
killAllTaskAttempts(stageId, interruptThread, reason = "Stage cancelled") // 根据 stageId 对应 Map[Int, TaskSetManager] 结构,遍历每个 TaskSetManager 中的每个 task(tid) 和其对应的 executor(execId), 调用backend 进行 kill
backend.killTask(tid, execId, interruptThread, reason)
对于Task的具体操作交给 SchedulerBackend
来具体操作,Yarn, K8S 可能每个平台的实现都不一样,自己去 Kill Task 把, DAGScheduler
自己的事情完成了
SchedulerBackend 发送Kill消息到 Executor
CoarseGrainedSchedulerBackend
是基于Yarn的实现,内部的所有操作还是基于RPC通信,通过Event来完成相关操作的。
- CoarseGrainedSchedulerBackend.killTask() – KillTask EVENT --> CoarseGrainedSchedulerBackend.DriverEndpoint // CoarseGrainedSchedulerBackend 向 DriverEndpoint发送 KillTask 消息
- CoarseGrainedSchedulerBackend.DriverEndpoint – KillTask EVENT --> CoarseGrainedExecutorBackend // DriverEndpoint 在接收到 KillTask 消息后,根据 executorId 找到 executorEndpoint, 并发送 KillTask 消息
- CoarseGrainedExecutorBackend 在接收到 KillTask 消息后, 调用内部的 executor的
Executor.killTask(taskId, interruptThread, reason)
Executor 的 killTask 处理过程
- 如果开启
spark.task.reaper.enabled
(default=false)
,会根据 taskId 创建一个 TaskReaper 线程,在通过通过 taskReaperPool 线程池提交, 在线程内部调用TaskRunner.kill()
方法来终止Task - 如果没有开启
spark.task.reaper.enabled
,根据tid找到TaskRunner
对象,直接调用 kill 方法,杀死Task。
TaskRunner 线程的生命周期
在继续下文之前,先要说明一些 Task 运行的生命周期的相关背景,熟悉的请跳过。
- 准备好相关的变量,管理工具,更新运行状态
- 更新task的dependencies
- 通过 deserializer 反序列化 task
- 调用 Task.run() 方法执行Task
- Task执行结束后,清理Task相关资源
- 调用 env.serializer 将执行结果序列化为bytes
- 更新整个执行过程中的Task metrics
- 根据执行结果大小判断,如果结果比较小的,直接在 TaskState.FINISHED 的心跳中返回结果,否则,通过异步返回执行结果
TaskRunner kill Task过程
Task 在执行 run() 方法前,会获取当前线程实例,再通过 TaskRunner.kill() -> Task.kill() -> taskThread.interrupt()
方式,实现终止当前Task,再捕获这个一场即可。
Executor 在 Shutdown 过程中是如果造成 DeadLock 的
CoarseGrainedExecutorBackend 注册Signal事件处理
CoarseGrainedExecutorBackend
负责运行具体的Executor,为了保证进行不泄漏,会向JVM注册系统事件处理,这样就可以接收系统信号,作出JVM退出等相关操作。
Singnal信号注册
CoarseGrainedExecutorBackend.main()
CoarseGrainedExecutorBackend.run()
Utils.initDaemon(log)
SignalUtils.registerLogger(log) // 这里会系统注册 "TERM", "HUP", "INT" 三个事件处理
SignalUtils.register(sig)
SignalUtils.register(signal, failMessage, logStackTrace)
ActionHandler 负责 signal 信号处理
在 SignalUtils.register()
方法内,每个系统事件对应一个 ActionHandler
对象, ActionHandler
对象内存放一组Action方法,每次接收到 signal, 会执行一遍全部Action方法。
// ActionHandler 在初始化的时候先记录了系统原来的 SignalHandler,然后向系统注册自己用于Signal处理
// original signal handler, before this handler was attached
private val prevHandler: SignalHandler = Signal.handle(signal, this)
/**
* Called when this handler's signal is received. Note that if the same signal is received
* before this method returns, it is escalated to the previous handler.
*/
override def handle(sig: Signal): Unit = {
// 进入Signal 处理,因为我已经在处理事件了,在这个是时候,如果再来一个相同事件,就使用系统默认的Handle 来处理
// register old handler, will receive incoming signals while this handler is running
Signal.handle(signal, prevHandler)
// Run all actions, escalate to parent handler if no action catches the signal
// (i.e. all actions return false). Note that calling `map` is to ensure that
// all actions are run, `forall` is short-circuited and will stop evaluating
// after reaching a first false predicate.
// 执行所有的action方法,对于log就直接返回false
val escalate = actions.asScala.map(action => action()).forall(_ == false)
if (escalate) {
// 执行完我们自己注册的action方法,再调用一次系统默认的handler来处理,保证不走样
prevHandler.handle(sig)
}
// 再次注册handler本身,准备处理下一个事件
// re-register this handler
Signal.handle(signal, this)
}
对于TERM事件, JVM 默认的SignalHandler 是在 Terminator 中注册的:
class Terminator {
private static SignalHandler handler = null;
static void setup() {
if (handler != null) return;
SignalHandler sh = new SignalHandler() {
public void handle(Signal sig) {
Shutdown.exit(sig.getNumber() + 0200);
}
};
handler = sh;
...
try {
Signal.handle(new Signal("TERM"), sh);
} catch (IllegalArgumentException e) {
}
}
}
通过 2021-06-10 17:21:19,801 SIGTERM handler ERROR org.apache.spark.executor.CoarseGrainedExecutorBackend: RECEIVED SIGNAL TERM
可以看到 CoarseGrainedExecutorBackend 收到了 TERM 信号,并执行了对应的Action
SparkShutdownHookManager 的设计和调用
- 通过Signal的注册,JVM可以接收到 TERM 等进程信号,然后会调用到 Terminator 去终止进程。终止进程的时候,JVM会调用在JVM中注册的 ShutdownHook.
- 通过
org.apache.hadoop.util.ShutdownHookManager.get()
给 Runtime 注册一个 ShutdownHook 线程。 - 这个hook线程内维护了一组
SparkShutdownHook
,SparkShutdownHook
为无参数,无返回值,具有优先级的方法,线程执行时根据优先级依次执行 - Executor 就注册了一个这样的方法,确保 JVM 退出的时候,会执行内部的
Executor.stop()
方法
SparkShutdownHookManager 执行Executor.stop() 流程
- run all hooks
- run Executor shutdown hook
- executor execute stop() method to close spark env
- NettyRpcEnv.shutdown()
- NettyRpcEnv.cleanup()
- Dispatcher.stop()
- dispatcher 对每个 endpoint 维护了一个 SharedMessageLoop,同时内部还维护了一个独立的 SharedMessageLoop 需要全部进行stop
- SharedMessageLoop 内部参考了Netty的React 模型, 内部有一个线程池,为了保证消息不丢失,SML 关闭的时候必须等待内部线程全部关闭
SharedMessageLoop 平滑关闭
在上面的执行Executor清理操作的时候,需要等待RPC 中的 SharedMessageLoop
线程都被平滑关闭掉。但是其中的 dispatcher-event-loop-1
线程会再次接收到会导致系统退出的事件
- 在主方法 receiveLoop() 中死循环处理消息
- 每次处理都是将 inbox 中接收的消息
- inbox.process(dispatcher: Dispatcher) : 循环获取 inbox 中的消息,并进行RPC处理
- 当处理到 RemoteProcessDisconnected 消息时, 调用 WorkerWatcher.onDisconnected() 处理
- 如果 endpoint 地址是当前worker 本身的地址, 调用 System.exit(-1) 退出 JVM
- 等待获取 java.lang.Shutdown 类的锁,但是该锁之前已经被加锁。
DeadLock