先驗知識
接之前文章 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_000001
、container_1587104773637_0002_01_000002
和container_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
。