從源碼角度看Spark on yarn client & cluster模式的本質區別 原 薦

首先區分下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

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