首页 / 资讯中心 / 文章详情

Spark源码解析:spark-submit提交流程与Driver启动全链路剖析

Spark源码解析:spark-submit提交流程与Driver启动全链路剖析 ★ FEATURED ARTICLE
之前在交流群里有人问过我一个问题Spark 源码解析系列里为什么 Submit 提交流程要单独拆出来讲我当时给的答案是因为整个 Spark 任务从“你敲下一条命令”到“真正开始跑计算”中间隔了非常长的链路而这个链路恰恰是新手看源码最容易迷路的一段。尤其是Driver 启动流程不同部署模式、不同提交模式下走的路径几乎完全不一样类名绕、消息多、日志还分散在好几个节点上。这篇文章就以 Spark 源码为主线把spark-submit从客户端启动到 Driver 在集群中被拉起的完整流程拆开。我会以 Standalone 模式为主轴穿插 YARN 模式的差异对比尽量把每个关键类、每个关键消息、每次状态切换都讲清楚。适合正在看 Spark 源码的同学、面试前想理清提交流程的人以及被“提交了半天没反应、日志不知道去哪看”折磨过的运维和开发。1. 提交流程的全局视角四个角色、两个阶段1.1 参与提交流程的四个核心角色在看任何源码之前先得把参与方认齐。Spark 提交阶段涉及的组件比运行时多一个“客户端侧”的角色很多人一开始搞混就是因为没区分这些进程分别在哪里。角色进程/实例职责提交客户端spark-submit脚本启动的 JVM解析参数、选择入口、发送提交请求或直接启动 Driver集群管理节点MasterStandalone/ ResourceManagerYARN接收应用注册、分配资源、调度启动 Driver/Executor集群工作节点WorkerStandalone/ NodeManagerYARN实际拉起 Driver 进程或 Executor 进程驱动进程Driver / ApplicationMaster / DriverWrapper运行用户 main 方法、创建 SparkContext、调度任务这里特别提醒一点Driver 不是一个固定的守护进程而是你提交的那个应用的主控制进程。在 Client 模式下它就是spark-submit本身在 Cluster 模式下它是在某个 Worker 或 NM 上被远程拉起的独立 JVM。这个区别贯穿整个提交流程务必先记住。1.2 整个提交流程本质上分两个阶段我在读源码时最大的顿悟就是Spark 的“提交”不是一个动作而是两个动作。第一阶段客户端把应用描述提交给集群集群为其建立一个“应用实体”并决定在哪里启动 Driver。第二阶段Driver 进程起来之后内部创建 SparkContextSparkContext 又会反向向集群申请 Executor 资源。也就是说第一次提交是“注册应用并启动 Driver”第二次注册是“Driver 就绪后申请计算资源”。很多人只看客户端提交那一段源码以为 Executor 也是那时候一起分配的其实不对。Standalone Cluster 模式下Master 第一次看到这个应用时只会尝试启动 DriverExecutor 是等 Driver 进程内 SparkContext 创建完毕、TaskScheduler 反向发起注册之后才分配的。后面第 5 章我会展开讲这条反向链路。1.3 为什么部署模式决定了源码阅读路线Spark 支持 Standalone、YARN、Kubernetes 三种主流部署提交入口虽然都是SparkSubmit但内部选择的childMainClass完全不同--master spark://host:7077且--deploy-mode cluster走 Standalone 集群提交逻辑本地方只用 REST 客户端发请求--master yarn且--deploy-mode cluster走 YARN Client 提交类向 ResourceManager 提交 application--master yarn且--deploy-mode client本地直接运行用户主类轮询或等待集群回报 Executor 注册情况--master local[N]干脆不经过任何集群本地起线程模拟。本文主线是 Standalone Cluster 模式因为它最能体现“提交 - 注册 - 调度 - 拉起 Driver”的完整链路。YARN 模式我会在第 5 章单独对比因为它的 Driver 启动位置和 Standalone 有本质区别。2. SparkSubmit 的入口逻辑从脚本到反射调用用户主类2.1 bin/spark-submit 到底做了什么先说最外层。你执行的bin/spark-submit本质上是一个 shell 脚本做了一些 JAVA_HOME、SPARK_HOME 的环境检查之后最终会执行类似这样的命令exec ${SPARK_HOME}/bin/spark-class org.apache.spark.deploy.SparkSubmit $也就是说SparkSubmit这个类是所有提交动作的唯一起点。它内部第一件事就是把你传进来的--class、--master、--deploy-mode、--conf、--executor-memory等参数封装成一个SparkSubmitArguments对象。这个对象非常关键因为后面prepareSubmitEnvironment方法要根据它决定“当前这个提交动作应该由谁来执行”。一个常见的错误是以为 SparkSubmit 就是用来启动用户主类的实际上在 Cluster 模式下SparkSubmit 只是个搬运工真正的用户主类是远程被别的 JVM 调用的。2.2 childMainClass 的三种选择SparkSubmit.doSubmit里会调用prepareSubmitEnvironment根据action默认是 SUBMIT和参数组合计算出childMainClass。我直接说结论Standalone ClusterchildMainClass是org.apache.spark.deploy.rest.RestSubmissionClient旧版本可能是org.apache.spark.deploy.ClientYARN ClusterchildMainClass是org.apache.spark.deploy.yarn.ClientClient 模式无论哪种 masterchildMainClass就是你自己写的那个--class指定的主类。换句话说SparkSubmit会根据模式决定“我要不要直接运行用户代码”。如果是 Client 模式用户代码是在提交端运行的那么它返回的主类就是用户主类如果是 Cluster 模式用户代码应该到集群里运行所以本地只启动一个提交客户端。你可以用--verbose跑一次提交日志里会打印类似Main class: org.apache.spark.deploy.rest.RestSubmissionClient的信息这能帮你确认当前走的是哪条路径。2.3 doRunMain 与 runMain 的反射调用边界选定childMainClass之后SparkSubmit会调用doRunMain再由runMain通过反射找到主类的main方法并执行。这里有一个容易忽略的细节分配给子主类的ClassLoader和classpath已经被prepareSubmitEnvironment调整过。比如 Client 模式下用户的 classpath 必须包含业务 jar 路径所以SparkSubmit会把--jars、--packages等依赖参数拼进 classpath而 Cluster 模式下提交端并不需要用户业务代码因此本地参数解析完之后只会把用户 jar 的路径作为描述信息传给集群而不是加载进本地 JVM。所以在 Client 模式下spark-submit进程不会马上退出它会一直运行到你作业跑完而 Cluster 模式下本地进程往往提交完就退出了后续状态都在集群侧维护。很多同学第一次用 Cluster 模式提交后很困惑“为什么终端没输出日志了”原因就在这里。3. 提交请求从客户端到 Master 的注册链路3.1 Standalone 模式下的两条提交通道前文说到Standalone Cluster 模式下本地会启动RestSubmissionClient。为什么不是直接连 Master 发消息这其实是 Spark 演进的结果。老版本里提交端走的是org.apache.spark.deploy.Client它会启动一个ClientEndpoint通过 RPC 直接向 Master 发送RegisterApplication消息。新版本为了解耦引入了 REST 提交接口客户端向 Master 的 HTTP 端口发送 JSON 格式的应用描述Master 侧由一个RestSubmissionServer接收再转成内部消息交给主消息循环。两条通道最终都会汇入 Master 对应用注册的处理逻辑。区别只是客户端侧的行为REST 客户端提交后会轮询应用状态直到成功或失败老版ClientEndpoint则一直挂在 RPC 通道里监听事件。我建议阅读源码时以 REST 提交路径为主因为它更干净并且本地 JVM 生命周期更短更容易理解“提交端与运行端分离”这层设计。3.2 Master 收到注册请求后的内部动作不管请求从哪条通道进来Master 核心逻辑都在Master.receive里。对应到 REST 提交场景请求会被包装成类似RequestSubmitApplication的消息Master 处理流程大致是为应用创建ApplicationInfo分配一个全局唯一的 appId格式通常是app-YYYYMMDDHHMMSS-xxxx将应用状态置为WAITING放进waitingApps队列调用schedule()触发一次资源调度。这里有一个很重要的状态机概念一个应用从提交到结束会经历WAITING - RUNNING - FINISHED/FAILED。Driver 没启动成功之前应用永远是WAITING一旦 Driver 启动Master 从 Worker 那里收到 Driver 状态变更才会把应用状态改成RUNNING。所以你如果要判断一个任务到底卡在哪先去看 Master UI 上应用的状态如果一直是WAITING说明调度阶段还没完成如果变成RUNNING但任务没进度说明 Driver 起来了问题可能出在 Executor 注册或者任务执行阶段。3.3 REST 提交的细节与轮询机制RestSubmissionClient的提交过程其实不复杂但它有几个关键点值得源码阅读者留意提交请求是 POST 到${masterUrl}/v1/submissions/create请求体是应用描述含主类、jar、参数、资源要求Master 的RestSubmissionServer在启动 Master 时一并启动监听 REST 端口创建成功后客户端会收到一个submissionId然后开始轮询${masterUrl}/v1/submissions/status/{submissionId}轮询直到状态从SUBMITTED变成RUNNING或FINISHED或超时退出。这个轮询机制的源码值得好好读一下它里面包含了各类状态对应的日志输出。比如你经常会看到这样两行日志INFO RestSubmissionClient: Submitting application to ... INFO RestSubmissionClient: Application submission successful with submissionId app-xxx如果第二行没出现说明请求根本没到 Master问题出在网络、端口或参数解析如果出现了但后面没有状态更新问题往往在集群侧资源调度上。4. Master 的资源调度与 Driver 被拉起的路径4.1 schedule() 的触发时机与调度策略Master.schedule()是整个提交流程的中枢方法。它会在以下几个时机被触发新应用注册完成新的 Worker 注册或 Worker 状态更新比如 Executor 退出释放资源Driver 状态变化比如某个 Driver 执行完成或失败。其核心逻辑可以概括为遍历waitingApps为每个应用挑选满足 CPU 和内存要求的 Worker然后决定在哪台机器上启动 Driver、在哪些 Worker 上启动 Executor。这里要特别说明Standalone 的资源判断依赖 Worker 上报的心跳信息每个 Worker 会记录自己的coresFree和memoryFree。Master 调度时不会只看总资源还要扣掉 Worker 预留的系统内存。Worker 启动时默认会预留一部分内存spark.worker.reserved.memory默认 1024MB也就是说一个 8GB 的 Worker可用内存通常不到 8GB。你申请资源时如果把 executor 内存顶到上限很容易出现“应用排队但一直调度不出来”的现象。4.2 LaunchDriver 消息到 Worker 的执行路径当 Master 决定在某台 Worker 上启动 Driver 时会向该 Worker 发送一条LaunchDriver消息消息里包含一个DriverInfo对象里面有 Driver 的唯一 ID、应用描述、启动命令、环境变量、SPARK_HOME 路径等信息。Worker 收到后会创建一个DriverRunner来管理这个 Driver 进程的生命周期。DriverRunner做的事情可以拆成三步在工作目录下创建 driver 专属子目录通常是${SPARK_HOME}/work/${driverId}重定向日志把 Driver 的 stdout 和 stderr 分别写到该目录下的stdout和stderr文件构造启动命令用ProcessBuilder拉起一个新的 JVM运行的入口类通常是org.apache.spark.deploy.DriverWrapper。这一步你应该能看出一个实用价值Standalone Cluster 模式下Driver 日志不在你提交任务的终端上而是在被分配的那台 Worker 节点的 work 目录下。很多新手提交后看不到日志就以为任务挂了其实去 Worker 节点翻driver-xxx/stdout和stderr就能看到完整输出。4.3 Client 模式和 Cluster 模式在 Driver 启动上的本质差异这一小节必须强调因为前面讲的“Master 调度 Worker 拉起 Driver”只属于 Cluster 模式。Client 模式完全是另一条路维度Client 模式Cluster 模式Driver 位置提交端本地 JVMMaster 选定的 Worker 节点提交端进程常驻承担 Driver 职责提交完退出用户日志位置提交端终端Worker 的 work 目录失败表现提交端退出作业立刻失败提交端可能看到轮询超时应用场景调试、交互式任务生产定时任务、资源隔离要求高Client 模式下SparkSubmit直接运行用户主类用户代码里的SparkContext是在提交端创建的。此时虽然也会走“注册应用 - 分配 Executor”的流程但 Master 不需要启动 Driver因为它已经存在了。源码里你会看到 Master 在schedule()里判断如果应用的 Driver 已经启动或不需要启动就只分配 Executor。所以当你看到网上说“Driver 由 Master 拉起”时要想想他讲的是哪种模式。源码解析里最怕的就是把两种模式的代码路径混在一起讲读者越看越乱。5. Driver 进程内部DriverWrapper 到 SparkContext 的初始化顺序5.1 DriverWrapper.main 的第一件事Cluster 模式下Worker 拉起的 JVM 入口是org.apache.spark.deploy.DriverWrapper。它的 main 方法会接收、解析几个关键参数driverId、driverUrlDriver 注册到 RPC 环境中的地址、用户 jar 路径、用户主类名、用户传给主类的参数。DriverWrapper的第一件事并不是直接调用你的main而是先创建 SparkContext。原因很好理解用户代码里往往要使用SparkSession、SparkContext做各种操作如果先把用户 main 跑起来再初始化上下文那用户代码里第一个 RDD 操作都无从谈起。所以它的流程是用传入的配置构造SparkConf创建SparkContext然后向当前 RPC 环境注册一个标识自身状态的 endpoint最后通过反射调用用户类main方法传入解包后的用户参数。你的main方法真正开始执行时Driver 端的运行环境已经就绪了。5.2 SparkContext 构造阶段的关键组件SparkContext的构造函数是理解 Driver 启动后初始化逻辑的最佳入口。它初始化组件的顺序很重要因为后面的组件往往依赖前面的组件。我用几个关键节点串一下SparkEnv创建 Driver 端的环境对象内部包含 RpcEnv、BlockManager、MemoryManager、MapOutputTracker、ShuffleManager 等核心组件TaskSchedulerImpl任务调度器的具体实现负责把任务分发到 ExecutorDAGScheduler高层调度器把 Job 拆成 Stage再由 Stage 拆成 Task提交给 TaskSchedulerStandaloneSchedulerBackend / YarnSchedulerBackend调度后端负责与集群通信、注册 Driver、申请 ExecutorWebUI、LiveListenerBus、ContextCleaner辅助组件分别负责监控页面、事件监听、清理内存中的临时数据。这里要理解一个设计点TaskScheduler 和 DAGScheduler 是分层配合的。DAGScheduler 面向业务逻辑负责划分 StageTaskScheduler 面向资源负责把 Task 发到 Executor。真正和 Master 通信的是 TaskScheduler 底层的 SchedulerBackend。所以 Executor 的申请动作发生在哪里发生在TaskSchedulerImpl.start()被调用的时候。Standalone 模式下SchedulerBackend 会启动一个StandaloneAppClient由它向 Master 发送注册请求申请当前应用需要的 Executor。这个注册动作是Driver 内部发起的第二次注册和第一次spark-submit提交时的注册完全不同。5.3 用户 main 何时执行Job 何时真正提交前面说过SparkContext 构造完成之前用户 main 不会运行构造完成后DriverWrapper才反射调用用户 main。但“用户 main 跑起来”不等于“提交了 Job”。Job 的提交发生在你调用 RDD 的 action如count、collect、saveAsTextFile时。action 会触发DAGScheduler.submitJobDAGScheduler 划分 Stage生成 TaskSet投递给 TaskSchedulerTaskScheduler 再通过 SchedulerBackend 把任务序列化后发送给 Executor。换句话说Driver 启动完成后它还不会马上“干活”它要先把自己注册到集群、申请到足够数量的 Executor然后坐在那里等你的业务代码触发 action。这一段时间如果你看到 Master UI 上应用是 RUNNING 但没有任务在跑不用慌很可能只是在等 Executor 全部注册完成。如果spark.dynamicAllocation.enabled开启了还会出现先申请少量 Executor、跑起来再动态加减的情况。但不管怎么加入口都绕不开 Driver 内 TaskScheduler 对 Executor 的申请请求。5.4 YARN 模式下 Driver 启动的差异对比最后把 YARN 模式的路径拉出来对比一下因为标题里的“Driver 启动流程”在 YARN 下完全是另一套类名。YARN Cluster 模式下SparkSubmit启动的是org.apache.spark.deploy.yarn.Client。这个 Client 负责向 ResourceManager 提交 application 请求RM 找一个 NodeManager 启动ApplicationMaster。此时有两种情况Cluster 模式ApplicationMaster 内部会直接承担 Driver 职责。它先创建 SparkContext 或 SparkSession再运行用户 main 方法然后向 RM 申请 Container 来启动 ExecutorClient 模式ApplicationMaster 只是一个ExecutorLauncher负责为本地 Driver 申请 Executor真正的 Driver 在提交端 JVM 里。所以你看同样是“Cluster”Standalone 的 Cluster 用DriverWrapper作为入口YARN 的 Cluster 用ApplicationMaster作为入口同样是“Client”Standalone 的 Client 本地直接跑用户类YARN 的 Client 本地也直接跑用户类但需要一个额外的 AM 去申请资源。这些差异如果混在一起记很容易记串。建议按“Driver 进程到底在哪台机器上”来记而不是按命令参数来记。6. 提交与启动过程中的典型故障排查6.1 提交后一直卡在“已提交但没反应”这是我被问得最多的一类问题。症状是终端里出现了类似Application submission successful的日志但之后半天没有新日志应用状态一直不前进。按照链路顺序排查先看提交端是否进入了轮询等待如果是 Standalone REST 提交本地进程会一直轮询状态直到超时登录 Master Web UI看应用的 state 是WAITING还是RUNNING如果是WAITING说明 Master 还没把 Driver 调度出去重点看Resources栏通常是可用 Worker 资源不足如果是RUNNING但没进度去 Driver 所在 Worker 的 work 目录看driver.stdout和driver.stderr。最常见的原因是第 3 步。我见过不少同学把spark.driver.memory调到接近单机内存上限结果 Worker 上根本没有能容纳 Driver 的机器。Master 调度的逻辑是“找到一个剩余内存不小于 Driver 内存的 Worker”找不到就一直等没有任何超时报错。6.2 Driver 启动后立刻退出如果 Worker 日志显示 Driver 进程启动后又退出核心排查依据是退出码和 stderr 文件。如果是非零退出码先看 stderr 里有没有Exception如果是 0 退出说明用户 main 正常执行完了可能只是任务短或逻辑里自己退出了如果 stderr 出现No such file或ClassNotFound检查启动命令里的 classpath 是否包含了业务 jar。还有一个容易忽略的点Worker 拉起的 Driver 进程的用户是 Worker 进程的用户。如果你的业务代码要访问某个只有当前用户有权限的路径可能本地能跑、集群里报 Permission denied。这个坑在读取 HDFS、写本地文件时特别常见。6.3 Executor 起不来或数量不足Driver 成功启动后下一个常见故障是 Executor 反复启动失败。此时日志分布在两个地方Master 日志会显示向哪些 Worker 发了LaunchExecutorWorker 日志和 Executor 的 stderr 会显示失败原因。排查要点现象可能原因Executor 申请后长时间LAUNCHINGWorker 资源不足或 Executor 镜像/环境准备慢Executor 启动后立即退出用户 jar 里依赖缺失、classpath 错误、内存配置超过机器物理内存Executor 数量少于配置Master 无法凑齐所需资源会先分配一部分反复出现Lost executor节点网络不稳、Executor 内存被系统 OOM Killer 杀掉、心跳超时特别提醒Spark 对 Executor 的内存判断不是只看你填的spark.executor.memory还会额外计算 overhead默认是 executor 内存的 10% 或 384MB 中较大值。Worker 剩余内存不够时即使看起来“理论够”实际也调度不出去。这个参数在 YARN 模式下更敏感因为需要同时通过 RM 的容器内存校验。6.4 日志追踪的现实顺序最后给一张我实际排查问题时使用的日志查看顺序希望能减少你走弯路的时间提交端终端日志确认模式、入口类、是否提交成功Master Web UI看应用状态、Driver 分配在哪台 Worker、Executor 列表Master 运行日志查调度阶段的拒绝或异常原因Driver 所在 Worker 的 work 目录driver-xxx看 Driver 的 stderr/stdoutExecutor 所在 Worker 的 work 目录app-xxx看 Executor 的 stderr/stdoutSpark 历史服务器如果启用看 Job/Stage 维度的失败信息。一般来说90% 的问题在 2 和 4 两步里就能找到答案。如果这两步都没发现异常再往上翻 Master 日志和 Worker 日志重点搜Exception、Error、WARN关键字的上下文。我个人在调试 Standalone Cluster 模式时还习惯用jps在 Worker 节点上看进程。如果是DriverWrapper说明 Driver 进程还在如果是CoarseGrainedExecutorBackend说明 Executor 进程在跑。配合 PID 再去看对应的日志目录定位速度会快很多。这套方法我用了很久比盯屏幕等日志靠谱得多。
阅读完成 · 觉得有帮助?
咨询建站