Spark on YARN:‘Yarn application has already ended‘报错排查指南 这行报错百分之九十的人第一次看到都会头皮发麻。明明代码在本地跑得欢一放到 YARN 上就冒出一句 “Yarn application has already ended! It might have been killed or unable to connect to the Application Master.”。最气的是这句描述里既没有说具体是哪个 Container 挂了也没有说是在哪一步断的看起来像是 Spark 在“半路被截胡”。这篇文章就是把这句话彻底讲透它到底从哪冒出背后可能有哪些根因用什么手段一步步定位以及我实际调过的参数和经验教训。适合读这篇文章的朋友有两类一是刚从单机转向 YARN 集群第一次把 Spark 任务丢上去就撞到这堵墙的新手二是在集群上跑过不少任务但遇到这类“玄学报错”还只能靠猜的老手。只要照着后面几节的排查顺序走八成能在一小时之内把问题定位到具体原因而不是像无头苍蝇一样反复改几个毫无关联的参数。1. client模式下这段报错到底发生在哪一步1.1 client模式与cluster模式的本质差异很多人看到这句话的第一反应是“应用是不是被 kill 了”。但这里要先分清 client 模式和 cluster 模式的差异因为理解不到位后面排查方向就全偏了。client 模式提交有个很典型的结构SparkContext 运行在提交任务的客户端 JVM 里也就是你执行 spark-submit 的那台机器。Driver 在你本地而 ApplicationMasterAM也会以某种形态和提交端绑定。Executor 则分散在集群各 NodeManager 上。任务跑起来之后Executor 需要反向连接 Driver把计算结果和心跳回报给 Driver。如果客户端网络环境特殊或者 Driver 端口被防火墙挡住哪怕 Executor 已经正常启动了它也没办法把数据传回 Driver。这时候 YARN 层可能认为应用还活着但 Spark 客户端这一侧已经感知不到 AM 或者 Executor 的健康状态了。cluster 模式就完全不一样。Driver 和 AM 一起运行在集群的一个 Container 里提交端只是把任务交出去剩下的全部在集群内部完成。这样对提交端的网络依赖很小但日志不容易直接看到调试也不如 client 模式顺手。很多团队最喜欢用 client 模式因为调试方便Driver 日志直接刷在终端里还能随手打印一些调试信息。可一旦真正跑在大集群上client 模式对提交端的网络稳定性、端口开放情况、本机内存要求都很高。这行“already ended”的报错常见触发场景就是 Driver 侧已经失去和 YARN 应用的连接而你会发现它和“任务被 YARN 杀掉”还不完全是一回事。1.2 报错文本拆解状态是FINISHED、FAILED还是KILLED这句话最迷惑人的地方在于它没有告诉你应用最终是哪种状态。我用几个不同环境实测过发现同样的输出可能对应三种不同结局应用根本没跑起来AM 在启动阶段就崩了YARN 在尝试几次之后标记为 FAILED应用跑了一半某个 Executor 内存超限被 NodeManager 秒杀随后 AM 也连带失败应用其实已经跑完了但因为 Driver 和 AM 之间的通信提前断了Spark 客户端误认为自己被抛弃。所以如果你只在 spark-submit 的终端里看到这句话第一件事不是改代码而是去查 YARN 上这个应用的最终状态。怎么看最简单的办法是带 applicationId 执行yarn application -status application_xxx输出里会有一个 State 字段可能是 FINISHED、FAILED、KILLED也可能是 RUNNING。如果是 RUNNING说明提交端只是短暂失去了联系问题基本锁死在网络配置或 Driver 连接上。如果是 FAILED就去翻日志里的 Exception 和 Diagnostics 字段。如果是 KILLED就要想想是不是你自己在另一个终端执行了yarn application -kill或者调度器因为资源紧缺而主动清理了闲置应用。这三种状态背后对应完全不同的解决策略所以在任何节点深入之前先用这个命令把“结局”定死。2. 第一次定位必做的三件事从状态诊断到日志抓取2.1 先把ApplicationState和诊断信息拉到眼前不夸张地说这类问题有三分之一是可以通过yarn application -status直接看出答案的因为 YARN 的 Diagnostics 字段往往已经写得很直白。我曾经在一个内部集群上遇到过 AM 反复启动失败执行状态查询命令后Diagnostics 里直接写了类似“Container killed on request. Exit code is 143”这样的信息。这代表 AM Container 是被强杀的而不是代码里自己抛异常。为了拿到完整信息建议按下面的顺序操作拿到 applicationId。如果 spark-submit 命令还没退出通常终端输出里就有如果已经退出可以登录 ResourceManager 页面查找或者去 YARN 的日志目录扫一遍。执行yarn application -status applicationId重点看 State、FinalStatus 和 Diagnostics。如果有多个 attempt例如 AM 被重启了好几次再用yarn applicationattempt -list applicationId列出每一次 attempt 的 id 和状态。最后用yarn applicationattempt -status attemptId查看单次 attempt 的详细诊断。上面这套命令如果你以前没操作过可能会觉得多了一点但胜在结论明确。它能把“应不应该继续深挖日志”这个问题快速回答清楚。如果 Diagnostics 里已经写明了资源不足、队列拒绝、认证失败等字样那就不用再大海捞针。2.2 日志不是全都要看看这几处就够了从状态命令拿到线索之后下一步就是定点拆日志。很多人一打开日志目录就像进了迷宫不知道从哪个文件看起。实际只需要抓住三个层次客户端日志也就是你执行 spark-submit 的终端输出Driver 的所有输出都在这里。client 模式的好处就在这如果 AM 还没启动就挂了终端里大概率会有完整的堆栈比如 ClassNotFoundException、文件不存在、配置解析失败等。YARN 聚合日志用yarn logs -applicationId applicationId把该应用的所有 Container 日志汇总拉到本地然后按容器类型过滤。重点找 Executor 日志里有没有 “ExecutorLostFailure”、AM 日志里有没有 “Exception from container-launch” 这类关键字。NodeManager 本机日志如果聚合日志因为配置没开启而缺就得登录日志报错中对应的 NodeManager 主机去 yarn 的用户日志目录翻 container 日志。这里能看到被 JVM 参数撑爆内存之前的最后痕迹以及系统级 kill 的标记。在抓日志的过程中记住一个原则先看 AM 日志再看 Executor 日志最后看客户端。如果 AM 本身就没起来那 Executor 日志毫无意义如果 AM 活着但 Executor 全部失联重点就是网络或资源分配如果所有日志都正常但客户端报错问题多半出在提交端到 AM 的连接上。3. 高频根因逐一拆解为什么会被kill、为什么连不上3.1 根因一AM或Executor内存超限被YARN强杀我从实践里统计过这个报错最高频的根因是容器实际内存超过 YARN 允许上限被 NodeManager 判定违规后强制杀掉。YARN 默认对内存的判定比较死板它认为 Container 申请的内存配额是 executor-memory 加上 memoryOverhead 的总和如果 JVM 实际使用的物理内存超过这个总和就会触发超限杀。这类情况在日志里最典型的标志是Container killed by YARN for exceeding memory limits. 10.0 GB of 8 GB physical memory used。为什么 JVM 只设置了 8GB实际却吃到 10GB原因很多堆内对象膨胀、堆外内存、Direct Memory、网络缓冲区、Spark 本身因为 shuffle 或序列化产生的额外开销。调整的时候不要无脑加大 executor-memory而是要同时修改 memoryOverhead。这个 overhead 默认取 max(executorMemory*0.10, 384MB)对很多负载来说是偏小的。比较稳妥的做法如果 Executor 日志显示 GC 频繁但容器被强杀说明堆可能够用但要明显调大spark.yarn.executor.memoryOverhead如果堆一直处于高水位说明代码里对象太多优先降低单 Executor 内存、增加 Executor 数量不要无限增大 overhead。AM 内存也同理。client 模式里 AM 和 Driver 绑定在提交端所以spark.yarn.am.memoryOverhead也要给够。默认值偏向保守如果 Driver 在 shuffle service 注册、序列化大量任务描述时出现奇怪断连建议先把 AM 的 overhead 调整到 1g 左右再观察。3.2 根因二客户端网络或端口问题导致Driver失去心跳client 模式的隐藏门槛往往是“提交端能不能被所有 Executor 反向访问”。还记得有一次我在本地公司网络里测试Spark 应用在 YARN 上启动成功Executor 也正常起来但过了几十秒就报出这句 already ended。查了一圈后才发现本地防火墙默认丢弃了 Spark 随机选定的 driver 端口段Executor 的心跳都发不过来。类 Unix 系统上如果开着防火墙默认策略很容易把高位随机端口拦截。解决办法是在提交命令里显式指定 Driver 端口避免随机--conf spark.driver.port55510 \ --conf spark.driver.blockManager.port55520 \ --conf spark.driver.host客户端对外可访问的IPspark.driver.host这步很多人会踩坑。如果你的机器有多个网卡Spark 可能会自动选择一个只在内网可见的 IP导致集群节点根本无法连接。这时候在客户端执行hostname -I或ip addr看看哪个 IP 是集群可以访问的然后手动通过 spark.driver.host 指定。另外不要在远程 SSH 会话中断开的场景下长期跑 client 模式任务。如果 SSH 窗口在网络抖动中临时断了连接Driver 进程虽然还在但本地终端可能已经收不到输出也可能因为终端退出触发进程清理看起来就像应用被“杀”了。生产环境建议使用界面窗口运行或直接使用 cluster 模式。3.3 根因三资源队列限制与调度拒绝还有一个很容易忽略的根因是 YARN 队列资源限制。集群管理员通常会为不同业务设置不同的队列并规定最大可分配资源。如果你提交任务时用的队列资源紧张或者单个任务申请的资源超过队列上限YARN 不会马上让你失败而是会让应用一直停在 ACCEPTED 状态。如果客户端侧的超时设置偏偏又比较短客户端就会认为应用已经结束了抛出那句“It might have been killed”。这种情况状态查询的结果可能依然显示 ACCEPTED也可能显示 FAILEDDiagnostics 里会提到类似 “Queue is being closed” 或 Application 无法满足资源申请。应对方法先用yarn queue -status 队列名查看当前队列可用资源适当减少单次申请资源比如降低 num-executors 或 executor-cores如果必须使用某队列请管理员调大队列资源上限或加白名单。在资源相对紧张的共享集群上我建议把 Driver 内存和 Executor 内存都设置成符合业务规模的“够用值”不要随手写成 16g、32g。过大的申请不一定更快反而更容易被调度器限制。3.4 根因四依赖或环境不一致导致AM启动即失败如果客户端里打包的依赖和集群环境不匹配AM 很可能在启动时抛 ClassNotFoundException 或方法不存在异常。这种问题发生时YARN 应用会在短时间快速失败几次然后客户端收到这句 already ended。定位手段非常粗暴看客户端完整堆栈。如果终端里能够看到一个明确的 NoSuchMethodError 或 ClassNotFoundException多半是以下情况之一本地打出的 fat jar 用了 scala 版本和集群 Spark 版本不兼容的依赖忘记把运行时要用的第三方 jar 带上只在本地 classpath 里存在spark-submit 用 --jars 引用的路径在客户端不可读或者路径配置错了。解决时先检查驱动端版本。比如把 Spark 升级到高版本后以前写的代码还在用旧 API即使编译能过运行期也会炸。这种情况没有任何参数能够救回来只能统一依赖版本后重新打包。4. 一套可以照着改的提交参数模板4.1 基础提交命令示例先给一个我目前生产环境里常用的提交模板适配中量级任务然后说明每个参数背后的思路。直接抄过去不代表一定适合你的集群但它提供的是一套经过验证的“起步框架”。spark-submit \ --master yarn \ --deploy-mode client \ --name demo-wordcount \ --class com.example.WordCount \ --queue default \ --driver-memory 4g \ --executor-memory 8g \ --executor-cores 4 \ --num-executors 20 \ --conf spark.yarn.executor.memoryOverhead1g \ --conf spark.yarn.am.memoryOverhead1g \ --conf spark.network.timeout600s \ --conf spark.yarn.maxAppAttempts2 \ --conf spark.driver.port55510 \ --conf spark.driver.blockManager.port55520 \ /data/app/spark-demo.jar \ /user/demo/input \ /user/demo/output在 client 模式里Driver 就是运行在提交端的 AM 相关进程所以--driver-memory不要写得过大。提交端如果同时跑多个任务过大的 Driver 内存会造成本机内存压力最终 OS 层也可能把进程 OOM Kill。通常 2g 到 4g 的 driver-memory 已经能覆盖大部分 SparkContext 管理开销真正耗内存的 Executor 端才是需要重点关注的对象。spark.network.timeout从默认的 120s 放大到 600s是为了应对 Executor 数量多、注册时长可能拉长的情况。尤其在 Executor 数量超过几十个的时候每批容器启动都要经历资源分配、节点下载依赖、JVM 初始化、向 Driver 注册这一整套流程120 秒在很多慢盘或网络繁忙的集群上根本不够。4.2 关键参数取值逻辑与经验参考表参数默认值什么时候需要调经验参考spark.yarn.executor.memoryOverheadmax(executorMemory*0.1, 384m)shuffle 量大、堆外内存频繁、物理内存超限被 kill先给 1g 再续观察日志spark.yarn.am.memoryOverheadmax(amMemory*0.1, 384m)client 模式 Driver 莫名断连、AM 容器超限先给 1gspark.network.timeout120sExecutor 数量大、网络波动、集群繁忙、注册超时300s600sspark.driver.port随机提交端在防火墙/NAT 后、Executor 连接 Driver 失败固定一段可用端口如 55510spark.driver.blockManager.port随机提交端被防火墙拦截 BlockManager 传输固定一段可用端口如 55520spark.yarn.maxAppAttempts2想让 AM 失败后额外重试时24不宜过大spark.sql.shuffle.partitions200数据量大但并行度低、Executor 内存波动大按 Executor 总数或核心总数的倍数调整调参数有一个原则一次只改一两个而不是把所有参数全部放大。很多新手一上来就把 Executor 内存设成 32g、overhead 设成 4g结果资源申请又超队列上限反而触发另一种形式的失败。参数的组合效果需要靠日志验证改完必须重跑并对比日志关键词。4.3 调试期的一组“降级”参数遇到这类报错时很多人并不需要立刻上生产参数而应该先用“降级方案”快速复现和定位。调试期我会把任务规模压到最小同时降低不确定性spark-submit \ --master yarn \ --deploy-mode client \ --conf spark.yarn.executor.memoryOverhead1g \ --conf spark.yarn.am.memoryOverhead1g \ --conf spark.network.timeout300s \ --conf spark.dynamicAllocation.enabledfalse \ --num-executors 2 \ --executor-memory 2g \ --executor-cores 1 \ --driver-memory 1g \ ...jar path把动态分配关掉是很关键的一步。动态分配打开时Executor 会根据负载增加或减少调整过程中会不断有 Executor 发起注册或退出网络和注册相关的报错也更容易被放大。先用固定数量的 Executor 跑通小任务再逐步恢复动态分配能显著降低排查面。5. 那些容易误判的场景日志里没有报错任务却结束5.1 SparkSession复用带来的“假死”这一类特别容易犯我也是在写了一个长期运行的数据服务之后才撞上的。服务里维护了一个 SparkSession 单例第一次任务正常跑完第二次任务接着用同一个 SparkSession 提交结果报出 already ended。原因很简单SparkSession 底层绑定的 SparkContext 与某个 YARN Application 关联当第一个 Application 结束后如果代码没有重建 SparkContext 而是继续沿用旧的连接再执行 Action 时 Spark 发现对应的 Application 早已不在只能抛出这段提示。解决办法也直接一个应用的生命周期内复用 SparkSession但在应用结束后重新创建不要在做批处理时长期持有同一个 Session 并反复执行 Task。如果确实要在服务进程里反复运行多个 Spark 任务建议每次任务都调用spark.stop()并重新SparkSession.builder().getOrCreate()。5.2 断点续跑与交互式环境中的特殊问题如果在 Jupyter 或者本地 IDE 里写脚本习惯上可能在一个进程里多次运行同一个 cell。第一次运行没问题第二次运行直接报 already ended多半就是因为上一次配套的 SparkContext 被自动或手动 stop 了但新执行的代码还试图通过旧上下文访问 YARN 上的应用。这类环境还容易出现 JVM 内存堆积Driver 进程的句柄、线程没有及时清理导致反复提交后本地端口被占满。建议交互式调试时每个 notebook 或调试会话只初始化一次 Spark 环境然后把所有相关代码放在同一个执行块里中途如果手动调用了 stop必须重启内核或显式重新初始化。5.3 认证过期与元数据清理等冷门因素安全集群里还容易出现一个隐蔽问题提交端使用了 keytab 进行认证但 keytab 过期或者 TSGT 过期导致 AM 启动后无法正常访问 HDFS应用只能反复失败。状态查询里的 Diagnostics 可能会写类似 token 过期、无法获取租约等描述这类信息不会直接出现在 spark-submit 的本地终端里很容易被忽略。另外当 application ID 非常庞大、提交时间距今较长或集群开启了状态清理策略YARN 的资源管理器可能已经删除了应用状态。此时客户端试图获取旧应用信息时也会得到 already ended 的提示。这种情况几乎不需要处理反正就是一个过期引用问题重新提交一次新任务即可。6. 常见问题速查表日志词加根因加对策6.1 “日志词→根因→对策”对照表日志或现象关键词根因判断优先对策Container killed by YARN for exceeding memory limitsExecutor 或 AM 物理内存超限调大对应 memoryOverhead或降低单 Executor 内存Exception from container-launchAM 容器启动失败可能是依赖或环境问题看完整堆栈检查 jar、脚本、路径权限ExecutorLostFailureExecutor 中途丢失查看容器日志与心跳间隔调大 spark.network.timeoutQueue is being closed队列资源受限换队列或减少单个任务申请资源Could not connect to the Application Master客户端与 AM 无法连通检查 spark.driver.host、端口、防火墙、网络策略NoSuchMethodError / ClassNotFoundException依赖版本冲突或缺少依赖统一 Spark/Scala/依赖版本并重新打包Token expired / lease mismatch认证过期刷新 keytab、ticket检查集群时钟同步SparkSession 在同一进程中二次提交上下文复用导致的引用失效重建 SparkSession 或重启应用进程这张表是我每次排查这类问题时都贴在旁边用的。它没法覆盖所有情况但至少能帮助在日志还没有明确堆栈的情况下先把问题缩小到一到两个方向。很多时候看到对应关键词再翻一次当前任务的参数答案基本就浮出来了。6.2 我用过的最实用踩坑清单再补充几条文档之外的经验都是我踩过坑之后才记得住的第一不要一上来就怀疑 YARN 或集群不稳定。同一个集群上如果别人跑得好好的首先对比的是你和别人的 submit 参数尤其是 memoryOverhead 和网络超时。第二client 模式下提交端的时钟必须和集群节点保持一致。Kerberos 或 TLS 环境下客户端时间误差超过一定阈值就会出现“时间偏移”导致的握手失败看起来也像断连。第三Spark 客户端版本尽量和集群版本保持大版本一致。小版本差异有时候能跑有时候跑到一半出现序列化异常最后在 YARN 侧表现为应用异常结束。使用官方推荐的版本匹配是降低混乱的最佳路径。第四出现问题后保存好当时完整的 spark-submit 命令、YARN 状态输出、客户端日志。这些信息在第二次排查时就是你的对照组。如果修改了参数后报错变了说明上一轮已经排除了一个问题这一点很重要。结尾最后分享一个习惯我每次处理完这类问题都会把“状态查询输出 对应日志片段 本次参数变更”缩写成一行笔记记录在一个 markdown 文件里。日积月累之后再遇到“Yarn application has already ended”就再也不慌直接去查之前的笔记先对日志关键词再对参数切线。这个办法比死记硬背参数列表更有用。如果看完这篇你还是没能定位建议立刻按第二节的顺序把状态查到再用第四节的调试参数模板跑一个最小任务让日志把真实原因推到你面前。