首先區分下AppMaster和Driver,任何一個yarn上運行的任務都必須有一個AppMaster,而任何一個Spark任務都會有一個Driver,Driver就是運行SparkContext(它會構建TaskScheduler和DAGScheduler)的進程,當然在Driver上你也可以做很多非Spark的事情,這些事情只會在Driver上面執行,而由SparkContext上牽引出來的代碼則會由DAGScheduler分析,並形成Job和Stage交由TaskScheduler,再由TaskScheduler交由各Executor分佈式執行。
所以Driver和AppMaster是兩個完全不同的東西,Driver是控制Spark計算和任務資源的,而AppMaster是控制yarn app運行和任務資源的,只不過在Spark on Yarn上,這兩者就出現了交叉,而在standalone模式下,資源則由Driver管理。在Spark on Yarn上,Driver會和AppMaster通信,資源的申請由AppMaster來完成,而任務的調度和執行則由Driver完成,Driver會直接跟Executor通信,讓其執行具體的任務。
client與cluster的區別
對於yarn-client和yarn-cluster的唯一區別在於,yarn-client的Driver運行在本地,而AppMaster運行在yarn的一個節點上,他們之間進行遠程通信,AppMaster只負責資源申請和釋放(當然還有DelegationToken的刷新),然後等待Driver的完成;而yarn-cluster的Driver則運行在AppMaster所在的container裏,Driver和AppMaster是同一個進程的兩個不同線程,它們之間也會進行通信,AppMaster同樣等待Driver的完成,從而釋放資源。
Spark裏AppMaster的實現:
org.apache.spark.deploy.yarn.ApplicationMaster
Yarn裏MapReduce的AppMaster實現:org.apache.hadoop.mapreduce.v2.app.MRAppMaster
在yarn-client模式裏,優先運行的是Driver(我們寫的應用代碼就是入口),然後在初始化SparkContext的時候,會作爲client端向yarn申請AppMaster資源,當AppMaster運行後,它會向yarn註冊自己並申請Executor資源,之後由本地Driver與其通信控制任務運行,而AppMaster則時刻監控Driver的運行情況,如果Driver完成或意外退出,AppMaster會釋放資源並註銷自己。所以在該模式下,如果運行spark-submit的程序退出了,整個任務也就退出了
在yarn-cluster模式裏,本地進程則僅僅只是一個client,它會優先向yarn申請AppMaster資源運行AppMaster,在運行AppMaster的時候通過反射啓動Driver(我們的應用代碼),在SparkContext初始化成功後,再向yarn註冊自己並申請Executor資源,此時Driver與AppMaster運行在同一個container裏,是兩個不同的線程,當Driver運行完畢,AppMaster會釋放資源並註銷自己。所以在該模式下,本地進程僅僅是一個client,如果結束了該進程,整個Spark任務也不會退出,因爲Driver是在遠程運行的
下面從源碼的角度看看SparkSubmit的代碼調用(基於Spark2.0.0):
代碼公共部分
SparkSubmit#main =>
val appArgs = new SparkSubmitArguments(args)
appArgs.action match {
// normal spark-submit
case SparkSubmitAction.SUBMIT => submit(appArgs)
// use --kill specified
case SparkSubmitAction.KILL => kill(appArgs)
// use --status specified
case SparkSubmitAction.REQUEST_STATUS => requestStatus(appArgs)
}
SparkSubmit的main方法是在用戶使用spark-submit
腳本提交Spark app的時候調用的,可以看到正常情況下,它會調用SparkSubmit#submit方法
SparkSubmit#submit =>
val (childArgs, childClasspath, sysProps, childMainClass) = prepareSubmitEnvironment(args)
// 此處省略掉代理賬戶,異常處理,提交失敗的重提交邏輯,只看主幹代碼
runMain(childArgs, childClasspath, sysProps, childMainClass, args.verbose)
在submit方法內部,會先進行提交環境相關的處理,調用的是SparkSubmit#prepareSubmitEnvironment方法,之後利用拿到的mainClass等信息,再調用SparkSubmit#runMain方法來執行對於主函數
SparkSubmit#prepareSubmitEnvironment =>
主幹相關的代碼如下:
// yarn client mode
if (deployMode == CLIENT) {
// client 模式下,運行的是 --class 後指定的mainClass,也即我們的代碼
childMainClass = args.mainClass
if (isUserJar(args.primaryResource)) {
childClasspath += args.primaryResource
}
if (args.jars != null) { childClasspath ++= args.jars.split(",") }
if (args.childArgs != null) { childArgs ++= args.childArgs }
}
// yarn cluster mode
val isYarnCluster = clusterManager == YARN && deployMode == CLUSTER
if (isYarnCluster) {
// cluster 模式下,運行的是Client類
childMainClass = "org.apache.spark.deploy.yarn.Client"
if (args.isPython) {
childArgs += ("--primary-py-file", args.primaryResource)
childArgs += ("--class", "org.apache.spark.deploy.PythonRunner")
} else if (args.isR) {
val mainFile = new Path(args.primaryResource).getName
childArgs += ("--primary-r-file", mainFile)
childArgs += ("--class", "org.apache.spark.deploy.RRunner")
} else {
if (args.primaryResource != SparkLauncher.NO_RESOURCE) {
childArgs += ("--jar", args.primaryResource)
}
// 這裏 --class 指定的是AppMaster裏啓動的Driver,也即我們的代碼
childArgs += ("--class", args.mainClass)
}
if (args.childArgs != null) {
args.childArgs.foreach { arg => childArgs += ("--arg", arg) }
}
}
在 prepareSubmitEnvironment 裏,主要負責解析用戶參數,設置環境變量env,處理python/R等依賴,然後針對不同的部署模式,匹配不同的運行主類,比如: yarn-client>args.mainClass
,yarn-cluster>o.a.s.deploy.yarn.Client
SparkSubmit#runMain =>
骨幹代碼如下
try {
mainClass = Utils.classForName(childMainClass)
} catch {
// ...
}
val mainMethod = mainClass.getMethod("main", new Array[String](0).getClass)
try {
// childArgs就是用戶自己傳給Spark應用代碼的參數
mainMethod.invoke(null, childArgs.toArray)
} catch {
// ...
}
在runMain方法裏,會設置ClassLoader,根據用戶代碼優先的設置(spark.driver.userClassPathFirst
)來加載對應的類,然後反射調用prepareSubmitEnvironment方法返回的主類,並調用其main方法
從所反射的不同主類,我們來看看具體調用方式的不同:
對於yarn-cluster
o.a.s.deploy.yarn.Client#main =>
val sparkConf = new SparkConf
val args = new ClientArguments(argStrings)
new Client(args, sparkConf).run()
在Client伴生對象裏構建了Client類的對象,然後調用了Client#run方法
o.a.s.deploy.yarn.Client#run =>
this.appId = submitApplication()
// report application ...
run方法核心的就是提交任務到yarn,其調用了Client#submitApplication方法,拿到提交完的appID後,監控app的狀態
o.a.s.deploy.yarn.Client#submitApplication =>
try {
// 獲取提交用戶的Credentials,用於後面獲取delegationToken
setupCredentials()
yarnClient.init(yarnConf)
yarnClient.start()
// Get a new application from our RM
val newApp = yarnClient.createApplication()
val newAppResponse = newApp.getNewApplicationResponse()
// 拿到appID
appId = newAppResponse.getApplicationId()
// 報告狀態
reportLauncherState(SparkAppHandle.State.SUBMITTED)
launcherBackend.setAppId(appId.toString)
// Verify whether the cluster has enough resources for our AM
verifyClusterResources(newAppResponse)
// 創建AppMaster運行的context,爲其準備運行環境,java options,以及需要運行的java命令,AppMaster通過該命令在yarn節點上啓動
val containerContext = createContainerLaunchContext(newAppResponse)
val appContext = createApplicationSubmissionContext(newApp, containerContext)
// Finally, submit and monitor the application
logInfo(s"Submitting application $appId to ResourceManager")
yarnClient.submitApplication(appContext)
appId
} catch {
case e: Throwable =>
if (appId != null) {
cleanupStagingDir(appId)
}
throw e
}
在 submitApplication 裏完成了app的申請,AppMaster context的創建,最後完成了任務的提交,對於cluster模式而言,任務提交後本地進程就只是一個client而已,Driver就運行在與AppMaster同一container裏,對於client模式而言,執行 submitApplication 方法時,Driver已經在本地運行,這一步就只是提交任務到yarn而已
o.a.s.deploy.yarn.Client#createContainerLaunchContext
createContainerLaunchContext方法的功能是創建AppMaster container context,在這裏就會指定AppMaster裏是否運行Driver,其主要代碼如下:
val appStagingDirPath = new Path(appStagingBaseDir, getAppStagingDir(appId))
// 非pySpark時,pySparkArchives爲Nil
val launchEnv = setupLaunchEnv(appStagingDirPath, pySparkArchives)
// 這一步會進行delegationtoken的獲取,存於Credentials,在AppMasterContainer構建完的最後將其存入到context裏
val localResources = prepareLocalResources(appStagingDirPath, pySparkArchives)
val amContainer = Records.newRecord(classOf[ContainerLaunchContext])
// 設置AppMaster container運行的資源和環境
amContainer.setLocalResources(localResources.asJava)
amContainer.setEnvironment(launchEnv.asJava)
// 設置JVM參數
val javaOpts = ListBuffer[String]()
javaOpts += "-Djava.io.tmpdir=" + tmpDir
// other java opts setting...
// 對於cluster模式,通過 --class 指定AppMaster運行我們的Driver端,對於client模式則純作爲資源申請和分配的工具
val userClass =
if (isClusterMode) {
Seq("--class", YarnSparkHadoopUtil.escapeForShell(args.userClass))
} else {
Nil
}
// 設置AppMaster運行的主類
val amClass =
if (isClusterMode) {
Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName
} else {
// ExecutorLauncher只是ApplicationMaster的一個warpper
Utils.classForName("org.apache.spark.deploy.yarn.ExecutorLauncher").getName
}
val amArgs =
Seq(amClass) ++ userClass ++ userJar ++ primaryPyFile ++ primaryRFile ++
userArgs ++ Seq(
"--properties-file", buildPath(YarnSparkHadoopUtil.expandEnvironment(Environment.PWD),
LOCALIZED_CONF_DIR, SPARK_CONF_FILE))
// Command for the ApplicationMaster
val commands = prefixEnv ++ Seq(
YarnSparkHadoopUtil.expandEnvironment(Environment.JAVA_HOME) + "/bin/java", "-server"
) ++
javaOpts ++ amArgs ++
Seq(
"1>", ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout",
"2>", ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr")
val printableCommands = commands.map(s => if (s == null) "null" else s).toList
// 設置需運行的命令
amContainer.setCommands(printableCommands.asJava)
val securityManager = new SecurityManager(sparkConf)
// 設置應用權限
amContainer.setApplicationACLs(
YarnSparkHadoopUtil.getApplicationAclsForYarn(securityManager).asJava)
// 設置delegationToken
setupSecurityToken(amContainer)
對於yarn-client
args.mainClass =>
在我們的Spark代碼裏,需要創建一個SparkContext來執行Spark任務,而在其構造器裏創建TaskScheduler的時候,對於client模式就會向yarn申請資源提交任務,如下
// 調用createTaskScheduler方法,對於yarn模式,master=="yarn"
val (sched, ts) = SparkContext.createTaskScheduler(this, master, deployMode)
_schedulerBackend = sched
_taskScheduler = ts
// 創建DAGScheduler
_dagScheduler = new DAGScheduler(this)
SparkContext#createTaskScheduler =>
這裏會根據master匹配不同模式,比如local/standalone/yarn,在yarn模式下會利用ServiceLoader裝載YarnClusterManager,然後由它創建TaskScheduler和SchedulerBackend,如下:
// 當爲yarn模式的時候
case masterUrl =>
// 利用當前loader裝載YarnClusterManager,masterUrl爲"yarn"
val cm = getClusterManager(masterUrl) match {
case Some(clusterMgr) => clusterMgr
case None => throw new SparkException("Could not parse Master URL: '" + master + "'")
}
try {
// 創建TaskScheduler,這裏masterUrl並沒有用到
val scheduler = cm.createTaskScheduler(sc, masterUrl)
// 創建SchedulerBackend,對於client模式,這一步會向yarn申請AppMaster,提交任務
val backend = cm.createSchedulerBackend(sc, masterUrl, scheduler)
cm.initialize(scheduler, backend)
(backend, scheduler)
} catch {
case se: SparkException => throw se
case NonFatal(e) =>
throw new SparkException("External scheduler cannot be instantiated", e)
}
YarnClusterManager#createSchedulerBackend
sc.deployMode match {
case "cluster" =>
new YarnClusterSchedulerBackend(scheduler.asInstanceOf[TaskSchedulerImpl], sc)
case "client" =>
new YarnClientSchedulerBackend(scheduler.asInstanceOf[TaskSchedulerImpl], sc)
case _ =>
throw new SparkException(s"Unknown deploy mode '${sc.deployMode}' for Yarn")
}
可以看到yarn下的SchedulerBackend實現對於client和cluster模式是不同的,yarn-client模式爲 YarnClientSchedulerBackend
,yarn-cluster模式爲 YarnClusterSchedulerBackend
,之所以不同,是因爲在client模式下,YarnClientSchedulerBackend 相當於 yarn application 的client,它會調用o.a.s.deploy.yarn.Client#submitApplication 來準備環境,申請資源並提交yarn任務,如下:
val driverHost = conf.get("spark.driver.host")
val driverPort = conf.get("spark.driver.port")
val hostport = driverHost + ":" + driverPort
sc.ui.foreach { ui => conf.set("spark.driver.appUIAddress", ui.appUIAddress) }
val argsArrayBuf = new ArrayBuffer[String]()
argsArrayBuf += ("--arg", hostport)
val args = new ClientArguments(argsArrayBuf.toArray)
totalExpectedExecutors = YarnSparkHadoopUtil.getInitialTargetExecutorNumber(conf)
// 創建o.a.s.deploy.yarn.Client對象
client = new Client(args, conf)
// 調用submitApplication準備環境,申請資源,提交任務,並把appID保存下來
// 對於submitApplication,前文有詳細的分析,這裏與前面是一致的
bindToYarn(client.submitApplication(), None)
而在 YarnClusterSchedulerBackend 裏,由於 AppMaster 已經運行起來了,所以它並不需要再做申請資源等等工作,只需要保存appID和attemptID並啓動SchedulerBackend即可
歡迎閱讀轉載,轉載請註明出處:https://my.oschina.net/kavn/blog/1540548