SparkContext 终止 Task 运行过程及Executor Shutdown 造成 DeadLock 分析

当我们通过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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值