在實際工廠環境下使用的絕大多數的集羣管理器是Hadoop YARN,因此我們關注的重點是Hadoop YARN模式下的Spark集羣部署。
先熟悉一下Yarn集羣模式下spark部署流程,然後再跟蹤源碼進行分析。
- 任務提交後會和ResourceManager通訊申請啓動ApplicationMaster,隨後ResourceManager分配container;在合適的NodeManager上啓動ApplicationMaster;
- ApplicationMaster啓動之後會啓動一個Driver線程;
- Driver啓動後,ApplicationMaster會向ResourceManager註冊,並申請資源。
- ResourceManager接到ApplicationMaster的資源申請後會分配container;
- ApplicationMaster在合適的NodeManager上啓動Executor;
- Executor啓動後會向Driver反向註冊;
- 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的部署流程應該更加熟悉,中間涉及到哪些類,是怎樣相互調用的。
碼字不易,還請點波關注/贊