Yarn源碼剖析(二) --- spark-submit

spark-submit

前言

上文Yarn源碼剖析(一) --- RM與NM服務啓動以及心跳通信介紹了yarn是如何啓動的,本文將介紹在yarn正常啓動後,任務是如何通過spark-submit提交到yarn上的。

spark-submit腳本

1. 先來觀察一下任務提交時的spark-submit腳本中各個參數的含義(並沒列舉所有,只列舉了關鍵的幾個參數)

/spark/bin/spark-submit \
--master yarn \ //提交模式,顯而易見我們是提交到yarn上
--deploy-mode cluster \ //運行的模式,還有一種client模式,但大多用於調試,此處使用cluster模式
--class org.apache.spark.test \ //提交的任務
--name "test" \ //任務名字
--queue root.default \ //提交的隊列
--driver-memory 3g \ //爲driver申請的內存
--num-executors 1 \ //executors的數量,可以理解爲線程數,對應yarn中的Container個數
--executor-memory 6g \ //爲每一個executor申請的內存
--executor-cores 4 \ //爲每一個executor申請的core
--conf spark.yarn.driver.memoryOverhead=1g \ //driver可使用的非堆內存,這些內存用於如VM,字符 串常量池以及其他額外本地開銷等
--conf spark.yarn.executor.memoryOverhead=2g \ //每個executor可使用的非堆內存,這些內存用於如 VM,字符串常量池以及其他額外本地開銷等

2. 用戶按照自己的需求提交了該腳本,然後進入到任務提交階段,實現類是org.apache.spark.deploy.SparkSubmit:

從main方法中可以看到這段代碼,如果是SUBMIT,則調用submit 
調用順序:main--> submit(appArgs)->runMain(childArgs, childClasspath,sysProps,hildMainClass, args.verbose)

 appArgs.action match {
 case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog)
 case SparkSubmitAction.KILL => kill(appArgs)
 case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs)
 }

3. 在runMain方法中會先進入prepareSubmitEnvironment方法,在該方法中得知,如果運行的模式是yarn則會使用org.apache.spark.deploy.yarn.YarnClusterApplication類作爲提交任務到yarn程序的入口。其實prepareSubmitEnvironment方法中就是設置參數、準備submit環境之類的事情,代碼量較大,得知在這個方法中得知了相關yarn的啓動類後就不再多做分析了。


4. YarnClusterApplication這是org.apache.spark.deploy.yarn.Client中的一個內部類,在YarnClusterApplication中new了一個Client對象,並調用了run方法

private[spark] class YarnClusterApplication extends SparkApplication {
    override def start(args: Array[String], conf: SparkConf): Unit = {
     //SparkSubmit在yarn模式下會使用yarn緩存去處理文件和jar包,因此在這裏將它們從sparkConf中移除
     conf.remove("spark.jars")
     conf.remove("spark.files")
     new Client(new ClientArguments(args), conf).run()
    }
}

5. 在run方法中會調用submitApplication方法,此方法則是實現向yarn中的ResourceManager(後文全部簡稱RM),提交運行任務,並且運行我們的ApplicationMaster(後文簡稱AM),在該方法中初始化並啓動了yarnClient用以使用yarn提供的各種API,而在submitApplication內部有兩個關鍵的方法,最終調用yarnClient.submitApplication(appContext)向yarn提交任務啓動的請求。

 // 關鍵是這兩個方法:
 // 1. 創建ApplicationMaster ContainerLaunch上下文,
 // 將ContainerLaunch命令、jar包、java變量等環境準備完畢;
 val containerContext = createContainerLaunchContext(newAppResponse)

 // 2. 創建Application提交至YARN的上下文,主
 // 要讀取配置文件設置調用YARN接口前的上下文變量。
 val appContext = createApplicationSubmissionContext(newApp, containerContext)

 // Finally, submit and monitor the application
 logInfo(s"Submitting application $appId to ResourceManager")
 yarnClient.submitApplication(appContext) //提交應用程序
 launcherBackend.setAppId(appId.toString)
 reportLauncherState(SparkAppHandle.State.SUBMITTED)

6. 在createContainerLaunchContext內部初始化了ApplicationMaster所需的資源,環境變量等,所以不做贅述,我們直接來看ApplicationMaster的main()方法,內部new了一個AM對象以及執行run方法,並且在run方法中根據任務選擇的模式再選擇對應的方法,我們的任務是Cluster模式所以我們進入runDriver方法。

def main(args: Array[String]): Unit = {
    SignalUtils.registerLogger(log)
    val amArgs = new ApplicationMasterArguments(args)
    master = new ApplicationMaster(amArgs)
    System.exit(master.run())
} 

if (isClusterMode) {
    runDriver() //yarn-cluster
 } else {
 runExecutorLauncher() //yarn-client
 }

7. 在runDriver方法中執行了一個registerAM方法(),該方法中有兩個重要的方法

allocator = client.register(driverUrl, driverRef, yarnConf, _sparkConf, uiAddress,
historyAddress, securityMgr, localResources) //向RM註冊AM

rpcEnv.setupEndpoint("YarnAM", new AMEndpoint(rpcEnv, driverRef))

allocator.allocateResources() //爲executor分配資源

8. 先看第一個方法client.register,該方法向RM註冊AM(是調用yarn中AMRMClient的API實現的,具體的實現我會在後續的章節中介紹),並申請供AM使用的Container

 amClient = AMRMClient.createAMRMClient()
 amClient.init(conf)
 amClient.start()
 this.uiHistoryAddress = uiHistoryAddress

 val trackingUrl = uiAddress.getOrElse {
 if (sparkConf.get(ALLOW_HISTORY_SERVER_TRACKING_URL)) uiHistoryAddress else ""
 }

 logInfo("Registering the ApplicationMaster")
 synchronized {
 amClient.registerApplicationMaster(Utils.localHostName(), 0, trackingUrl)
 registered = true
 }

9. 再看第二個方法allocator.allocateResources(),該方法拿到了yarn返回給AM的Container集合,然後去處理這些集合使得executor可以在這些Container中啓動

updateResourceRequests() //更新同步yarn資源信息

val progressIndicator = 0.1f
//調查ResourceManager。如果沒有掛起的容器請求,這將充當心跳.
//此方法就是去yarn查看所有節點可用的資源信息
val allocateResponse = amClient.allocate(progressIndicator)
//獲得yarn分配回來的Container
val allocatedContainers = allocateResponse.getAllocatedContainers()

if (allocatedContainers.size > 0) {
 logDebug(("Allocated containers: %d. Current executor count: %d. " +
 "Launching executor count: %d. Cluster resources: %s.")
 .format(
 allocatedContainers.size,
 numExecutorsRunning.get,
 numExecutorsStarting.get,
 allocateResponse.getAvailableResources))
 //處理獲取的Container
 handleAllocatedContainers(allocatedContainers.asScala)
}

10. 在這個handleAllocatedContainers方法中,根據一些傳入的配置對Container進行了一些設置,然後調用該方法中的runAllocatedContainers(containersToUse)去啓動executor,最終真正執行啓動Container的是在ExecutorRunnable.run()中。創建了NMClient客戶端調用提供的API最終實現在NM上啓動Container,具體如何啓動Container將在後文中進行介紹。

def run(): Unit = {
 logDebug("Starting Executor Container")
 nmClient = NMClient.createNMClient()
 nmClient.init(conf)
 nmClient.start()
 startContainer()
}

總結

本文通過對spark-submit源碼的剖析試圖介紹用戶運行任務的整個過程。其實本文中的AM是spark自己封裝的AM,真正的AM啓動還是在hadoop yarn的代碼中,在後文我將會介紹在yarn中AM的啓動,而RM、NodeManager、AM之間又是怎麼交互的,以及整個資源調度的流程,最終是怎麼返回Container給spark以啓動executor的。

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