Spark技術內幕:Master的故障恢復

Spark技術內幕:Master基於ZooKeeper的High Availability(HA)源碼實現  詳細闡述了使用ZK實現的Master的HA,那麼Master是如何快速故障恢復的呢?

處於Standby狀態的Master在接收到org.apache.spark.deploy.master.ZooKeeperLeaderElectionAgent發送的ElectedLeader消息後,就開始通過ZK中保存的Application,Driver和Worker的元數據信息進行故障恢復了,它的狀態也從RecoveryState.STANDBY變爲RecoveryState.RECOVERING了。當然了,如果沒有任何需要恢復的數據,Master的狀態就直接變爲RecoveryState.ALIVE,開始對外服務了。

一方面Master通過

beginRecovery(storedApps, storedDrivers, storedWorkers) 


恢復Application,Driver和Worker的狀態,一方面通過

recoveryCompletionTask = context.system.scheduler.scheduleOnce(WORKER_TIMEOUT millis, self,
          CompleteRecovery)


在60s後主動向自己發送CompleteRecovery的消息,開始恢復數據完成後的操作。

首先看一下如何通過ZooKeeperLeaderElectionAgent提供的接口恢復數據。

  override def readPersistedData(): (Seq[ApplicationInfo], Seq[DriverInfo], Seq[WorkerInfo]) = {
    val sortedFiles = zk.getChildren().forPath(WORKING_DIR).toList.sorted // 獲取所有的文件
    val appFiles = sortedFiles.filter(_.startsWith("app_")) //獲取Application的序列化文件
    val apps = appFiles.map(deserializeFromFile[ApplicationInfo]).flatten //將Application的元數據反序列化
    val driverFiles = sortedFiles.filter(_.startsWith("driver_")) //獲取Driver的序列化文件
    val drivers = driverFiles.map(deserializeFromFile[DriverInfo]).flatten //將Driver的元數據反序列化
    val workerFiles = sortedFiles.filter(_.startsWith("worker_")) // 獲取Worker的序列化文件
    val workers = workerFiles.map(deserializeFromFile[WorkerInfo]).flatten // 將Worker的元數據反序列化
    (apps, drivers, workers)
  }

獲取了原來的Master維護的Application,Driver和Worker的列表後,當前的Master通過beginRecovery來恢復它們的狀態。

恢復Application的步驟:

  1. 置待恢復的Application的狀態爲UNKNOWN,向AppClient發送MasterChanged的消息
  2. AppClient收到後改變其保存的Master的信息,包括URL和Master actor的信息,回覆MasterChangeAcknowledged(appId)
  3. Master收到後通過appId後將Application的狀態置爲WAITING
  4. 檢查如果所有的worker和Application的狀態都不是UNKNOWN,那麼恢復結束,調用completeRecovery()

恢復Worker的步驟:

  1. 重新註冊Worker(實際上是更新Master本地維護的數據結構),置狀態爲UNKNOWN
  2. 向Worker發送MasterChanged的消息
  3. Worker收到消息後,向Master回覆 消息WorkerSchedulerStateResponse,並通過該消息上報executor和driver的信息。
  4. Master收到消息後,會置該Worker的狀態爲ALIVE,並且會檢查該Worker上報的信息是否與自己從ZK中獲取的數據一致,包括executor和driver。一致的executor和driver將被恢復。對於Driver,其狀態被置爲RUNNING。
  5. 檢查如果所有的worker和Application的狀態都不是UNKNOWN,那麼恢復結束,調用completeRecovery()
beginRecovery的源碼實現:


  def beginRecovery(storedApps: Seq[ApplicationInfo], storedDrivers: Seq[DriverInfo],
      storedWorkers: Seq[WorkerInfo]) {
    for (app <- storedApps) { // 逐個恢復Application
      logInfo("Trying to recover app: " + app.id)
      try {
        registerApplication(app)
        app.state = ApplicationState.UNKNOWN
        app.driver ! MasterChanged(masterUrl, masterWebUiUrl) //向AppClient發送Master變化的消息,AppClient會回覆MasterChangeAcknowledged
      } catch {
        case e: Exception => logInfo("App " + app.id + " had exception on reconnect")
      }
    }

    for (driver <- storedDrivers) {
      // Here we just read in the list of drivers. Any drivers associated with now-lost workers
      // will be re-launched when we detect that the worker is missing.
      drivers += driver // 在Worker恢復後,Worker會主動上報運行其上的executors和drivers從而使得Master恢復executor和driver的信息。
    }

    for (worker <- storedWorkers) { //逐個恢復Worker
      logInfo("Trying to recover worker: " + worker.id)
      try {
        registerWorker(worker) //重新註冊Worker
        worker.state = WorkerState.UNKNOWN
        worker.actor ! MasterChanged(masterUrl, masterWebUiUrl) //向Worker發送Master變化的消息,Worker會回覆WorkerSchedulerStateResponse
      } catch {
        case e: Exception => logInfo("Worker " + worker.id + " had exception on reconnect")
      }
    }
  }

通過下面的流程圖可以更加清晰的理解這個過程:


如何判斷恢復是否結束?
在上面介紹Application和Worker的恢復時,提到了每次收到他們的迴應,都要檢查是否當前所有的Worker和Application的狀態都不爲UNKNOWN,如果是,那麼恢復結束,調用completeRecovery()。這個機制並不能完全起作用,如果有一個Worker恰好也是宕機了,那麼該Worker的狀態會一直是UNKNOWN,那麼會導致上述策略一直不會起作用。這時候第二個判斷恢復結束的標準就其作用了:超時機制,選擇是設定了60s得超時,在60s後,不管是否有Worker或者AppClient未返回相應,都會強制標記當前的恢復結束。對於那些狀態仍然是UNKNOWN的app和worker,Master會丟棄這些數據。具體實現如下:

  //調用時機
  // 1. 在恢復開始後的60s會被強制調用
  // 2. 在每次收到AppClient和Worker的消息回覆後會檢查如果Application和worker的狀態都不爲UNKNOWN,則調用
  def completeRecovery() {
    // Ensure "only-once" recovery semantics using a short synchronization period.
    synchronized {
      if (state != RecoveryState.RECOVERING) { return }
      state = RecoveryState.COMPLETING_RECOVERY
    }

    // Kill off any workers and apps that didn't respond to us. 刪除在60s內沒有迴應的app和worker
    workers.filter(_.state == WorkerState.UNKNOWN).foreach(removeWorker)
    apps.filter(_.state == ApplicationState.UNKNOWN).foreach(finishApplication)

    // Reschedule drivers which were not claimed by any workers
    drivers.filter(_.worker.isEmpty).foreach { d => // 如果driver的worker爲空,則relaunch。
      logWarning(s"Driver ${d.id} was not found after master recovery")
      if (d.desc.supervise) {
        logWarning(s"Re-launching ${d.id}")
        relaunchDriver(d)
      } else {
        removeDriver(d.id, DriverState.ERROR, None)
        logWarning(s"Did not re-launch ${d.id} because it was not supervised")
      }
    }

    state = RecoveryState.ALIVE
    schedule() 
    logInfo("Recovery complete - resuming operations!")
  }

但是對於一個擁有幾千個節點的集羣來說,60s設置的是否合理?畢竟現在沒有使用Standalone模式部署幾千個節點的吧?因此硬編碼60s看上去也十分合理,畢竟都是邏輯很簡單的調用,如果一些節點60S沒有返回,那麼下線這部分機器也是合理的。

通過設置spark.worker.timeout,可以自定義超時時間。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章