前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住给大家分享一下。点击跳转到网站:https://www.captainai.net/dongkelun
前言
在第七篇文章 Standalone Client 模式提交流程(七) 中,我们分析了 Standalone Client 模式的完整提交流程。本文继续分析 Standalone Cluster 模式,其 Driver 运行在集群 Worker 节点上,而不是提交客户端。
Standalone Cluster 模式是 Spark 自带集群管理器的完整形态:Master 负责资源调度和 Driver 分发,Worker 负责启动 Driver 和 Executor。其流程比 Client 模式多了一层”提交网关”。
版本
Spark 3.2.3
Standalone Cluster 模式概述
Standalone Cluster 模式下,Driver 运行在集群中的某个 Worker 节点上。spark-submit 进程只负责提交请求,不运行用户程序。
注意:Master 和 Worker 需要提前启动(start-master.sh / start-worker.sh),spark-submit 时集群资源管理的基础设施已经就绪。
与 Standalone Client 的核心差异:
| 维度 | Standalone Client | Standalone Cluster |
|---|---|---|
| Driver 位置 | spark-submit 进程内(客户端) | Master 调度到某个 Worker 节点 |
| spark-submit 主类 | 用户 mainClass | ClientApp 或 RestSubmissionClientApp(包装器) |
| 提交后 spark-submit | 一直运行,直到应用退出 | 提交成功后默认即退出 |
| LauncherBackend(状态通知通道) | 建立(StandaloneSchedulerBackend.start 中) | 不建立(提交后即退出,不需要) |
| Python/R 支持 | 支持 | 不支持 |
--supervise 支持 |
不适用 | 支持(Driver 失败后自动重启) |
流程对比总览:
1 | Standalone Client: |
1 | // 提交命令示例 |
0. RPC 通信方式汇总
本文涉及的 RPC 通信对照(与 Standalone Client 相同的部分不再重复):
| 位置 | 方式 | 原因 |
|---|---|---|
3.1 Client → Master:masterRef.ask(RequestSubmitDriver) |
ask |
需要知道 Master 是否接受了 Driver 提交 |
4.1 Master → Worker:worker.endpoint.send(LaunchDriver) |
send |
通知 Worker fork 子进程运行 Driver |
5.1 Worker → Driver:WorkerWatcher 监控 |
RPC 连接 | 监控 Worker 连接状态,Worker 挂则 Driver 退出 |
6.2 Driver → Master:StandaloneAppClient.registerWithMaster |
send |
通知 Master 注册 Application |
完整提交流程
1. SparkSubmit 准备阶段
1.1 prepareSubmitEnvironment
在 SparkSubmit.scala 的 prepareSubmitEnvironment 中,当 master.startsWith("spark://") 且 deployMode == "cluster" 时:
1 | // SparkSubmit.scala — prepareSubmitEnvironment |
首先进入 Fail fast 段落,排除了不支持的模式——Python 和 R 在 Standalone Cluster 模式下会直接报错终止:
1 | // SparkSubmit.scala:prepareSubmitEnvironment — Fail fast 段落 |
通过 match 模式匹配在准备阶段就拦截,而不是等到运行时才报错。如果走 REST API 方式(useRest=true),StandaloneRestServer:buildDriverDescription 也会对 mainClass 做非空校验——Python 应用没有 mainClass,同样会提前失败:
1 | // StandaloneRestServer.scala:buildDriverDescription |
拦截后,进入 Standalone Cluster 模式的分支,根据 useRest 区分 Legacy 和 REST 两种提交方式,设置 childMainClass:
1 | // SparkSubmit.scala — prepareSubmitEnvironment 中的 Standalone Cluster 分支 |
useRest 的默认值为 false,读取配置 spark.master.rest.enabled(默认为 "false")。因此默认走 Legacy 方式。
注意 childMainClass 不再是用户主类,而是 ClientApp 或 RestSubmissionClientApp——与 Standalone Client 模式(spark-submit 直接运行用户代码)不同,此模式下 spark-submit 进程变成了一个纯粹”提交客户端”。
childArgs 中有一行 childArgs += "launch"。这个 "launch" 最终会原封不动传给 ClientApp,参数传递链路如下:
1 | // SparkSubmit.scala:prepareSubmitEnvironment — 拼装 childArgs |
2. 两种提交方式总览
Standalone Cluster 模式支持两种提交方式:
| 提交方式 | 启用条件 | 入口类 | 通信协议 |
|---|---|---|---|
| Legacy 提交(默认) | spark.master.rest.enabled=false | ClientApp |
RPC(Netty) |
| REST 提交 | spark.master.rest.enabled=true | RestSubmissionClientApp |
HTTP REST |
两种方式都只负责向 Master 提交 Driver 描述信息,然后轮询 Driver 状态。spark-submit 进程不运行用户代码。
1 | spark-submit |
如果启用了 REST 方式但连接失败,会 fallback 到 Legacy 方式:
1 | // SparkSubmit.scala submit() |
3. Legacy 提交方式(默认,RPC)
3.1 ClientApp.start()
ClientApp 的 start 方法创建 RPC 环境,连接到 Master,注册 ClientEndpoint:
1 | // Client.scala — ClientApp.start() |
3.2 ClientEndpoint.onStart()
ClientEndpoint.onStart() 是 Standalone Cluster 模式的关键,它构造 DriverDescription 并提交给 Master:
1 | // Client.scala — ClientEndpoint.onStart() |
核心要点:
mainClass被替换为DriverWrapper,而不是用户的 mainClass- 用户的 mainClass 被放入
command.arguments中,作为DriverWrapper的参数传入 {{WORKER_URL}}和{{USER_JAR}}是占位符,Worker 启动时替换为实际值asyncSendToMasterAndForwardReply向所有 Master 发送请求,回复由receive处理
3.3 启动轮询
ClientEndpoint.onStart() 最后一步是启动定时任务,向 Master 轮询 Driver 状态(详细分析见第 7 节):
1 | // Client.scala — ClientEndpoint.onStart() |
4. REST 提交方式(可选)
4.1 RestSubmissionClientApp
启用方式:spark-submit --conf spark.master.rest.enabled=true ...
RestSubmissionClientApp.start() 解析参数后调用 RestSubmissionClient:
1 | // RestSubmissionClient.scala — RestSubmissionClientApp.start() |
4.2 REST API 通信
createSubmission 向 Master 发送 HTTP POST 请求:
1 | POST http://master:6066/v1/submissions/create |
请求流程:
1 | // RestSubmissionClient.scala — createSubmission |
提交成功后,同样会轮询 submission 状态(pollSubmissionStatus,最多 10 次,间隔 1s)。
5. Master 调度 Driver
无论是 Legacy 还是 REST 方式,最终 Master 都会收到提交 Driver 的请求。以 Legacy 方式为例:
5.1 Master 接收 RequestSubmitDriver
1 | // Master.scala — receive 中的 RequestSubmitDriver 处理 |
5.2 schedule() 调度 Driver
Master 的 schedule() 方法中,Driver 调度优先于 Executor 调度:
1 | // Master.scala — schedule() |
canLaunchDriver 检查 Worker 是否有足够的空闲内存和 CPU 核心来运行 Driver。
launchDriver 向 Worker 发送 LaunchDriver 消息:
1 | // Master.scala — launchDriver |
6. Worker fork 子进程运行 Driver
6.1 Worker 接收 LaunchDriver
1 | // Worker.scala — receive |
DriverRunner.start() 在新线程中启动 Driver 进程:
1 | // DriverRunner.scala — start() |
start() 的核心逻辑:新线程运行 → prepareAndRunDriver() → ProcessBuilder.start() 创建全新 OS 进程(DriverWrapper)→ 根据退出码设置最终状态 → 通知 Worker。
DriverWrapper 和 Worker 是两个完全独立的 JVM 进程,只是运行在同一台机器上。Worker 通过 ProcessBuilder.start() fork 出子进程运行 DriverWrapper,并非在 Worker 进程内启动。
6.2 DriverRunner 准备并运行 Driver
DriverRunner.prepareAndRunDriver() 的完整源码和关键步骤:
1 | // DriverRunner.scala — prepareAndRunDriver() |
对应之前的文字说明,这 4 步分别做了:
- 创建 Driver 工作目录
- 下载用户 JAR:通过
driverDesc.command中的jarUrl将用户 JAR 下载到本地(注意:Worker 通过Utils.fetchFile()下载,file:// 或裸路径走本地拷贝,需所有 Worker 该路径下有文件;也可用 HDFS/HTTP 等远程存储) - 替换占位符:
{{WORKER_URL}}替换为当前 Worker 的 RPC 地址,{{USER_JAR}}替换为下载后的本地路径 - 构建 ProcessBuilder:用
CommandUtils.buildProcessBuilder构造启动命令 - 启动子进程:
ProcessBuilder.start()
实际启动的命令等价于:
1 | java -cp <classpath> \ |
6.3 DriverWrapper 入口
DriverWrapper 是 Worker fork 出的子进程的入口类。它接收 Worker URL、用户 JAR 路径和用户主类名:
1 | // DriverWrapper.scala — main |
WorkerWatcher 是一个关键组件——它监控与 Worker 的 RPC 连接。如果 Worker 进程挂了(网络断开),WorkerWatcher 会主动退出 Driver 进程,实现”命运共享”。
7. 轮询 Driver 状态
Client 向 Master 提交 Driver 后,spark-submit 进程需要知道:Driver 提交成功了吗?运行完了没有?我该不该退出? 由于 Master 的调度和 Worker 的启动都是异步的,Client 只能通过轮询来感知进展。
轮询结果决定了 spark-submit 的退出行为:
| 场景 | spark-submit 行为 |
|---|---|
提交成功(RUNNING),默认 waitAppCompletion=false |
立即 exit(0),提交机使命完成 |
提交成功 + waitAppCompletion=true |
继续轮询,等 Driver FINISHED/FAILED 后再 exit(0) |
| 提交失败(exception / 找不到 driverId / backup Master) | exit(-1) |
monitorDriverStatus 通过 asyncSendToMasterAndForwardReply 向 Master 发送 RequestDriverStatus,Master 回复后经 receive 路由到 reportDriverStatus:
1 | // Client.scala — reportDriverStatus |
整体逻辑分三层:
1 | reportDriverStatus(found, state, workerId, workerHostPort, exception) |
exception 为什么会出现——状态查询的回复怎么会带异常信息?看 Master 端如何回复 RequestDriverStatus:
1 | // Master.scala — receive 中的 RequestDriverStatus 处理 |
exception 在三个场景中的含义:
- Master 非 ALIVE(如 backup):
found=false, exception=Some(msg),Client 识别为 backup 后静默忽略 - Driver 运行出错:
found=true, exception=driver.exception,进入Some(e)分支exit(-1) - 查无此 driverId:
found=false, exception=None,进入最后的else分支exit(-1)
8. SparkContext 初始化
用户 mainClass 中的代码开始执行,例如 SparkPi.main() 中调用了 new SparkContext()。
这一步 与 Standalone Client 模式完全一致:
1 | // SparkContext.scala — createTaskScheduler |
创建 TaskSchedulerImpl + StandaloneSchedulerBackend,没有分支区别。
8.1 StandaloneSchedulerBackend.start()
1 | // StandaloneSchedulerBackend.scala — start() |
Cluster 模式跳过 launcherBackend.connect()——提交成功后 spark-submit 默认即退出,不需要接收 Driver 状态通知。
8.2 信号枪机制
与 Standalone Client 一样,使用 Semaphore(0) 阻塞等待 StandaloneAppClient 注册完成:
1 | // StandaloneSchedulerBackend.scala |
当 StandaloneAppClient 收到 RegisteredApplication 回复后,回调 connected() 释放信号量:
1 | override def connected(msg: String): Unit = { |
9. Application 注册与 Executor 启动
9.1 StandaloneAppClient 注册
client.start() → ClientEndpoint.onStart() → registerWithMaster(1) → tryRegisterAllMasters() → 向所有 Master 发送 RegisterApplication。
这一步与 Standalone Client 模式完全一致,包括重试机制(3 次,间隔 20 秒)。
9.2 Master 调度 Executor
Master 收到 RegisterApplication 后,创建 App 并触发 schedule():
1 | // Driver 已经调度完成,现在调度 Executor |
schedule() 中 Driver 部分已经跳过(waitingDrivers 为空),直接调用 startExecutorsOnWorkers(),其流程与 Standalone Client 完全一致:
scheduleExecutorsOnWorkers()— spreadOut 算法分配 Executor 到 WorkerallocateWorkerResourceToExecutors()— 为每个 Worker 创建 ExecutorDesclaunchExecutor()— 向 Worker 发送LaunchExecutor
9.3 Worker 启动 Executor
Worker 收到 LaunchExecutor,创建 ExecutorRunner,启动 CoarseGrainedExecutorBackend 子进程,替换占位符。与 Standalone Client 完全一致。
9.4 Executor 反向注册
CoarseGrainedExecutorBackend 进程启动后,通过 driverUrl 连接 DriverEndpoint,发送 RegisterExecutor,Driver 端创建 Executor 对象。
与 Standalone Client 完全一致。
10. 完整流程图
1 | spark-submit (提交机) |
与 Standalone Client 的差异汇总
| 编号 | 差异点 | Cluster 模式 | Client 模式 |
|---|---|---|---|
| 1 | spark-submit 主类 | ClientApp/RestSubmissionClientApp(包装器) |
用户 mainClass |
| 2 | Driver 位置 | Worker 节点(子进程) | spark-submit 进程内 |
| 3 | 是否使用 DriverWrapper | 是,Worker fork 子进程运行 DriverWrapper 反射调用用户类 | 否 |
| 4 | Python/R 支持 | 不支持,直接报错 | 支持 |
| 5 | spark-submit 进程与 Driver 状态通信 | 不建立状态通知通道(提交后即退出,不需要) | 通过 LauncherBackend 建立 Socket 通道,接收 Driver 状态变化通知 |
| 6 | --driver-memory |
生效(用于 Worker 分配资源) | 所有集群管理器 + 所有部署模式均生效 |
| 7 | --driver-cores |
生效(用于 Worker 分配资源) | 仅 STANDALONE/MESOS/YARN/K8s, CLUSTER 生效,Client 模式不生效 |
| 8 | --supervise |
支持 | 不适用 |
| 9 | SparkContext 初始化 | TaskSchedulerImpl + StandaloneSchedulerBackend | 完全一致 |
| 10 | 信号枪机制 | Semaphore(0) | 完全一致 |
| 11 | spark-submit 进程 | 提交成功后默认即退出 | 一直运行到应用退出 |
| 12 | 用户 JAR 访问 | 需所有 Worker 节点在相同路径下都有该 JAR,或使用远程存储(HDFS/HTTP) | 仅提交机本地即可,无需提前分发 |
| 13 | Master Web UI 显示 | 有 Running Drivers 列表 |
无 Running Drivers(只有 Running Applications) |
设计要点
- 提交网关模式:Standalone Cluster 的 spark-submit 不运行用户代码,只做提交。
ClientApp和RestSubmissionClientApp是两种提交网关实现。 - DriverWrapper 反射层:Worker 通过
DriverWrapper启动用户 mainClass,中间加了一层 ClassLoader 隔离,保证用户依赖和 Spark 依赖不冲突。 - WorkerWatcher 命运共享:Driver 进程中的
WorkerWatcher监控 Worker 连接,Worker 挂则 Driver 自动退出,避免孤立进程。 - Legacy + REST 双通道:默认走 RPC 方式,可选 REST 方式(更稳定的跨版本协议),REST 失败会自动 fallback 到 Legacy。
- 信号枪机制:与 Client 模式相同,
StandaloneSchedulerBackend使用Semaphore(0)阻塞等待注册完成,区别在于此时代码运行在 Worker 节点的 DriverWrapper 子进程中,而非 spark-submit 进程。