前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住给大家分享一下。点击跳转到网站: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 及各模式下角色的详细总结,将在系列文章完成后单独分析。
1 | // 提交命令示例 |
提交流程
在第二篇文章 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 模式,完整调用链如下:
1 | 用户主类 SparkPi(没有实现 SparkApplication) |
JavaMainApplication.start 方法:
1 | class JavaMainApplication(mainClass: Class[_]) extends SparkApplication { |
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 的交互。
1 | // YarnClientSchedulerBackend.scala |
说明:
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 的核心逻辑:
1 | // Client.scala |
3.3 AM/ExecutorLauncher 启动方式
上面分析了 client.submitApplication() 的源码,接下来分析 RM 收到 Application 提交请求后,Container 如何在 NodeManager 上启动。
在 createContainerLaunchContext() 中设置了 Container 的启动命令,决定了启动哪个类:
1 | // Client.scala — createContainerLaunchContext 方法内部 |
注意: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 启动后的调用链
1 | // ExecutorLauncher 入口(ApplicationMaster.scala) |
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 通信并创建资源分配器:
1 | // ApplicationMaster.scala |
3.5 createAllocator() 与汇报线程
上面第④步调用 createAllocator(),它的内部逻辑是这样的:
1 | // ApplicationMaster.scala |
可以看到,createAllocator() 内部做了三件事:
- 创建 YarnAllocator — 核心资源分配器
- 立即分配 — 调用
allocator.allocateResources()立刻向 RM 申请一次 Container - 启动汇报线程 —
launchReporterThread()启动一个后台线程,定期(心跳间隔)调用allocateResources(),持续从 RM 获取已分配的 Container
汇报线程实现如下:
1 | // ApplicationMaster.scala |
3.6 YarnAllocator 申请 Executor 资源
YarnAllocator 通过 allocateResources() 方法向 RM 发送资源请求,调用链为:
1 | allocateResources() → handleAllocatedContainers() → runAllocatedContainers() |
1 | // YarnAllocator.scala |
handleAllocatedContainers() 源码如下:
1 | // YarnAllocator.scala |
按 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() 方法:
1 | // YarnAllocator.scala 的 runAllocatedContainers 方法 |
ExecutorRunnable 通过 YARN 的 NMClient 向 NodeManager 发送启动 Container 请求:
1 | // ExecutorRunnable.scala |
prepareCommand() 将进程入口设置为 YarnCoarseGrainedExecutorBackend:
1 | // ExecutorRunnable.scala — prepareCommand() 方法 |
3.8 YarnCoarseGrainedExecutorBackend 向 Driver 注册
上一节中 runAllocatedContainers() 创建 ExecutorRunnable 并通过 NMClient 通知 NodeManager 启动进程。NodeManager 根据 prepareCommand() 设置的入口类启动 YarnCoarseGrainedExecutorBackend.main(),调用链如下:
1 | YarnCoarseGrainedExecutorBackend.main() |
关键的反向注册代码在 CoarseGrainedExecutorBackend.onStart() 中:
1 | // CoarseGrainedExecutorBackend.scala |
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 计算引擎:
1 | // CoarseGrainedExecutorBackend.scala — receive 方法 |
至此,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 执行
核心调用链
1 | SparkSubmit.main |
括号中的 [3.x] 对应本文各章节编号,便于快速回溯源码分析。
下一次我们将分析 Yarn Cluster 模式的详细提交流程。