YARN 模式运行机制

Yarn Cluster 模式

  1. 执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;
  2. SparkSubmit 类中的 main 方法反射调用 YarnClusterApplication 的 main 方法;
  3. YarnClusterApplication 创建 Yarn 客户端,然后向 Yarn 服务器发送执行指令:bin/java ApplicationMaster
  4. NM
  5. ApplicationMaster 启动 Driver 线程,执行用户的作业;
  6. AMRM 注册,申请资源;
  7. 获取资源后 AMNM 发送指令:bin/java YarnCoarseGrainedExecutorBackend
  8. 计算对象 Executor
  9. Driver 线程继续执行完成作业的调度和任务的执行。
  10. Driver 分配任务并监控任务的执行。

注意

SparkSubmit、ApplicationMaster 和 CoarseGrainedExecutorBackend 是独立的进程

Driver 是独立的线程;Executor 和 YarnClusterApplication 是对象




怎么从Mr切换spark引擎 spark启动模式_怎么从Mr切换spark引擎


Yarn Client 模式

  1. 执行脚本提交任务,实际是启动一个 SparkSubmit 的 JVM 进程;
  2. SparkSubmit 类中的 main 方法反射调用用户代码的 main 方法;
  3. 启动 Driver 线程,执行用户的作业,并创建 SchedulerBackend;
  4. YarnClientSchedulerBackend 向 RM 发送指令:bin/java ExecutorLauncher
  5. Yarn 框架收到指令后会在指定的 NM 中启动 ExecutorLauncher(实际上还是调用 ApplicationMaster 的 main 方法);
object ExecutorLauncher {
 def main(args: Array[String]): Unit = {
  ApplicationMaster.main(args)
 }
}


  1. AM 向 RM 注册,申请资源;
  2. 获取资源后 AM 向 NM 发送指令:bin/java CoarseGrainedExecutorBackend
  3. 启动计算对象 Executor
  4. Driver 分配任务并监控任务的执行。

注意

SparkSubmit、ApplicationMaster 和 YarnCoarseGrainedExecutorBackend 是独立的进程;

Executor 和 Driver 是对象。

源码

SparkSubmit 阶段

执行 Spark 提交命令:


bin/spark-submit 
--class com.atguigu.spark.WordCount 
--master yarn 
WordCount.jar 
/input 
/output


底层执行的是下面命令, 开启 SparkSubmit 进程:


bin/java org.apache.spark.deploy.SparkSubmit + "$@"


以下代码在 SparkSubmit 中, 从 main() 开始, 这一部分主要是根据运行模式先获取之后要反射调用的类名赋给元组中的 ChildMainClass. 如果是Yarn Cluster 模式, 则为 YarnClusterApplication; Yarn Client 模式为用户写的主类:


// 85 解析参数, 提交程序, 经过一些过度提交方法
submit.doSubmit(args)
// 90
case SparkSubmitAction.SUBMIT => submit(appArgs, uninitLog)
// 158
doRunMain()
// 870
runMain(args, uninitLog)
// 871 准备提交环境
val (childArgs, childClasspath, sparkConf, childMainClass) = prepareSubmitEnvironment(args){
    // 626 如果是 Yarn Client 模式
    if (deployMode == CLIENT) {
      childMainClass = args.mainClass (用户所写的主类 WordCount)
    }
    // 714 Yarn Cluster 模式
    if (isYarnCluster) {
      childMainClass = YARN_CLUSTER_SUBMIT_CLASS
      "org.apache.spark.deploy.yarn.YarnClusterApplication"
    }
    // 727 这样用户写的主类会作为参数封装到 childArgs 中
    childArgs += ("--class", args.mainClass)
}


接下来是获取到 ChildMainClass 之后反射调用 main 方法的过程: 反射获取类然后通过构造器获取一个示例并多态为 SparkApplication, 再调用它的 start 方法:


// 893
mainClass = Utils.classForName(childMainClass)
// 911
val app: SparkApplication = mainClass.getConstructor().newInstance().asInstanceOf[SparkApplication]
// 928
app.start(childArgs.toArray, sparkConf)


接着调用 YarnClusterApplication 的 start 方法.

new 一个 Client 对象, 里面有 yarnClient = YarnClient.createYarnClient 属性, 这就是 Yarn 在 SparkSubmit 中的一个客户端了, yarnClient 具体在下面 171 行初始化和开始, 即连接 Yarn 集群或 RM.

之后就可以通过这个客户端与 Yarn 的 RM 进行通信和提交应用, 即调用 run 方法. run 方法的源码注释如下, 印证了这一点:

Submit an application to the ResourceManager.

而主要过程是 1177 行的提交应用并返回一个 appId, 这个方法是提交一个将 ApplicationMaster 的应用 Application 给 RM:

Submit an application running our ApplicationMaster to the ResourceManager.

之后初始化 yarnClient 客户端, 从 RM 注册返回一个 App 并获取 appId (因为要返回), 然后验证集群是否有足够的资源来启动将运行 AM 的应用.


// 1583 
new Client(new ClientArguments(args), conf, null).run()
// 1177 
this.appId = submitApplication()
// 171
yarnClient.init(hadoopConf)
yarnClient.start()
// 178 YarnClient 从 RM 注册一个 App 并获取 AppId
val newApp = yarnClient.createApplication()
val newAppResponse = newApp.getNewApplicationResponse()
appId = newAppResponse.getApplicationId()

// 193 Verify whether the cluster has enough resources for our AM
verifyClusterResources(newAppResponse)


之后分别创建 Container 启动环境来启动运行 AM 的 Container (即创建 Container), 再创建应用提交环境以提交应用:


// 196 Set up the appropriate contexts to launch our AM
val containerContext = createContainerLaunchContext(newAppResponse)
val appContext = createApplicationSubmissionContext(newApp, containerContext)


创建 Container 启动环境, 最后获得 AM 的容器, 之后再封装成 appContext:


// java 进程启动命令怎么来的:
// 882
val javaOpts = ListBuffer[String]()
// 888 给参数中添加最大内存, 之后还有临时目录等略
javaOpts += "-Xmx" + amMemory + "m"
// 954 userClass 如果是集群模式就是 WordCount, 客户端模式不需要了, 因为在 SparkSubmit 中已经反射调动 main 启动了
val userClass =
      if (isClusterMode) {
        Seq("--class", YarnSparkHadoopUtil.escapeForShell(args.userClass))
      } else {
        Nil
      }

// 978 
// 然后加入 amClass, cluster 模式起的类叫 ApplicationMaster, client 模式起的类叫 ExecutorLauncher
val amClass =
if (isClusterMode) {
Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName
} else {
Utils.classForName("org.apache.spark.deploy.yarn.ExecutorLauncher").getName
}

/*
那么这里相当于这两个参数:
"-Xmx" + amMemory + "m"
--class WordCount // 这只是个参数
org.apache.spark.deploy.yarn.ApplicationMaster // 这是主类 

最后拼接 AM 进程的启动命令:

commond = JAVA_HOME/bin/java 
    "-Xmx" + amMemory + "m"
    -server org.apache.spark.deploy.yarn.ApplicationMaster
    --class WordCount --jars XX.jar --args userArgs
*/
val commands = prefixEnv ++
    Seq(Environment.JAVA_HOME.$$() + "/bin/java", "-server") ++
    javaOpts ++ amArgs ++
    Seq(
        "1>", ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stdout",
        "2>", ApplicationConstants.LOG_DIR_EXPANSION_VAR + "/stderr")

// 1008 将启动命令放入 amContainer 容器
amContainer.setCommands(printableCommands.asJava)
// 1030 最后返回容器
amContainer
// 197 封装
val appContext = createApplicationSubmissionContext(newApp, containerContext)


最后呢, yarnClinet 将封装的 appContext 提交交给 RM 在 Yarn 上的一个 NM 中启动这样一个运行 AM 的应用, 或者说启动一个叫 ApplicationMaster 的进程, 最后再把 appId 返回, 其实到这里 SparkSubmit 就结束了:


yarnClient.submitApplication(appContext)


之后就是抓一些异常, 比如出错了怎么处理, 但一般到这里可以认为结束了.

ApplicationMaster 阶段

org.apache.spark.deploy.yarn.ApplicationMaster 的 main

总的来讲, ApplicationMaster 阶段首先开启一个 Driver 新线程, 第二是 AM 向 RM 注册, 第三是 AM 向 RM 申请 Container 资源并处理, 封装 ExecutorBackend 启动命令, 最后 AM 向 NM 通信提交命令由 NM 启动 ExecutorBackend.


// 859 1. 创建自身对象, 调用 run() 方法, 并 runDriver()
master = new ApplicationMaster(amArgs, sparkConf, yarnConf)
master.run() // 890
runDriver() // 490

// 2. runDriver() 里面主要做以下几件事:

// 2.1 启动一个叫做 Driver(用户所写的主类) 的新线程 492
userClassThread = startUserApplication()

// 2.2 通过 YarnRMClient 对象向 RM 注册 AM 507
registerAM(host, port, userConf, sc.ui.map(_.webUrl), appAttemptId)

// 2.3 申请资源
createAllocator(driverRef, userConf, rpcEnv, appAttemptId, distCacheConf)


具体而言 2.1 在 ApplicationMaster 进程中 开启 Driver 线程. 这样我们自己写的代码就开始运行了, 就会开始创建 Spark 程序的入口 SparkContex, 之后创建 RDD, 由行动算子生成 job, 划分阶段提交 Task 等等:


// 718, 758, 759
val mainMethod = userClassLoader.loadClass(args.userClass)
    .getMethod("main", classOf[Array[String]])
userThread.setName("Driver")
userThread.start()


2.2 注册 AM:


// 99
private val client = new YarnRMClient()
// 428
client.register(host, port...)


2.3 申请资源 createAllocator( ) 即 AM 主线程向 RM 申请启动 ExecutorBackend.

2.3.1 在申请资源之前, AM 主线程创建了 Driver 的终端引用, 然后 createAllocator 时作为参数传入, 因为 Executor 启动后是要向 Driver 反向注册的, 所以启动过程必须封装 Driver 的 EndpointRef.

2.3.2 向 RM 申请获取可用资源 Container, 并处理这些资源:


// 2.3.1 509 
val driverRef = rpcEnv.setupEndpointRef(
          RpcAddress(host, port),
          YarnSchedulerBackend.ENDPOINT_NAME)
createAllocator(driverRef, userConf, rpcEnv, appAttemptId, distCacheConf)

// 2.3.2 465 479 创建资源分配者
allocator = client.createAllocator(...)
/*
2.3.2.1 Request resources such that, if YARN gives us all we ask for, we'll have a number of containers equal to maxExecutors(提交时配置的参数).

2.3.2.2  Deal with any containers YARN has granted to us by possibly launching executors in them.
*/
allocator.allocateResources(){
    // 2.3.2.1 向 RM 请求申请资源 260
    val allocateResponse = amClient.allocate(progressIndicator) 
    // 获取所有的可用资源 (Containers) 262
    val allocatedContainers = allocateResponse.getAllocatedContainers()
    // 2.3.2.2 处理这些从 Yarn 分配到的s)(Containers) 274
    handleAllocatedContainers(allocatedContainers.asScala)
}

// 2.3.2.2 
// 首先对 Containers 进行封装, 包括其节点 host, rack 信息等, 封装为 containersToUse, 然后启动containersToUse, 这一部即启动 ExecutorBackend 进程.
runAllocatedContainers(containersToUse)

// 启动 Container 558
// 首先开启一个线程, 专门用来启动 ExecutorBackend 进程. 因为 AM 要和 NM 去通信, 所以这里面有 NM 的客户端, 最后调用 startContainer() 用这个客户端提交命令使 NM 启动 ExecutorBackend 进程.
new ExecutorRunnable().run{
    nmClient.init(conf) // 66
    nmClient.start()
    startContainer()
}
// startContainer 方法功能如下: 
// 2.3.2.2.1 封装启动 Container 的命令
val commands = prepareCommand(){
    JAVA_HOME/bin/java -server
    "-Xmx" + executorMemoryString
    org.apache.spark.executor.YarnCoarseGrainedExecutorBackend
}

// 2.3.2.2.2 将命令进行包装
ctx.setCommands(commands.asJava)

// 2.3.2.2.3 AM 向 NM 提交申请, 启动 YarnCoarseGrainedExecutorBackend 125
// Send the start request to the ContainerManager
nmClient.startContainer(container.get, ctx)


ExecutorBackend

未完待续 ...