[Spark內核]Yarn集羣模式部署流程源碼分析

個人博客地址

在實際工廠環境下使用的絕大多數的集羣管理器是Hadoop YARN,因此我們關注的重點是Hadoop YARN模式下的Spark集羣部署。

先熟悉一下Yarn集羣模式下spark部署流程,然後再跟蹤源碼進行分析。
在這裏插入圖片描述

  1. 任務提交後會和ResourceManager通訊申請啓動ApplicationMaster,隨後ResourceManager分配container;在合適的NodeManager上啓動ApplicationMaster
  2. ApplicationMaster啓動之後會啓動一個Driver線程
  3. Driver啓動後,ApplicationMaster會向ResourceManager註冊,並申請資源
  4. ResourceManager接到ApplicationMaster的資源申請後會分配container
  5. ApplicationMaster在合適的NodeManager上啓動Executor
  6. Executor啓動後會向Driver反向註冊
  7. Executor全部註冊完成後Driver開始執行main函數,之後執行到Action算子時,觸發一個job,並根據寬依賴開始劃分stage,每個stage生成對應的taskSet,之後將task分發到各個Executor上執行

熟悉了spark的部署流程後,源碼擼起來。
萬事開頭難,從哪開始呢?沒錯從我們提交spark應用程序開始。

bin/spark-submit
–class org.apache.spark.examples.SparkPi
–num-executors 2
–master yarn
–deploy-mode cluster
./examples/jars/spark-examples_2.11-2.1.1.jar
100

通過提交這個spark-submit一提交我們的程序就啓動起來了,所以從spark-submit開刀。

vim spark-submit

查看腳本,我們可以發現其實裏面其實就是通過java命令運行了一個java程序;

// java命令運行這個類
spark-submit-> /bin/java org.apache.spark.deploy.SparkSubmit

很顯然運行了org.apache.spark.deploy.SparkSubmit這個類,那麼我們只需要去找類的main方法,接着就開始了源碼之旅。

整個源碼我就不沾過來了,只看核心部分,寫的是僞代碼。

org.apache.spark.deploy.SparkSubmit
main{
    // main方法裏面掉了submit方法
	submit(){
		// 解析腳本參數
		prepareSubmitEnvironment(args){
        
			isYarnCluster -> 
            // yarn集羣模式的話運行的是這個類
				childMainClass = "org.apache.spark.deploy.yarn.Client"
                
			Client ->   // 即驗證了client模式,Driver直接在客戶端上運行
				childMainClass = args.mainClass	// 即用戶程序的主類
			
		} 
		
		doRunMain(){
			runMain(){
				//var mainClass: Class[_]; 通過反射拿到childMainClass的Class對象
				mainClass = Utils.classForName(childMainClass)
		
				// 獲取Class對像的main方法
				val mainMethod = mainClass.getMethod("main", new Array[String](0).getClass)
		
				// 調用運行main
				mainMethod.invoke(null, childArgs.toArray)
			}
		}	
	}
}

看到這裏我們可以知道,submint方法裏面運行了org.apache.spark.deploy.yarn.Client這個類的main方法。所以我們接下來就去找這個類的main方法

如果沒找到的話,在pom文件中加入這個依賴。

<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-yarn_2.11</artifactId>
    <version>2.1.1</version>
</dependency>
org.apache.spark.deploy.yarn.Client
main(){
	
	new Client(args, sparkConf).run()
	
	// 提交一個application到ResourceManager
	run(){
        // 提交application返回appid
		this.appId = submitApplication()
		{
			//LauncherBackend 是跟LauncherServer通信的客戶端,向LauncherServer發送狀態變化的通信端點
			launcherBackend.connect()	// 連接RM
			
			// Get a new application from our RM
			val newApp = yarnClient.createApplication()	// 讓RM創建一個application
			val newAppResponse = newApp.getNewApplicationResponse() // 獲取新application的響應
			appId = newAppResponse.getApplicationId()	// 獲取appid
			
			// 創建一個amContainer容器
			val containerContext = createContainerLaunchContext(newAppResponse)
			{
				// 設置java虛擬機的運行參數
				
				// 獲取am運行的主類(Cluster模式)
				val amClass = Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName
				
				// 封裝指令 command = bin/java amClass ...
			}
			// 主要進行sparkConf的配置
			val appContext = createApplicationSubmissionContext(newApp, containerContext)
			
			// 向Yarn提交應用,實際上提交的是指令
			yarnClient.submitApplication(appContext)
		}
	}
}

提交application實際上就是提交封裝的一個command,裏面就是一個啓動java進程的一個命令,啓動的類是
org.apache.spark.deploy.yarn.ApplicationMaster,所以我們就去找這個類的main方法

去找這個mian方法之前我們需要明白,提交application之後,ResourceManager會將application打包成一個任務放入任務隊列中,NodeManger就會來領取任務,運行ApplicationMaster,即下面這個類。

org.apache.spark.deploy.yarn.ApplicationMaster
main(){
	// 封裝用戶提交的參數,即 java --jar ... --class ...等參數
	val amArgs = new ApplicationMasterArguments(args)
	
	// 創建ApplicationMaster,同時傳入用戶參數和與RM交互的客戶端
	master = new ApplicationMaster(amArgs, new YarnRMClient)
	
	// 啓動創建ApplicationMaster
	master.run(){
		// isClusterMode 運行Driver
		runDriver(securityMgr)
		{
			// 啓動用戶的應用
			userClassThread = startUserApplication()
			{
				// 獲取用戶的參數
				var userArgs = args.userArgs
				// 獲取用戶程序的主方法	,args.userClass即拿到了用戶主類路徑
				val mainMethod = userClassLoader.loadClass(args.userClass).getMethod("main", classOf[Array[String]])
				
				// 創建一個Driver線程執行用戶程序的main方法
				val userThread = new Thread {
					run(){
						mainMethod.invoke(null, userArgs.toArray)
					}
				}
				userThread.setName("Driver")
				userThread.start()
			}
			
			// 向RM註冊AM
			registerAM(sc.getConf, rpcEnv, driverRef, sc.ui.map(_.appUIAddress).getOrElse("")
			{
				// client是YarnRMClient, 向RM註冊自己並獲取資源
				allocator = client.register(driverUrl,driverRef,yarnConf,_sparkConf,uiAddress,historyAddress,securityMgr,localResources)
			
				// 分配的資源
				allocator.allocateResources()
				{
					// 處理可分配的資源
					handleAllocatedContainers(allocatedContainers.asScala)
					{	
						// 這裏會涉及到本地化策略,如進程本地化、節點本地化、機架本地化...
						
						// 運行可分配的Container
						runAllocatedContainers(containersToUse)
						{
							// 遍歷containers
							for (container <- containersToUse) {
							
								// 正在運行的Executor的數量還要小於總共需要的Executors的數量,則繼續創建運行
								if (numExecutorsRunning < targetNumExecutors) {
								
									// launcherPool線程池來執行
									launcherPool.execute(new Runnable {
										run(){
										
											new ExecutorRunnable(Some(container),conf,sparkConf,driverUrl,executorId...).run()
											{
												// 創建與NodeManager交互的客戶端並啓動
												nmClient = NMClient.createNMClient()
												nmClient.init(conf)
												nmClient.start()
												
												// 啓動容器
												startContainer()
												{
													
													val ctx = Records.newRecord(classOf[ContainerLaunchContext]).asInstanceOf[ContainerLaunchContext]
													
													// 封裝命令,commands = /bin/java org.apache.spark.executor.CoarseGrainedExecutorBackend ...
													val commands = prepareCommand()
													ctx.setCommands(commands.asJava)
													
													// 讓客戶端去對應的NM上啓動容器
													nmClient.startContainer(container.get, ctx)
												}
											}
										}
									}
								}
							}
						}
					}
					
				}
			}
			
			// Driver線程沒執行完,當前線程不能繼續往下執行,也就是說ApplicationMaster不能結束
			userClassThread.join()
		}
	}
}

ApplicationMaster會通過NodeManager的客戶端和NodeManager通信啓動容器,同時將commands發送過去,在commands裏面要運行的類是org.apache.spark.executor.CoarseGrainedExecutorBackend,所以接下來就要去找這個類的main方法。

org.apache.spark.executor.CoarseGrainedExecutorBackend
main(){
	
	run(driverUrl, executorId, hostname, cores, appId, workerUrl, userClassPath)
	{
		// 創建一個Executor後臺程序
		env.rpcEnv.setupEndpoint("Executor", new CoarseGrainedExecutorBackend(env.rpcEnv, driverUrl, executorId, hostname, cores, userClassPath, env))
	}
}

這個mian方法很簡單就是創建了CoarseGrainedExecutorBackend對象,很顯然這個對象就是Executor後臺程序,所以接下來就來看看這個對象裏面在幹嘛。

private[spark] class CoarseGrainedExecutorBackend() extends ThreadSafeRpcEndpoint{
	// 由於該類繼承了Rpc端點,所以該對象的生命週期是 constructor(創建) -> onStart(啓動) -> receive*(接收消息) -> onStop(停止)

	// 我們所說的Executor就是CoarseGrainedExecutorBackend中的一個屬性對象
	var executor: Executor = null
	
	override def onStart() {
		//向Driver反向註冊
		driver = Some(ref)
		ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls))
	}
	
	override def receive: PartialFunction[Any, Unit] = {
		// 收到Driver註冊成功的消息
		case RegisteredExecutor =>
			// 創建計算對象Executor
			executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false)
		
		// 收到Driver端發送過來的task
		case LaunchTask(data) =>
			// 由executor對象調用方法運行
			executor.launchTask(this, taskId = taskDesc.taskId, attemptNumber = taskDesc.attemptNumber,taskDesc.name, taskDesc.serializedTask)
	}
}

通過對Yarn集羣模式源碼解讀,應該對這個spark的部署流程應該更加熟悉,中間涉及到哪些類,是怎樣相互調用的。

碼字不易,還請點波關注/贊

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