☰
Spark on YARN报错‘Yarn application has already ended’排查与解决方案
2026/10/13 4:08:12 网站建设 项目流程

你有没有在凌晨的跑批窗口里,盯着终端猛然刷出一行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 模式跑任务时,整个链路是这样的:

  1. 你在提交机上执行spark-submit --master yarn --deploy-mode client,提交机上的 JVM 会先启动 driver。
  2. driver 创建 SparkContext,通过 YarnClient 向 ResourceManager 提交 application,并等待 AM 注册回来。
  3. ResourceManager 在某个 NodeManager 上为 AM 分配 container 并启动它。
  4. AM 启动后,再向 ResourceManager 申请 executor 的 container,拉起一个或多个 executor。
  5. 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 ApplicationMasterAM主动杀了containerAM黑名单、任务失败策略
Exit code: 137容器被NodeManager强杀内存超限(物理内存或虚拟内存)
Exit code: 143收到SIGTERM正常退出应用被kill、动态回收
Connection refused端口不可达driver端口、防火墙
Connection timed out网络不可达路由、安全组
Failed to register application masterAM与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=22999

spark.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.10

spark.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.memory1G根据实际数据量调整,不建议直接给到节点全部内存
spark.yarn.executor.memoryOverheadmax(384M, 10%)大对象多的任务提到 15%~20%
spark.yarn.am.memory512M大 application 调到 1G~2G
spark.yarn.am.memoryOverheadmax(384M, 10%)AM 内存充足时可不调
spark.yarn.am.cores1复杂调度逻辑可给 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 端口固定下来,这些一劳永逸的事,越早做越好。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询