Spark 源码 | Yarn Client 模式提交流程(五)

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站:https://www.captainai.net/dongkelun

前言

在第四篇文章 SparkContext 初始化与 TaskScheduler 创建流程(四) 中,我们分析了 SparkContext 初始化和 TaskScheduler 创建流程。本文详细介绍 Yarn Client 模式的提交流程。

版本

Spark 3.2.3

Yarn Client 模式概述

Yarn Client 模式下,Driver 运行在提交应用的客户端机器上,直接与 YARN ResourceManager 通信申请资源,Executor 运行在 YARN 的 NodeManager 节点上。

什么是 Driver?Driver 为什么运行在客户端?

在 Spark 中,Driver 并不对应某个单独的类或对象,而是一个运行时角色 ------用户主类的 main 方法在哪里执行,Driver 就在哪里(用户主类中会创建 SparkContext/SparkSession,驱动整个应用的执行)。

第二篇文章 SparkSubmit 提交流程分析(二) 中,prepareSubmitEnvironment 方法根据 deployMode 设置 childMainClass,决定了 main 方法在何处执行:

模式 childMainClass main 方法执行位置 Driver 位置
Yarn Client args.mainClass(用户主类) runMain 在客户端反射调用 客户端
Yarn Cluster YarnClusterApplication runDriver 在 AM 内调用 集群

总结 :Yarn Client 模式 childMainClass = args.mainClass,本地反射执行用户主类的 main 方法,Driver 在客户端;Yarn Cluster 模式 childMainClass = YarnClusterApplication,由 ApplicationMaster 通过 runDriver() 启动用户主类,Driver 在集群中。

关于 Driver 及各模式下角色的详细总结,将在系列文章完成后单独分析。

scala 复制代码
// 提交命令示例
spark-submit --master yarn --deploy-mode client --class org.apache.spark.examples.SparkPi $SPARK_HOME/examples/jars/spark-examples_2.12-3.2.3.jar

提交流程

在第二篇文章 Spark 源码 | SparkSubmit 提交流程分析(二) 中已经详细分析了 SparkSubmit 的提交流程,包括:

  • doSubmit 方法:参数解析和 action 匹配
  • submit 方法:代理用户处理和 runMain 调用
  • prepareSubmitEnvironment 方法:设置 childMainClass 和 classpath
  • runMain 方法:反射调用用户主类

本文重点分析 Yarn Client 模式下的完整提交流程,包括 SparkSubmit 到 Driver 初始化、Client 与 RM 通信、AM 启动、Executor 分配与注册。

1. runMain 执行用户主类

在第二篇文章中已经分析了 runMain 方法,它会:

  1. 调用 prepareSubmitEnvironment 获取 childMainClass
  2. 通过反射加载主类
  3. 如果主类实现了 SparkApplication 接口,直接实例化;否则包装成 JavaMainApplication
  4. 调用 app.start 启动应用

对于 Yarn Client 模式,完整调用链如下:

复制代码
用户主类 SparkPi(没有实现 SparkApplication)
         ↓
包装成 JavaMainApplication(SparkPi),赋值给 app
         ↓
调用 app.start
         ↓
实际执行的是 JavaMainApplication.start
         ↓
内部通过反射调用 SparkPi.main

JavaMainApplication.start 方法:

scala 复制代码
class JavaMainApplication(mainClass: Class[_]) extends SparkApplication {
  override def start(args: Array[String], conf: SparkConf): Unit = {
    // 通过反射调用用户主类的 main 方法
    val mainMethod = mainClass.getMethod("main", classOf[Array[String]])
    mainMethod.invoke(null, args)
  }
}

2. 用户主类执行

用户的主类(如 org.apache.spark.examples.SparkPi)的 main 方法会:

  1. 创建 SparkConf
  2. 创建 SparkSession(内部会创建 SparkContext)
  3. SparkContext 初始化时会创建 TaskScheduler 和 SchedulerBackend(详见第四篇文章),确定 Yarn Client 模式下创建的是 YarnScheduler + YarnClientSchedulerBackend ,然后调用 _taskScheduler.start() 触发 YarnClientSchedulerBackend 的启动
  4. 编写 RDD/DataFrame 业务代码,并调用 action(如 count、collect、save 等)触发 job 提交

3. Yarn Client 核心提交流程

第 2 步提到 _taskScheduler.start() 会触发 YarnClientSchedulerBackend.start()。第四篇文章列出了该方法的关键源码:

主要完成:

  • 创建 Client 对象
  • 通过 client.submitApplication() 向 YARN RM 提交 Application
  • 通过 bindToYarn() 绑定 appId
  • 等待 Application 运行

但并未深入分析具体实现,本文接下来详细分析。

3.1 YarnClientSchedulerBackend.start()

YarnClientSchedulerBackend 继承自 YarnSchedulerBackendCoarseGrainedSchedulerBackendCoarseGrainedSchedulerBackend 是通用的 Driver 端后端,负责 Executor 注册和 Task 分发(Task 分发将在后续 Job 调度文章中详细分析);YarnClientSchedulerBackend 是 Yarn Client 模式的实现,额外管理 Client 与 RM 的交互。

scala 复制代码
// YarnClientSchedulerBackend.scala
override def start(): Unit = {

  // ① 调用父类 CoarseGrainedSchedulerBackend.start()
  //    创建 DriverEndpoint(RPC 端点),用于与 Executor 通信
  //    启动等待 Executor 注册的超时定时器
  super.start()

  // ② 获取 Driver 的 host:port,传递给 AM
  //    AM 需要通过这个地址连回 Driver 注册 YarnSchedulerEndpoint
  val driverHost = conf.get(config.DRIVER_HOST_ADDRESS)
  val driverPort = conf.get(config.DRIVER_PORT)
  val hostport = driverHost + ":" + driverPort

  // 设置 Driver Web UI 地址,AM 启动后会通过 YarnSchedulerBackend 的
  // YarnSchedulerEndpoint 获取此地址并注册到 YARN RM
  sc.ui.foreach { ui => conf.set(DRIVER_APP_UI_ADDRESS, ui.webUrl) }

  // ③ 构造 ClientArguments 参数,将 hostport 作为 --arg 传递
  //    AM 启动后会解析这个参数,获取 Driver 地址并连接
  val argsArrayBuf = new ArrayBuffer[String]()
  argsArrayBuf += ("--arg", hostport)

  logDebug("ClientArguments called with: " + argsArrayBuf.mkString(" "))
  val args = new ClientArguments(argsArrayBuf.toArray)

  // ④ 获取预期 Executor 总数(根据 spark.executor.instances 或动态分配配置)
  totalExpectedExecutors = SchedulerBackendUtils.getInitialTargetExecutorNumber(conf)

  // ⑤ 创建 Client 对象 ------ 核心的 YARN 客户端
  //    Client 封装了对 YARN RM 的所有操作(提交、监控、停止)
  client = new Client(args, conf, sc.env.rpcEnv)

  // ⑥ client.submitApplication() 向 YARN RM 提交 Application
  //    这是最核心的步骤,内部实现复杂(详见下文单独分析)
  //    bindToYarn() 将返回的 appId 绑定到 YarnSchedulerBackend
  bindToYarn(client.submitApplication(), None)

  // ⑦ 轮询等待 Application 进入 RUNNING 状态
  //    如果应用已结束/失败/被 kill,直接抛出异常
  waitForApplication()

  // ⑧ 启动后台监控线程,持续监控 Application 状态
  //    如果 AM 异常退出,自动调用 sc.stop() 停止 SparkContext
  monitorThread = asyncMonitorApplication()
  monitorThread.start()

  // ⑨ 执行后续绑定操作(验证 appId 已设置等)
  startBindings()
}

说明start() 方法中的核心逻辑是步骤⑥的 client.submitApplication(),它是整个 YARN 提交过程的入口,内部实现了与 RM 交互的完整流程。

其他步骤(①②③④⑤⑦⑧⑨)的重要性与复杂度分析:

  • ①②③④ 是准备工作(获取配置、组装参数)------各模式都会做类似的事,不涉及 Client 与 Cluster 的模式差异,理解相对直观
  • ⑤ 创建 Client 对象------Client 类本身在 Client 和 Cluster 模式下都会使用,不体现模式差异
  • ⑥ client.submitApplication()------既能体现 YARN 提交的核心流程,又能体现 Client 和 Cluster 的关键差异(启动类不同、参数不同)
  • ⑦⑧⑨ 是等待和收尾------各模式差异不大

因此本文重点分析 client.submitApplication()。其他步骤根据实际需求和复杂度,可能在后继文章中单独补充总结。

3.2 Client.submitApplication()

下面详细分析 client.submitApplication() 方法的实现,这是向 YARN RM 提交 Application 的核心逻辑:

scala 复制代码
// Client.scala
def submitApplication(): ApplicationId = {

  // ① 校验 Spark 配置中的资源请求是否合法
  //    检查 spark.yarn.executor.resource.* 等配置项
  ResourceRequestHelper.validateResources(sparkConf)

  var appId: ApplicationId = null
  try {
    // ② 连接 LauncherBackend:用于接收客户端的停止请求
    //    当用户按下 Ctrl+C 或调用 kill 时,LauncherBackend 能收到通知
    launcherBackend.connect()

    // ③ 初始化 YarnClient:YARN 官方 Hadoop API 的客户端
    //    YarnClient 封装了与 RM 通信的底层细节
    yarnClient.init(hadoopConf)
    yarnClient.start()

    logInfo("Requesting a new application from cluster with %d NodeManagers"
      .format(yarnClient.getYarnClusterMetrics.getNumNodeManagers))

    // ④ 向 RM 请求创建新 Application,RM 返回 ApplicationId
    //    createApplication() 内部会与 RM 通信,RM 分配一个唯一 ID
    val newApp = yarnClient.createApplication()
    val newAppResponse = newApp.getNewApplicationResponse()
    appId = newAppResponse.getApplicationId()

    // ⑤ 创建 Container 启动上下文
    //    createContainerLaunchContext() 中指定了 AM 的启动命令、环境变量、classpath 等
    //    这是最关键的配置------决定了 AM 进程启动什么类、带什么参数
    val containerContext = createContainerLaunchContext(newAppResponse)

    // ⑥ 创建 Application 提交上下文
    //    createApplicationSubmissionContext() 中指定了应用名称、队列、资源需求等
    val appContext = createApplicationSubmissionContext(newApp, containerContext)

    // ⑦ 正式提交 Application 到 RM
    //    提交后 RM 会调度资源,在某个 NodeManager 上启动 AM Container
    logInfo(s"Submitting application $appId to ResourceManager")
    yarnClient.submitApplication(appContext)

    // ⑧ 设置 appId,通知 LauncherBackend 提交已完成
    launcherBackend.setAppId(appId.toString)
    reportLauncherState(SparkAppHandle.State.SUBMITTED)

    appId
  } catch {
    case e: Throwable =>
      // ⑨ 提交失败时清理 staging 目录
      if (stagingDirPath != null) {
        cleanupStagingDir()
      }
      throw e
  }
}
3.3 AM/ExecutorLauncher 启动方式

上面分析了 client.submitApplication() 的源码,接下来分析 RM 收到 Application 提交请求后,Container 如何在 NodeManager 上启动。

createContainerLaunchContext() 中设置了 Container 的启动命令,决定了启动哪个类:

scala 复制代码
// Client.scala --- createContainerLaunchContext 方法内部
val amClass =
  if (isClusterMode) {
    Utils.classForName("org.apache.spark.deploy.yarn.ApplicationMaster").getName
  } else {
    // Client 模式下,启动的是 ExecutorLauncher
    Utils.classForName("org.apache.spark.deploy.yarn.ExecutorLauncher").getName
  }

// 构建 AM/ExecutorLauncher 的启动命令
val amArgs = Seq(amClass) ++ userClass ++ userJar ++ ...
val commands = Seq(Environment.JAVA_HOME.$$() + "/bin/java", "-server") ++ javaOpts ++ amArgs

注意:Client 模式下 YARN 启动的类名是 ExecutorLauncher,Cluster 模式下则是 ApplicationMaster。但实际上 ExecutorLauncher.main() 内部直接调用了 ApplicationMaster.main(args),本质上执行的是同一个逻辑 。之所以区分两个类名,是为了运维方便------通过 ps/jps 查看进程名就能区分当前是 Client 模式还是 Cluster 模式(参考 ExecutorLauncher 注释:It exists so that it's easy to tell apart the client-mode AM from the cluster-mode AM when using tools such as ps or jps)。

yarnClient.submitApplication(appContext) 提交后,以下由 YARN 框架自动完成:

  1. RM 收到请求后,会在某个 NodeManager 上分配 Container
  2. RM 通知该 NodeManager 启动 Container
  3. NodeManager 根据 ContainerLaunchContext 中的命令,在 Container 中启动对应的进程(Client 模式为 ExecutorLauncher)
ExecutorLauncher 启动后的调用链
scala 复制代码
// ExecutorLauncher 入口(ApplicationMaster.scala)
object ExecutorLauncher {
  def main(args: Array[String]): Unit = {
    // 直接调用 ApplicationMaster.main()
    ApplicationMaster.main(args)
  }
}

// ApplicationMaster.main() →
def main(args: Array[String]): Unit = {
  // 解析参数
  val amArgs = new ApplicationMasterArguments(args)
  val sparkConf = new SparkConf()
  // ...
  // 创建 ApplicationMaster 实例
  master = new ApplicationMaster(amArgs, sparkConf, yarnConf)
  // ...
  // 最终调用 master.run()
  ugi.doAs(new PrivilegedExceptionAction[Unit]() {
    override def run(): Unit = System.exit(master.run())
  })
}

// ApplicationMaster.run() →
final def run(): Int = {
  if (isClusterMode) {
    runDriver()             // Cluster 模式:先启动 Driver,再申请 Executor(详见后续 Cluster 模式文章)
  } else {
    runExecutorLauncher()   // Client 模式:只申请 Executor(Driver 已在客户端运行)
  }
}

isClusterMode 的判断依据是 args.userClass != null。Cluster 模式会传递 --class 参数,所以 isClusterMode = true;Client 模式不传 --class,所以 isClusterMode = false

核心区别:Client 模式下,Driver 已经在客户端机器上运行,AM(ExecutorLauncher)只需要申请 Executor 资源;而 Cluster 模式下,AM(ApplicationMaster)需要先启动 Driver,再申请 Executor。

3.4 runExecutorLauncher()

runExecutorLauncher() 是 Client 模式下 AM 的核心入口,负责与 Driver 通信并创建资源分配器:

scala 复制代码
// ApplicationMaster.scala
private def runExecutorLauncher(): Unit = {
  val hostname = Utils.localHostName
  val amCores = sparkConf.get(AM_CORES)

  // ① 创建 RPC 环境
  val rpcEnv = RpcEnv.create("sparkYarnAM", hostname, hostname, -1, sparkConf, securityMgr,
    amCores, true)

  // ② 向 YARN RM 注册 AM,建立心跳
  registerAM(hostname, -1, sparkConf, sparkConf.get(DRIVER_APP_UI_ADDRESS), appAttemptId)

  // ③ 解析 Driver 地址,连接到 Driver 的 YarnSchedulerEndpoint
  val (driverHost, driverPort) = Utils.parseHostPort(args.userArgs(0))
  val driverRef = rpcEnv.setupEndpointRef(
    RpcAddress(driverHost, driverPort),
    YarnSchedulerBackend.ENDPOINT_NAME)

  // ④ 创建 YarnAllocator------核心资源分配器
  //    YarnAllocator 负责向 RM 申请 Container、管理 Executor 生命周期
  createAllocator(driverRef, sparkConf, rpcEnv, appAttemptId, distCacheConf)

  // ⑤ 等待汇报线程结束,持续与 RM 通信申请资源
  reporterThread.join()
}
3.5 createAllocator() 与汇报线程

上面第④步调用 createAllocator(),它的内部逻辑是这样的:

scala 复制代码
// ApplicationMaster.scala
private def createAllocator(...): Unit = {
  // 获取 Driver 端地址,用于 Executor 反向注册
  val driverUrl = RpcEndpointAddress(driverRef.address.host, driverRef.address.port,
    CoarseGrainedSchedulerBackend.ENDPOINT_NAME).toString

  // 创建 YarnAllocator 实例
  allocator = client.createAllocator(yarnConf, _sparkConf, appAttemptId,
    driverUrl, driverRef, securityMgr, localResources)

  // 设置 AM RPC 端点,接收 Driver 发来的请求(如调整 Executor 数量)
  rpcEnv.setupEndpoint("YarnAM", new AMEndpoint(rpcEnv, driverRef))

  // 立即执行一次资源分配
  allocator.allocateResources()

  // 启动汇报线程,定期调用 allocateResources()
  reporterThread = launchReporterThread()
}

可以看到,createAllocator() 内部做了三件事:

  1. 创建 YarnAllocator --- 核心资源分配器
  2. 立即分配 --- 调用 allocator.allocateResources() 立刻向 RM 申请一次 Container
  3. 启动汇报线程 --- launchReporterThread() 启动一个后台线程,定期(心跳间隔)调用 allocateResources(),持续从 RM 获取已分配的 Container

汇报线程实现如下:

scala 复制代码
// ApplicationMaster.scala
private def launchReporterThread(): Thread = {
  val t = new Thread {
    override def run(): Unit = {
      try {
        allocationThreadImpl()  // 循环调用 allocator.allocateResources()
      } finally {
        allocator.stop()
      }
    }
  }
  t.setDaemon(true)
  t.setName("Reporter")
  t.start()
  t
}
3.6 YarnAllocator 申请 Executor 资源

YarnAllocator 通过 allocateResources() 方法向 RM 发送资源请求,调用链为:

复制代码
allocateResources() → handleAllocatedContainers() → runAllocatedContainers()
scala 复制代码
// YarnAllocator.scala
def allocateResources(): Unit = synchronized {
  // 更新资源请求
  updateResourceRequests()

  // 向 RM 发送 Allocate 请求,获取已分配的 Container
  val allocateResponse = amClient.allocate(progressIndicator)
  val allocatedContainers = allocateResponse.getAllocatedContainers()

  if (allocatedContainers.size > 0) {
    // 处理已分配的 Container(按 locality 匹配请求)
    handleAllocatedContainers(allocatedContainers.asScala.toSeq)
  }
}

handleAllocatedContainers() 源码如下:

scala 复制代码
// YarnAllocator.scala
def handleAllocatedContainers(allocatedContainers: Seq[Container]): Unit = {
  val containersToUse = new ArrayBuffer[Container](allocatedContainers.size)

  // ① 按 node-local 匹配:优先使用与数据在同一节点的 Container
  val remainingAfterHostMatches = new ArrayBuffer[Container]
  for (allocatedContainer <- allocatedContainers) {
    matchContainerToRequest(allocatedContainer, allocatedContainer.getNodeId.getHost,
      containersToUse, remainingAfterHostMatches)
  }

  // ② 按 rack-local 匹配:次优先使用同一机架的 Container
  val remainingAfterRackMatches = new ArrayBuffer[Container]
  if (remainingAfterHostMatches.nonEmpty) {
    for (allocatedContainer <- remainingAfterHostMatches) {
      val rack = resolver.resolve(allocatedContainer.getNodeId.getHost)
      matchContainerToRequest(allocatedContainer, rack, containersToUse,
        remainingAfterRackMatches)
    }
  }

  // ③ any-host 匹配:最后一批,无 locality 偏好
  val remainingAfterOffRackMatches = new ArrayBuffer[Container]
  for (allocatedContainer <- remainingAfterRackMatches) {
    matchContainerToRequest(allocatedContainer, ANY_HOST, containersToUse,
      remainingAfterOffRackMatches)
  }

  // release 掉完全不需要的 Container
  if (remainingAfterOffRackMatches.nonEmpty) {
    for (container <- remainingAfterOffRackMatches) {
      internalReleaseContainer(container)
    }
  }

  // ④ 在匹配成功的 Container 上启动 Executor
  runAllocatedContainers(containersToUse)
}

按 node-local → rack-local → any-host 的优先级匹配已分配的 Container,匹配成功的放入 containersToUse,最后调用 runAllocatedContainers(containersToUse)

3.7 ExecutorRunnable 启动 YarnCoarseGrainedExecutorBackend

YarnCoarseGrainedExecutorBackend 继承自 CoarseGrainedExecutorBackendCoarseGrainedExecutorBackend 是通用的 Executor 后端基类,管理 Executor 注册和生命周期;YarnCoarseGrainedExecutorBackend 是 YARN 专用子类。

runAllocatedContainers() 为每个匹配成功的 Container 创建 ExecutorRunnable 并通过线程池执行其 run() 方法:

scala 复制代码
// YarnAllocator.scala 的 runAllocatedContainers 方法
private def runAllocatedContainers(containersToUse: ArrayBuffer[Container]): Unit = {
  for (container <- containersToUse) {
    // 为每个 Container 创建 ExecutorRunnable,在线程池中运行
    launcherPool.execute(() => {
      try {
        new ExecutorRunnable(
          Some(container),
          conf, sparkConf, driverUrl, executorId, executorHostname,
          containerMem, containerCores,
          appAttemptId.getApplicationId.toString,
          securityMgr, localResources, rp.id
        ).run()
      } catch {
        case e: Throwable => // 失败处理
      }
    })
  }
}

ExecutorRunnable 通过 YARN 的 NMClient 向 NodeManager 发送启动 Container 请求:

scala 复制代码
// ExecutorRunnable.scala
def run(): Unit = {
  logDebug("Starting Executor Container")
  nmClient = NMClient.createNMClient()
  nmClient.init(conf)
  nmClient.start()
  startContainer()
}

def startContainer(): java.util.Map[String, ByteBuffer] = {
  val ctx = Records.newRecord(classOf[ContainerLaunchContext])
    .asInstanceOf[ContainerLaunchContext]
  val env = prepareEnvironment().asJava
  ctx.setLocalResources(localResources.asJava)
  ctx.setEnvironment(env)
  val commands = prepareCommand()
  ctx.setCommands(commands.asJava)
  // 使用 YARN 的 NMClient 向 NodeManager 发送启动 Container 请求
  // NodeManager 会根据 ContainerLaunchContext 中的命令启动 YarnCoarseGrainedExecutorBackend 进程
  nmClient.startContainer(container.get, ctx)
}

prepareCommand() 将进程入口设置为 YarnCoarseGrainedExecutorBackend

scala 复制代码
// ExecutorRunnable.scala --- prepareCommand() 方法
val commands = Seq(Environment.JAVA_HOME.$$() + "/bin/java", "-server") ++
  javaOpts ++
  Seq("org.apache.spark.executor.YarnCoarseGrainedExecutorBackend",
    "--driver-url", masterAddress,
    "--executor-id", executorId,
    "--hostname", hostname, ...)
3.8 YarnCoarseGrainedExecutorBackend 向 Driver 注册

上一节中 runAllocatedContainers() 创建 ExecutorRunnable 并通过 NMClient 通知 NodeManager 启动进程。NodeManager 根据 prepareCommand() 设置的入口类启动 YarnCoarseGrainedExecutorBackend.main(),调用链如下:

复制代码
YarnCoarseGrainedExecutorBackend.main()
  → CoarseGrainedExecutorBackend.run()
    → 创建 RpcEnv
    → 连接 Driver,获取 SparkConf 配置(driver.askSync[SparkAppConfig])
    → 创建 SparkEnv
    → 创建 YarnCoarseGrainedExecutorBackend 实例
    → setupEndpoint("Executor", backend)
      → onStart() 被触发,向 Driver 注册

关键的反向注册代码在 CoarseGrainedExecutorBackend.onStart() 中:

scala 复制代码
// CoarseGrainedExecutorBackend.scala
override def onStart(): Unit = {
  logInfo("Connecting to driver: " + driverUrl)

  // 异步连接 Driver 的 DriverEndpoint
  rpcEnv.asyncSetupEndpointRefByURI(driverUrl).flatMap { ref =>
    driver = Some(ref)
    // 向 Driver 发送 RegisterExecutor 请求
    ref.ask[Boolean](RegisterExecutor(executorId, self, hostname, cores, extractLogUrls,
      extractAttributes, _resources, resourceProfile.id))
  }(ThreadUtils.sameThread).onComplete {
    case Success(_) =>
      // 注册成功后,通知自己继续启动
      self.send(RegisteredExecutor)
    case Failure(e) =>
      exitExecutor(1, s"Cannot register with driver: $driverUrl", e, notifyDriver = false)
  }(ThreadUtils.sameThread)
}

Executor 通过 RPC 向 Driver 的 DriverEndpoint 发送 RegisterExecutor 消息,Driver 收到后确认并记录该 Executor。注册成功后,onComplete 回调中 self.send(RegisteredExecutor) 触发,进入 receive 方法处理。

关于 onStart 和 RPC 通信

  • onStart()RpcEndpoint 生命周期的一部分,setupEndpoint 注册 RpcEndpoint 时会自动触发 onStart() 方法,详见之前总结的 Spark RPC 学习总结 中 "RpcEndpoint 的生命周期" 和 "方法调用" 部分
  • 本文涉及的多处 RPC 通信(DriverEndpoint、YarnSchedulerEndpoint、Executor 反向注册等)都是基于 Spark Netty RPC 框架实现的,后续将对 RPC 通信进行一次完整的源码总结
3.9 创建 Executor 计算引擎

CoarseGrainedExecutorBackend 收到 RegisteredExecutor 消息后,创建真正的 Executor 计算引擎:

scala 复制代码
// CoarseGrainedExecutorBackend.scala --- receive 方法
override def receive: PartialFunction[Any, Unit] = {
  case RegisteredExecutor =>
    logInfo("Successfully registered with driver")
    // 注册成功后,创建真正的 Executor 计算引擎
    executor = new Executor(executorId, hostname, env, userClassPath, isLocal = false, resources)
}

至此,YarnCoarseGrainedExecutorBackend (进程)完成了注册,内部创建了 Executor(计算对象),可以开始接收任务了。

关于 Executor 的详细分析Executor 是 Spark 真正的计算引擎,内部包含线程池、任务执行、内存管理、Shuffle 读写等核心逻辑。本文将不深入展开,后续会单独一篇文章进行详细分析。

3.10 Job 提交与 Task 执行(后续文章分析)

Executor 注册到 Driver 后,Yarn Client 模式的提交流程基本完成。接下来,Driver 继续执行用户主类中的业务代码:

  1. 创建 DataFrame / RDD
  2. 调用 Action 算子(如 countcollectsave 等)
  3. DAGScheduler 将 Job 划分为多个 Stage,每个 Stage 生成 TaskSet(多个 Task,数量等于分区数)
  4. TaskScheduler 将 TaskSet 中的 Task 调度到已注册的 Executor 上执行

这套 Job 提交、Stage 划分和 Task 执行的流程是各模式通用的公共逻辑,与部署模式无关(Yarn、Standalone 都一样)。为避免本文篇幅过长,将在后续文章中单独分析。


Yarn Client 核心提交流程总结:从 YarnClientSchedulerBackend.start() 向 RM 提交 Application,到 AM 启动、申请 Executor、Executor 反向注册到 Driver。

Yarn Client vs Yarn Cluster 对比

特性 Yarn Client Yarn Cluster
Driver 位置 客户端 AM(ApplicationMaster)
提交命令响应 立即返回 等待 AM 启动
资源占用 客户端需保持长连接,占用客户端资源 客户端可断开,作业在集群独立运行
childMainClass args.mainClass(用户类) YarnClusterApplication
日志可见性 本地直接可见 需要通过 YARN 查看
AM 启动类 ExecutorLauncher ApplicationMaster
AM 入口方法 runExecutorLauncher() runDriver()
isClusterMode 判断 args.userClass == null args.userClass != null
资源申请顺序 Driver 已就绪,直接申请 Executor AM 先启动 Driver,再申请 Executor
Driver 启动方式 用户主类在客户端被 runMain 反射调用 AM 内通过 runDriver() 调用 runMain() 启动

总结

流程概述

Yarn Client 模式的完整提交流程:

  1. SparkSubmit 提交程序 → 本地运行 Driver → 初始化 SparkContext(启动 TaskScheduler、YarnClientSchedulerBackend 等)
  2. YarnClientSchedulerBackendResourceManager 提交 Application,申请启动 ExecutorLauncher(AM)
  3. ResourceManager 分配 Container,在 NodeManager 上启动 ExecutorLauncher
  4. ExecutorLauncher(AM)向 ResourceManager 申请启动 Executor,RM 返回已分配的 Container 列表
  5. 根据返回的 Container 列表,在对应 NodeManager 上启动 YarnCoarseGrainedExecutorBackend
  6. YarnCoarseGrainedExecutorBackendDriver (YarnClientSchedulerBackend 的 DriverEndpoint)发送 RegisterExecutor 注册,注册成功后内部创建 Executor 对象执行计算任务
  7. Driver 运行 Main 函数,创建 DataFrame/RDD,调用 action 算子,DAGScheduler 划分 Stage、生成 TaskSet,TaskScheduler 调度 Task 到 Executor 执行

核心调用链

复制代码
SparkSubmit.main
  → submit → runMain
    → 反射调用用户主类.main(Driver 在客户端启动)
      → SparkContext 初始化 → _taskScheduler.start()
        → YarnClientSchedulerBackend.start()                          [3.1]
          → super.start() 创建 DriverEndpoint
          → new Client(args, conf, rpcEnv)
          → client.submitApplication()                                [3.2]
            → yarnClient.createApplication() 获取 appId
            → createContainerLaunchContext() 设置 AM 启动类
              → ExecutorLauncher(Client 模式)                       [3.3]
            → yarnClient.submitApplication(appContext)
          → waitForApplication()

        [ExecutorLauncher 在集群启动]
        ExecutorLauncher.main → ApplicationMaster.main
          → ApplicationMaster.run() → runExecutorLauncher()           [3.4]
            → registerAM()
            → createAllocator()                                       [3.5]
              → YarnAllocator.allocateResources()                     [3.6]
                → handleAllocatedContainers()(node-local → rack-local → any-host)
                  → runAllocatedContainers()
                    → ExecutorRunnable.run()                          [3.7]
                      → startContainer() → prepareCommand()
                        → NMClient → NodeManager
                          → YarnCoarseGrainedExecutorBackend.main()

        [Executor 进程启动]
        YarnCoarseGrainedExecutorBackend.main
          → CoarseGrainedExecutorBackend.run()                        [3.8]
            → 创建 RpcEnv / SparkEnv
            → setupEndpoint("Executor", backend)
              → onStart() → 发送 RegisterExecutor 到 DriverEndpoint
                → Driver 确认
                  → 收到 RegisteredExecutor
                    → new Executor(...)                               [3.9]

        [Driver 执行业务代码]
        DAGScheduler.runJob
          → handleJobSubmitted → createResultStage 划分 Stage
            → submitStage → submitMissingTasks 生成 TaskSet
              → taskScheduler.submitTasks                             [3.10]

括号中的 3.x 对应本文各章节编号,便于快速回溯源码分析。

下一次我们将分析 Yarn Cluster 模式的详细提交流程。

相关推荐
Urbano8 小时前
稳产提质、快返增效:慧拿智能模板机重塑服装厂精细化盈利模式
大数据·人工智能
YueLin1119 小时前
私域直播系统的下半场,是社区零售的分水岭
大数据·零售
哥本哈士奇9 小时前
医疗器械行业 Salesforce Territory 完整落地实例
大数据·人工智能
samLi06209 小时前
【无标题】
大数据
zandy10119 小时前
企业级BI平台选型指南:评估框架与核心能力矩阵
大数据·人工智能·矩阵
远铂10 小时前
BuildAdmin:GEO优化与AI内容营销一体化解决方案
大数据·人工智能·geo·buildadmin
abcy07121310 小时前
storm 实时性
大数据·storm
10岁的博客10 小时前
DevEco Code 的 Plan+Build 模式:审方案再执行的技术深度解析
大数据·数据库·人工智能
会助力智能会务11 小时前
会务系统供应商怎么选?会助力智能会务系统,一站式数字化会务服务商
大数据·运维·人工智能