Spark源碼分析之AM端運行流程(Driver)

先驗知識

接之前文章 Spark源碼分析之任務提交流程 介紹了Client提交Spark任務的源碼分析過程。本文繼續分析ApplicationMaster的啓動流程(源碼Hadoop2.7.1),首先給出Client的提交的一些先決條件如下:
提交命令:

spark-submit --master yarn \
--deploy-mode cluster \
--class org.apache.spark.examples.SparkPi \
/usr/local/spark-2.4.3-bin-hadoop2.7/examples/jars/spark-examples_2.11-2.4.3.jar 100000

由之前文章可知,最終是由SparkSubmit類向Yarn集羣提交任務,首先會把任務依賴的文件上傳hdfs,然後生成Yarn提交上下文參數,通過RPC方式向Yarn的Resource Manager提交任務,其中提交上下文參數如下:
在這裏插入圖片描述
提交給HDFS的文件如下圖
在這裏插入圖片描述

Yarn啓動AM流程

SparkSubmit最終會調用Yarn Client通過RPC方式提交給Yarn的RM,提交給Yarn的RM後會首先申請一個Container(其實質就是ApplicationMaster),並與之NodeManager建立聯繫,NodeManager先進行資源本地化,然後在工作目錄下生成並調用 default_container_executor.sh -> default_container_executor_session.sh -> launch_container.sh 啓動JVM進程用於執行AM(Yarn集羣的調度原理見 Yarn源碼分析之集羣啓動流程Yarn源碼分析之事件模型Yarn源碼分析之狀態機機制 )。其中NodeManager進行資源本地化後的磁盤目錄如下圖(假設yarn配置的參數 yarn.nodemanager.local-dirs=/var/lib/hadoop-yarn/cache/yarn/):
在這裏插入圖片描述
於測試集羣是單節點的,所以上圖包含三個容器container_1587104773637_0002_01_000001container_1587104773637_0002_01_000002container_1587104773637_0002_01_000003,第一個容器用於運行Driver容器,後兩個容器用於運行Executor容器。NodeManager的工作目錄爲 cd /tmp/hadoop-root/nm-local-dir/usercache/root/appcache/application_1587104773637_0002/container_1587104773637_0002_01_000001
重點關注兩個文件launch_container.sh__spark_conf__.properties

  • launch_container.sh文件的主要作用爲設置AM啓動環境變量和NM通過sh啓動JVM運行AM;
  • __spark_conf__.properties則是AM啓動Driver和Executor的配置參數。

上圖給出文件供參考(由於jar文件過多過大,刪除了解壓到nm-local-dir/usercache/root/filecache/13/__spark_libs__3004195479466524435.zip目錄中的所有jar包)。

單獨列出launch_container.sh供參考,可以看出AM的入口類 org.apache.spark.deploy.yarn.ApplicationMaster
在這裏插入圖片描述

AM啓動Driver流程

由上節可知,NodeManager啓動AM的入口類是org.apache.spark.deploy.yarn.ApplicationMaster,其啓動過程會通過伴生類的main作爲入口,代碼分析直接見註釋,如下圖:
在這裏插入圖片描述
然後代碼繼續執行 ApplicationMaster.run() -> ApplicationMaster.runDriver(),最終在runDiver()做Driver的初始化工作,代碼分析見註釋如下:
在這裏插入圖片描述
如上圖,我們也重點分析兩部分:AM如何啓動Driver程序和AM主線程如何獲得子線程中SparkContext的初始化。Driver程序的啓動是在userClassThread = startUserApplication()函數完成的,如下圖,首先獲得提交參數中定義的--class,反射並實例化運行初始化SparkContext(即Driver),如下圖:
在這裏插入圖片描述
然後我們回到runDriver()看AM主線程如何獲得子線程中SparkContext的初始化,答案就是採用了Promise機制,下面截取關鍵處代碼:

private[spark] class ApplicationMaster(
  ...
  // In cluster mode, used to tell the AM when the user's SparkContext has been initialized.
  private val sparkContextPromise = Promise[SparkContext]()
  private def sparkContextInitialized(sc: SparkContext) = {
    sparkContextPromise.synchronized {
      // Notify runDriver function that SparkContext is available
      sparkContextPromise.success(sc)
      // Pause the user class thread in order to make proper initialization in runDriver function.
      sparkContextPromise.wait()
    }
  }

 private def startUserApplication(): Thread = {
    logInfo("Starting the user application in a separate Thread")
    ...
    val mainMethod = userClassLoader.loadClass(args.userClass)
      .getMethod("main", classOf[Array[String]])

    val userThread = new Thread {
      override def run(): Unit = {
        try {
          if (!Modifier.isStatic(mainMethod.getModifiers)) {
            ...
          } else {
            mainMethod.invoke(null, userArgs.toArray)
            ...
          }
        } catch {
          ...
            sparkContextPromise.tryFailure(e.getCause())
        } finally {
          sparkContextPromise.trySuccess(null)
        }
      }
    }
    userThread.setContextClassLoader(userClassLoader)
    userThread.setName("Driver")
    userThread.start()
    userThread
  }

  private def runDriver(): Unit = {
    //開闢線程,加載用戶定義--class函數,即Driver
    userClassThread = startUserApplication()
    ...
    try {
      //等待用戶定義Driver完成SparkContext的初始化完成
      val sc = ThreadUtils.awaitResult(sparkContextPromise.future,
        Duration(totalWaitTime, TimeUnit.MILLISECONDS))
    ...
  }

如上代碼,關鍵就是val sc = ThreadUtils.awaitResult(sparkContextPromise.future, Duration(totalWaitTime, TimeUnit.MILLISECONDS)),此函數會阻塞在超時時間內等待sparkContextPromise的Future對象返回SparkContext實例。其原理是先在ApplicationMaster類中定義了變量private val sparkContextPromise = Promise[SparkContext](),這樣在userClassThread = startUserApplication()中開闢的子線程中在SparkContext的初始化後會調用hook,進而通知主線程完成SparkContext的初始化並賦值返回,過程如下圖:
在這裏插入圖片描述

AM申請Executors流程

runDriver()函數中完成sparkContext的初始化後,緊接着就將AM註冊到RM,並根據driver的host、port和rpc server名稱YarnSchedulerBackend.ENDPOINT_NAME獲取到driver的EndpointRef對象driverRef,用於AM與Driver通信;同時傳遞driverRef給Executor用於executor與driver之間進行rpc通信,最後通過調用createAllocator(driverRef, userConf, rpcEnv, appAttemptId, distCacheConf),向RM申請Container資源,並啓動Executors,代碼如下:
在這裏插入圖片描述
下面給出打印上下文參數示例:
在這裏插入圖片描述
如果申請分配了多個executor容器,提交上下文參數類似,如果區別是--executor-id參數不同。
如圖可以看出向Yarn申請Executors與申請Driver容器的過程類似,區別在於Executor的入口類爲org.apache.spark.executor.CoarseGrainedExecutorBackend

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