前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站: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 和 classpathrunMain方法:反射调用用户主类本文重点分析 Yarn Client 模式下的完整提交流程,包括 SparkSubmit 到 Driver 初始化、Client 与 RM 通信、AM 启动、Executor 分配与注册。
1. runMain 执行用户主类
在第二篇文章中已经分析了 runMain 方法,它会:
- 调用
prepareSubmitEnvironment获取 childMainClass - 通过反射加载主类
- 如果主类实现了
SparkApplication接口,直接实例化;否则包装成JavaMainApplication - 调用
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 方法会:
- 创建 SparkConf
- 创建 SparkSession(内部会创建 SparkContext)
- SparkContext 初始化时会创建 TaskScheduler 和 SchedulerBackend(详见第四篇文章),确定 Yarn Client 模式下创建的是 YarnScheduler + YarnClientSchedulerBackend ,然后调用
_taskScheduler.start()触发 YarnClientSchedulerBackend 的启动 - 编写 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 继承自 YarnSchedulerBackend → CoarseGrainedSchedulerBackend。CoarseGrainedSchedulerBackend 是通用的 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 框架自动完成:
- RM 收到请求后,会在某个 NodeManager 上分配 Container
- RM 通知该 NodeManager 启动 Container
- 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() 内部做了三件事:
- 创建 YarnAllocator --- 核心资源分配器
- 立即分配 --- 调用
allocator.allocateResources()立刻向 RM 申请一次 Container - 启动汇报线程 ---
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 继承自 CoarseGrainedExecutorBackend。CoarseGrainedExecutorBackend 是通用的 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 继续执行用户主类中的业务代码:
- 创建 DataFrame / RDD
- 调用 Action 算子(如
count、collect、save等) - DAGScheduler 将 Job 划分为多个 Stage,每个 Stage 生成 TaskSet(多个 Task,数量等于分区数)
- 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 模式的完整提交流程:
- SparkSubmit 提交程序 → 本地运行 Driver → 初始化 SparkContext(启动 TaskScheduler、YarnClientSchedulerBackend 等)
- YarnClientSchedulerBackend 向 ResourceManager 提交 Application,申请启动 ExecutorLauncher(AM)
- ResourceManager 分配 Container,在 NodeManager 上启动 ExecutorLauncher
- ExecutorLauncher(AM)向 ResourceManager 申请启动 Executor,RM 返回已分配的 Container 列表
- 根据返回的 Container 列表,在对应 NodeManager 上启动 YarnCoarseGrainedExecutorBackend
- YarnCoarseGrainedExecutorBackend 向 Driver (YarnClientSchedulerBackend 的 DriverEndpoint)发送
RegisterExecutor注册,注册成功后内部创建 Executor 对象执行计算任务 - 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 模式的详细提交流程。