你有没有在凌晨的跑批窗口里,盯着终端猛然刷出一行Yarn application has already ended! It might have been killed or unable to connect to ResourceManager。这句话最让人崩溃的地方在于——它根本没有告诉你到底是哪里挂了,只是把结果甩给你。很多时候你会发现,明明任务刚提交上去,application已经结束了,HDFS上写了一半的数据还停在那,集群里的其他任务倒是跑得好好的。
这个报错我在 Spark on YARN 的 client 模式下遇到过不少次,也帮同事排查过类似问题。它真正的含义是:你的 Spark 应用在 YARN 上已经处于 FINISHED/FAILED/KILLED 状态,而提交端的 driver 进程没有收到预期的注册确认或者状态更新。换句话说,driver、ApplicationMaster、ResourceManager 三者之间的通信链路断了,断的位置不同,解决方向就完全不一样。这篇文章把我踩过的那些坑、完整的排查链条和最终改的配置都写清楚,希望对正在被这个报错折磨的人有帮助。
1. 先认清:client模式下的Spark任务是怎么“活”着,又是怎么“死”的
1.1 client模式有别于cluster模式的运行链路
很多人一开始会被 client 和 cluster 两种 deploy-mode 搞混。先说结论:你本地启动的 SparkContext 才是 driver,集群里跑的 ApplicationMaster 只是帮你向 ResourceManager 要资源的“代办”。两者的角色分配完全不同,错误现象也因此不同。
用 client 模式跑任务时,整个链路是这样的:
- 你在提交机上执行
spark-submit --master yarn --deploy-mode client,提交机上的 JVM 会先启动 driver。 - driver 创建 SparkContext,通过 YarnClient 向 ResourceManager 提交 application,并等待 AM 注册回来。
- ResourceManager 在某个 NodeManager 上为 AM 分配 container 并启动它。
- AM 启动后,再向 ResourceManager 申请 executor 的 container,拉起一个或多个 executor。
- executor 启动后,会反向连接提交机上的 driver,注册自己的状态,之后双方就直接通信。
cluster 模式则不同:driver 被放到集群的 AM container 里,提交机上只有一个“提交客户端”,它把任务丢给 YARN 后,本质上可以退出。所以在 cluster 模式下,你很少看到 driver 端出现网络连接问题,因为 driver 就在集群内部。
client 模式的麻烦就是第 5 步那条反向链路。集群里的 executor 要访问你的提交机,你就必须保证提交机与集群之间的网络不光是“能通”,而且是双向可路由、无防火墙拦截的。很多人只测了从提交机 ping 集群节点,忽略了反向连接,结果任务一开始看起来很正常,等到 executor 要连回 driver 时就出问题。
1.2 这个报错到底出现在哪个时间点
Yarn application has already ended!这句话,通常出现在两个时间段:
一是任务提交后的等待阶段。driver 的 YarnClient 在向 RM 提交 application 后,会一直等待 AM 的注册响应。如果 RM 一直没把 AM 拉起来,或者 AM 起来之后没能成功注册,客户端就可能报出这个错。
二是任务运行中途。AM 因为某种原因被 kill,或者 AM 与 RM 的心跳中断,YARN 判定 application 结束,driver 这边等不到后续消息,就会在客户端输出这个错误。伴随这个错误的,往往还有一条更关键的 Diagnostics 信息,比如:
Application application_173_0001 failed 2 times due to AM Container for appattempt_173_0001_000001 exited with exitCode: -100 Diagnostics: ... Failed to register application master with ResourceManager注意这里有个重点:这个报错只是症状,不是根因。它显示的是 driver 观察到“application 已经结束”这个结果,真正原因藏在 YARN 页面的 Diagnostics、AM container 日志和 executor 日志里。所以别一看到它就去改代码,那是本末倒置。
2. 这句报错只告诉你“死了”,没告诉你“为什么”:抓现场日志是第一步
2.1 三分钟拉齐全套信息
遇到这个报错,我最反感的就是直接蒙一个原因然后重试。正确的做法是先花三分钟把现场信息固定下来,不管你是想快速恢复还是事后复盘,这些信息都缺一不可。
需要收集的包括:
- 完整的 spark-submit 命令,包括所有
--conf参数; - 客户端控制台从提交到报错之间的全部输出;
- YARN ResourceManager Web UI 上该 application 的详情页,尤其是 Diagnostics 字段;
- 该 application 对应的 AM container 日志和 executor container 日志;
- 提交机上的
spark-*.logdriver 日志。
从 YARN 侧拉日志,最省事的是:
yarn logs -applicationId application_173_0001 > /tmp/application_173_0001.log这个命令会把该 application 下所有 container 的日志汇总到一个文件里,方便 grep。前提是集群开了日志聚合,也就是yarn.log-aggregation-enable=true。如果没开,yarn logs是拉不到的,只能通过yarn node -list找到 container 所在的节点,去 NodeManager 本地的logs目录下翻文件。忘了这个前提,你会卡在“日志拉不到”这一步上很久。
拿到日志后,不要直接搜Yarn application has already ended,那只会让你绕回原点。重点搜这些关键行:
| 日志内容 | 含义 | 优先排查方向 |
|---|---|---|
Container killed by ApplicationMaster | AM主动杀了container | AM黑名单、任务失败策略 |
Exit code: 137 | 容器被NodeManager强杀 | 内存超限(物理内存或虚拟内存) |
Exit code: 143 | 收到SIGTERM正常退出 | 应用被kill、动态回收 |
Connection refused | 端口不可达 | driver端口、防火墙 |
Connection timed out | 网络不可达 | 路由、安全组 |
Failed to register application master | AM与RM握手失败 | RM状态、RM地址、AM配置 |
我见过有人盯着客户端那一行报错,反复调代码重试了三轮,最后才发现其实 application 的 Diagnostics 里明确写着Container killed by NodeManager due to memory limit。日志就在那,只是没人去看。
2.2 理解 YARN 里 application 的“死法”分类
YARN 对 application 的终态就三种:FINISHED(正常完成)、FAILED(失败)、KILLED(被杀死)。这三种状态背后的原因和处理方式完全不同。
FINISHED 状态却报“already ended”,多数情况下是因为 driver 侧的网络问题导致没有及时收到最终结果。FAILED 状态就要去看 AM 的重试次数:如果 AM 失败重试后依然起不来,就会把整个 application 标记为 FAILED。KILLED 则可能是有人手动 kill、队列超时、或者集群资源整理时被干掉。
诊断的第一步,就是先到 YARN UI 上看 application 的State和FinalStatus,这两个字段配合 Diagnostics 能筛掉一大半不相关的原因。比如一个 container 在被杀瞬间,YARN 会留下 exit code 和对应的 system logs,这些才是真正的第一手线索。客户端报错里的“unable to connect to ResourceManager”往往是 second-hand 信息,不要过度解读。
3. 三次典型现场还原:网络、容器被杀、AM失联
3.1 现场一:executor起来了,driver就是连不上
有一次我帮 A 同学排查一个问题。他提交的任务在 YARN 页面上能看到 executor 已经起来了,每个 executor 状态都是 RUNNING,但 application 自己迟迟不进入 RUNNING 状态,客户端过了几分钟就报Yarn application has already ended!。
当时我让他先在输出里找 executor 的反向连接日志。结果发现 executor 一直在重试,报的是java.net.ConnectException: Connection refused,目标地址居然是提交机的一个内网 IP,而那个 IP 上并没有监听对应端口。再查才发现,提交机是公司内网机器,上面有多个网卡,Spark 默认自动选择了其中一个网卡的 IP 作为 driver host,但那个网卡对集群网段不可达。
这个问题的解法很简单,但如果不加,它隔三差五就会复发:
--conf spark.driver.host=192.168.x.x --conf spark.driver.port=22999spark.driver.host强制指定提交机的内网 IP,spark.driver.port固定一个端口,避免每次随机端口导致防火墙策略难配。改了之后再从集群任意一台节点上用nc -zv 192.168.x.x 22999验证一下,能通才算真的解决。
这个现场是 client 模式特有的坑。cluster 模式下 driver 在集群内,天然不会遇到;而 client 模式就是在考验你对网络细节的把控。如果你提交的机器有多个网卡,或者/etc/hosts配置得乱七八糟,这一档事大概率会轮到你去处理。
3.2 现场二:Container被NodeManager强杀,AM连锁失败
另一个更隐蔽的场景:任务跑着跑着,突然 AM 就把整个 application 结束了。从客户端看报错确实是 “killed”,但到底是谁 kill 的?翻开 AM container 的日志,看到一串Container [executor_xxx] is running beyond physical memory limits. Current usage: 10.0 GB of 8 GB physical memory used...,然后就是Container killed by NodeManager。
这里有个很多人没算明白的账:在 Spark on YARN 里,一个 executor container 的实际内存上限是spark.executor.memory + spark.yarn.executor.memoryOverhead,不是只算你传给--executor-memory的那部分。比如说你配了--executor-memory 8g,默认 memoryOverhead 是max(384MB, 0.1 * 8g),差不多 819MB,那么 container 申请的内存就是 8.8GB 左右。如果 NodeManager 单容器最大内存是 8GB,executor 一起就会被杀。
排查这类问题有一个很典型的特征:YARN 页面上能看到 exit code 137,或者 AM Diagnostics 里直接写着Container killed by NodeManager due to memory limit。还有一种更气人的情况,是yarn.nodemanager.vmem-check-enabled和pmem-check-enabled开了虚拟内存检查,executor 进程的虚拟内存超过限制,也被 kill。虚拟内存检查在很多场景下会误伤,尤其 JVM 的 metaspace 和代码缓存很容易撑大虚拟内存地址空间,实际物理内存远没到限制。合理的做法是把物理内存配准,然后关闭或者放宽虚拟内存检查:
<property> <name>yarn.nodemanager.vmem-check-enabled</name> <value>false</value> </property>这个配置建议由基础设施团队统一评估,单独为一个任务去改 NodeManager 全局配置并不合适。
3.3 现场三:AM与ResourceManager失联,重试耗尽
第三种情况迷惑性最强:任务提交后,客户端偶尔能跑一段时间,但会在某个节点突然报Failed to register application master with ResourceManager,紧随其后的就是Yarn application has already ended!。
这通常是 AM 和 RM 之间的心跳断了。RM 是 active 还是 standby 在切换?AM 拿到的 RM 地址是不是已经失效?集群是不是发生过故障转移?在 HA 环境下,RM 切换后旧的 AM 若不感知新 RM 的地址,就会持续注册失败。如果你用的是 YARN 的老版本,还可能踩到 RM 地址配置写死、没有走服务发现的问题。
另外,AM 的堆内存和元空间本身也可能不够用。AM 不只是转发请求,还要维护任务状态、与 driver 通信,当 job 数量非常大时,AM 自身也会成为瓶颈。spark.yarn.am.memory默认只有 512MB 到 768MB,对于大的 application 是偏紧的。如果 AM 反复 OOM,重试几次之后整个 application 就被 YARN 判死,客户端自然收不到成功消息。
所以看到“失联”两个字,不要只盯网络,AM 自身的内存状况也要一起查。AM 日志里的OutOfMemoryError: Metaspace是很常见的元凶,对应加spark.yarn.am.extraJavaOptions=-XX:MaxMetaspaceSize=512m就能缓解。
4. 逐个击破:从driver网络到YARN资源再到AM参数的修改清单
4.1 先锁死driver的本机地址和端口
如果你决定继续用 client 模式跑,那么第一步就是把 driver 的网络身份固定下来,不要让它自己猜。
具体需要设置:
spark.driver.host=172.16.0.10 spark.driver.port=22999 spark.driver.bindAddress=172.16.0.10spark.driver.bindAddress是 driver 进程实际 bind 的地址,默认等于spark.driver.host。在多网卡机器上,只设置 host 可能还是会出现绑定地址不对的问题,所以三个最好一起设置。设完之后,在集群任意一个节点上验证:
nc -zv 172.16.0.10 22999如果端口不通,优先查提交机防火墙、安全组、IDC 网络策略。不要以为能 ssh 就代表所有端口都能通,很多公司的网络策略默认只放行 22 端口,其他端口全挡。固定端口还有一个好处:你可以提前把这条网络策略申请好,而不是让 Spark 每次启动随机选端口,安全团队都没法审批。
用spark-submit --verbose提交一下,确认最终生效的配置里 host 是你指定的那个值。--verbose会打印出完整的 Spark 配置表,比自己对着 git 记录猜要靠谱得多。
4.2 给executor和AM算清楚内存账
内存是最容易被忽略又最能制造“莫名其妙被杀”的环节。上面的现场二已经说过 container 内存的计算方式,这里再给一张实操对照表:
| 项目 | 默认值 | 建议 |
|---|---|---|
spark.executor.memory | 1G | 根据实际数据量调整,不建议直接给到节点全部内存 |
spark.yarn.executor.memoryOverhead | max(384M, 10%) | 大对象多的任务提到 15%~20% |
spark.yarn.am.memory | 512M | 大 application 调到 1G~2G |
spark.yarn.am.memoryOverhead | max(384M, 10%) | AM 内存充足时可不调 |
spark.yarn.am.cores | 1 | 复杂调度逻辑可给 2 |
一个比较稳妥的计算方式是:先看单节点物理内存和yarn.nodemanager.resource.memory-mb,再用 NodeManager 可用内存除以单容器最大限制去定 executor 大小。比如单节点 96GB,给 YARN 留 80GB,每个 executor container 是 8GB + 2GB overhead,那单节点最多放 8 个这样的 container,executor 分配数量最好不要超过节点数乘以 8 再乘上一个余量系数。
还有一点,spark.executor.memory设得太高,会导致单个 executor 总是被随机调到内存紧张的节点上,失败概率反而上升。同样的总内存,拆成更小的 executor 往往更稳定。这不是公式,是经验和概率问题——executor 越大,被 NM 判定超限的窗口就越宽。
4.3 重试参数和失效判定阈值别用默认值硬扛
AM 和 executor 的重试阈值直接决定了 application 的“耐受力”。
默认情况下,spark.yarn.max.executor.failures是 2(如果 executor 数量较多,值会动态变),spark.yarn.am.attempts是 2。如果你的集群节点偶发故障、网络抖动,这些默认值会让任务一次轻微抖动就直接失败。建议:
spark.yarn.max.executor.failures=4 spark.yarn.am.attempts=3 spark.yarn.am.attemptFailuresValidityInterval=1h注意一个副作用:spark.yarn.am.attempts调大以后,AM 重启期间 driver 会一直等待,客户端超时时间如果设置得不够,会先报超时。所以配套还要看spark.network.timeout和spark.yarn.client.connectionRetryTimeout。这几个参数要一起调,不能只加一个。
4.4 提交前的五条黄金验证命令
在最终提交之前,先用这几条命令把基本盘稳住。我每次在新环境操作都会走一遍,大概五分钟能搞定:
yarn application -list看 RM 是否正常,队列里有没有已经积压的任务。
yarn node -list -states RUNNING看节点数量、总资源是不是你预期值。如果节点数量是 0,后面肯定什么任务都起不来。
nc -zv <driver_ip> <driver_port>从集群节点测到提交机的反向连通性。这一步前面强调了,client 模式尤其重要。
hostname -i cat /etc/hosts确认提交机的主机名和 IP 映射没被改乱。很多诡异的“连不上”都是/etc/hosts里配了一个不存在的 hostname。
spark-submit --master yarn --deploy-mode client --class org.apache.spark.examples.SparkPi \ --num-executors 1 --executor-memory 1g --conf spark.driver.host=<你的IP> \ $SPARK_HOME/examples/jars/spark-examples*.jar 10用官方示例跑一个最小任务,确认链路是通的,再上你自己的业务代码。
这些命令都不难,但它们能帮你把“网络问题”“集群问题”“配置问题”几个大类提前筛掉。业务代码的问题不应该靠反复提交去试,那是浪费集群资源。
5. 跑批前我必做的几件事和长期可靠做法
5.1 固定提交脚本,不要每次手动拼参数
我见过太多报错,最终定位到“上一次有人往 spark-defaults.conf 里加了一行全局配置,影响了所有任务”这种原因。比如有人调了spark.dynamicAllocation.enabled=true,又没设置好executorIdleTimeout,导致 executor 频繁回收,任务一长就出状态异常。
靠谱的做法是,每个任务维护一个固定的提交脚本,把关键参数显式写进去,不依赖全局配置。同时用spark-submit --verbose打印最终生效配置,提交时自己对一遍。全局配置只放最通用的内容,业务相关参数全部收敛到任务脚本里。这样就算别人动过全局配置,你也能快速定位差异。
我在脚本里还会加一段日志收集逻辑:任务结束后自动拉取yarn logs保存到本地,按日期归档。这样后续要复盘,不需要再去 YARN 上找已过期的日志,本地直接翻就有。
5.2 监控盯这三个指标
长期维护 Spark on YARN 任务,只靠“出错了再说”会很被动。建议至少盯三个指标:AM 重启次数、executor 失败次数、队列 pending 时间。
AM 重启次数和 executor 失败次数直接反映 application 的稳定性,持续上升就说明集群或配置有问题。队列 pending 时间过长,说明资源和任务量不匹配,你再怎么调 executor 内存都没用。
YARN 的 ResourceManager Web UI 本身就能看这些数据,拿到源码自己改也很容易。不用一上来就上大而全的监控平台,先把这三个指标从日志里拎出来,就已经能覆盖 80% 的“诡异报错”场景。
5.3 一点个人体会
说实话,最初看到Yarn application has already ended!我也慌过。它的英文措辞带一种“你已经无力回天”的语气,但实际上绝大多数情况都是可排查、可恢复的。按我自己的经验分,这类报错里网络占四成、内存配置占三成、AM/RM 侧的问题占两成,真正的代码 bug 反而是少数。
如果你现在正被这个报错卡住,我的建议很简单:先去 YARN UI 看 State、FinalStatus、Diagnostics 三个字段,再拉 AM container 日志,最后再回到 spark-submit 参数上找问题。这套顺序几乎不会走偏。等问题解决之后,再回头想一下是不是可以彻底规避——比如把任务改成 cluster 模式,或者把 driver 端口固定下来,这些一劳永逸的事,越早做越好。