Spark RPC框架原始碼分析(三)Spark心跳機制分析

zzzzMing發表於2019-01-17

一.Spark心跳概述

前面兩節中介紹了Spark RPC的基本知識,以及深入剖析了Spark RPC中一些原始碼的實現流程。

具體可以看這裡:

這一節我們來看看一個Spark RPC中的運用例項--Spark的心跳機制。當然這次主要還是從程式碼的角度來看。

Spark心跳

我們首先要知道Spark的心跳有什麼用。心跳是分散式技術的基礎,我們知道在Spark中,是有一個Master和眾多的Worker,那麼Master怎麼知道每個Worker的情況呢,這就需要藉助心跳機制了。心跳除了傳輸資訊,另一個主要的作用就是Worker告訴Master它還活著,當心跳停止時,方便Master進行一些容錯操作,比如資料轉移備份等等。

與之前講Spark RPC一樣,我們同樣分成兩部分來分析Spark的心跳機制,分為服務端(Spark Context)和客戶端(Executor)。

二. Spark心跳服務端heartbeatReceiver解析

我們可以發現,SparkContext中有關於心跳的類以及RpcEndpoint註冊程式碼。

class SparkContext(config: SparkConf) extends Logging {
    ......
    private var _heartbeatReceiver: RpcEndpointRef = _
    ......
    //向 RpcEnv 註冊 Endpoint。
    _heartbeatReceiver = env.rpcEnv.setupEndpoint(HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))
    ......
      val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
    _schedulerBackend = sched
    _taskScheduler = ts
    _dagScheduler = new DAGScheduler(this)
    _heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)
    ......
}

這裡rpcEnv已經在上下文中建立好,通過setupEndpoint向rpcEnv註冊一個心跳的Endpoint。還記得上一節中HelloworldServer的例子嗎,在setupEndpoint方法中,會去呼叫Dispatcher建立這個Endpoint(這裡就是HeartbeatReceiver)對應的Inbox和EndpointRef,然後在Inbox監聽是否有新訊息,有新訊息則處理它。註冊完會返回一個EndpointRef(注意這裡有Refer,即是客戶端,用來傳送訊息的)。

所以這一句

_heartbeatReceiver = env.rpcEnv.setupEndpoint(HeartbeatReceiver.ENDPOINT_NAME, new HeartbeatReceiver(this))

就已經完成了心跳服務端監聽的功能。
那麼這條程式碼的作用呢?

_heartbeatReceiver.ask[Boolean](TaskSchedulerIsSet)

這裡我們要看上面那句val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode),它會根據master url建立SchedulerBackend和TaskScheduler。這兩個類都是和資源排程有關的,所以需要藉助心跳機制來傳送訊息。其中TaskScheduler負責任務排程資源分配,SchedulerBackend負責與Master、Worker通訊收集Worker上分配給該應用使用的資源情況。

這裡主要是告訴HeartbeatReceiver(心跳)的監聽端,告訴它TaskScheduler這個東西已經設定好啦。HeartbeatReceiver就會迴應你說好的,我知道的,並持有這個TaskScheduler。

到這裡服務端heartbeatReceiver就差不多完了,我們可以發現,HeartbeatReceiver除了向RpcEnv註冊並監聽訊息之外,還會去持有一些資源排程相關的類,比如TaskSchedulerIsSet。

三. Spark心跳客戶端傳送心跳解析

傳送心跳傳送在Worker,每個Worker都會有一個Executor,所以我們可以發現在Executor中傳送心跳的程式碼。

private[spark] class Executor(
    executorId: String,
    executorHostname: String,
    env: SparkEnv,
    userClassPath: Seq[URL] = Nil,
    isLocal: Boolean = false)
  extends Logging {
  ......
  // must be initialized before running startDriverHeartbeat()
  //建立心跳的 EndpointRef
  private val heartbeatReceiverRef = RpcUtils.makeDriverRef(HeartbeatReceiver.ENDPOINT_NAME, conf, env.rpcEnv)
  ......
  startDriverHeartbeater()
  ......
    /**
   * Schedules a task to report heartbeat and partial metrics for active tasks to driver.
   * 用一個 task 來報告活躍任務的資訊以及傳送心跳。
   */
  private def startDriverHeartbeater(): Unit = {
    val intervalMs = conf.getTimeAsMs("spark.executor.heartbeatInterval", "10s")

    // Wait a random interval so the heartbeats don't end up in sync
    val initialDelay = intervalMs + (math.random * intervalMs).asInstanceOf[Int]

    val heartbeatTask = new Runnable() {
      override def run(): Unit = Utils.logUncaughtExceptions(reportHeartBeat())
    }
    //heartbeater是一個單執行緒執行緒池,scheduleAtFixedRate 是定時執行任務用的,和 schedule 類似,只是一些策略不同。
    heartbeater.scheduleAtFixedRate(heartbeatTask, initialDelay, intervalMs, TimeUnit.MILLISECONDS)
  }
  ......
}

可以看到,在Executor中會建立心跳的EndpointRef,變數名為heartbeatReceiverRef。

然後我們主要看startDriverHeartbeater()這個方法,它是關鍵。
我們可以看到最後部分程式碼

    val heartbeatTask = new Runnable() {
      override def run(): Unit = Utils.logUncaughtExceptions(reportHeartBeat())
    }
    heartbeater.scheduleAtFixedRate(heartbeatTask, initialDelay, intervalMs, TimeUnit.MILLISECONDS)

heartbeatTask是一個Runaable,即一個執行緒任務。scheduleAtFixedRate則是java concurrent包中用來執行定時任務的一個類,這裡的意思是每隔10s跑一次heartbeatTask中的執行緒任務,超時時間30s。

為什麼到這裡還是沒看到heartbeatReceiverRef呢,說好的傳送心跳呢?別急,其實在heartbeatTask執行緒任務中又呼叫了另一個方法,我們到裡面去一探究竟。

private[spark] class Executor(
    executorId: String,
    executorHostname: String,
    env: SparkEnv,
    userClassPath: Seq[URL] = Nil,
    isLocal: Boolean = false)
  extends Logging {
  ......
  private def reportHeartBeat(): Unit = {
    // list of (task id, accumUpdates) to send back to the driver
    val accumUpdates = new ArrayBuffer[(Long, Seq[AccumulatorV2[_, _]])]()
    val curGCTime = computeTotalGcTime()

    for (taskRunner <- runningTasks.values().asScala) {
      if (taskRunner.task != null) {
        taskRunner.task.metrics.mergeShuffleReadMetrics()
        taskRunner.task.metrics.setJvmGCTime(curGCTime - taskRunner.startGCTime)
        accumUpdates += ((taskRunner.taskId, taskRunner.task.metrics.accumulators()))
      }
    }

    val message = Heartbeat(executorId, accumUpdates.toArray, env.blockManager.blockManagerId)
    try {
      //終於看到 heartbeatReceiverRef 的身影了
      val response = heartbeatReceiverRef.askWithRetry[HeartbeatResponse](
          message, RpcTimeout(conf, "spark.executor.heartbeatInterval", "10s"))
      if (response.reregisterBlockManager) {
        logInfo("Told to re-register on heartbeat")
        env.blockManager.reregister()
      }
      heartbeatFailures = 0
    } catch {
      case NonFatal(e) =>
        logWarning("Issue communicating with driver in heartbeater", e)
        heartbeatFailures += 1
        if (heartbeatFailures >= HEARTBEAT_MAX_FAILURES) {
          logError(s"Exit as unable to send heartbeats to driver " +
            s"more than $HEARTBEAT_MAX_FAILURES times")
          System.exit(ExecutorExitCode.HEARTBEAT_FAILURE)
        }
    }
  }
  ......
  
}

可以看到,這裡heartbeatReceiverRef和我們上一節的例子,HelloworldClient類似,核心也是呼叫了askWithRetry()方法,這個方法是通過同步的方式傳送Rpc訊息。而這個方法裡其他程式碼其實就是獲取task的資訊啊,或者是一些容錯處理。核心就是呼叫askWithRetry()方法來傳送訊息。

看到這你就明白了吧。Executor初始化便會用一個定時任務不斷髮送心跳,同時當有task的時候,會獲取task的資訊一併傳送。這就是心跳的大概內容了。

總的來說Spark心跳的程式碼也是比較雜的,不過這些也都是為了讓設計更加高耦合,低內聚,讓這些程式碼更加方便得複用。不過通過層層剖析,我們還是發現其實它底層就是我們之前說到的Spark RPC框架的內容!!

OK,Spark RPC三部曲完畢。如果你能看到這裡那不容易呀,給自己點個贊吧!!


推薦閱讀 :
從分治演算法到 MapReduce
大資料儲存的進化史 --從 RAID 到 Hadoop Hdfs
一個故事告訴你什麼才是好的程式設計師

相關文章