☰
Spark on YARN容器启动失败排查与根因速查
2026/10/24 5:37:35 网站建设 项目流程

1. 这个报错到底卡在哪一层:先把壳子拆开

跑 Spark 任务跑到一半,突然某个 container 挂了,AM 日志里甩出来一句Exit status: 1. Diagnostics: Exception from container-launch.,接着就是Container exited with a non-zero exit code 1和Container killed by the ApplicationMaster。这个场景我见过太多次,第一次遇到的时候我也懵——代码在本地跑得好好的,凭什么换台机器就起不来?后来啃了几次 NodeManager 日志才明白,这句话根本不是"你的 Spark 代码有问题",而是 YARN 在说"我连这个容器都没能拉起来"。

换句话说,Exception from container-launch是容器启动阶段的错误,属于 YARN 自己的地盘,和 Spark 的算子逻辑、数据倾斜、shuffle 策略统统无关。它发生在 NM 收到 RM 的startContainer指令之后、launch_container.sh真正 fork 出 JVM 进程之前或者刚 fork 的那一瞬间。理解这个时间窗口非常关键,因为排查方向会完全不同:你不用去看 Spark UI 里的 stage 进度,也不用怀疑 RDD 血缘,你要看的是那台节点的磁盘、权限、资源限制和运行环境。

这篇内容我打算把这条报错从"看到就头疼"拆到"看到就知道该敲哪条命令"。适合谁看?一类是刚开始接手 Spark on YARN 集群、被这类报错卡住的同学;另一类是已经在运维集群、想把零散经验固化成排查流程的老手。我会把常见根因、定位手法、现场命令、参数估算以及我自己踩过的坑都摊开讲,尽量做到你照着做就能复现定位路径。

1.1 Exit status: 1 是结果,不是原因

很多人一看到Exit status: 1就以为是退出码 1 对应某个具体错误,开始翻 Spark 源码找 exit(1) 的位置。这个思路在容器启动阶段是走偏的。

在 YARN 的容器启动模型里,NM 并不是直接 exec Java 进程,而是由ContainerExecutor生成一个launch_container.sh脚本,再由这个脚本去启动真正的进程。脚本本身可能因为环境变量缺失、目录不可写、shell 解释器路径不对而直接失败,此时返回的就是 1。Exit status: 1只是 Unix 世界里最通用的"失败了但没细分"的约定,它什么都没告诉你。

真正有信息量的是Diagnostics后面的内容以及 NM 日志里紧跟着的堆栈。我在实际排查里总结出一条经验:Exception from container-launch后面如果跟的是org.apache.hadoop.util.Shell$ExitCodeException,八成是 launch 脚本执行出错;如果跟的是java.io.IOException: Cannot run program,那是可执行文件路径或权限问题;如果是Failed to localize,那是资源本地化阶段就挂了。三种情况对应三套完全不同的处理路径,混在一起查只会浪费半天时间。

所以第一步永远是把完整的 diagnostics 段落摘出来,而不是盯着Exit status: 1看。很多同学截图只截了那一行,等于把最有价值的线索扔掉了。

注意:AM 日志里的 diagnostics 通常是截断的,完整堆栈要去 NM 日志里找,或者用yarn logs把该 container 的日志单独拉出来看。别拿半截信息就开始改配置。

1.2 为什么偏偏是"某节点"出问题

标题里"某节点"这三个字其实已经是重要线索。如果是全集群所有节点都起不来容器,那问题大概率在 RM 配置、队列资源或者提交端的 client 配置上;但如果是个别节点,问题几乎必然收敛到那台机器自身的状态。

这个判断逻辑很朴素:YARN 的容器调度是 RM 分配、NM 执行,同一份 jar、同一份配置、同一个用户,在 A 节点能起、在 B 节点起不来,说明 A 和 B 之间存在环境差异。差异可能藏在磁盘、权限、内核参数、JDK 路径、hosts 解析、时间同步这些"平时没人看"的地方。

我一般会先做一件事:把这台问题节点从集群里摘出来,标记为不可调度(yarn rmadmin -refreshNodes配合 exclude 文件),让业务先跑起来,然后再从容地登上去查。这一步看着粗暴,但对线上业务来说是最省心的——排查可以慢慢来,任务不能一直卡着。

摘节点之后,我会横向对比:找一台正常节点和被摘节点,逐个比对ulimit -a、df -h、df -i、mount、java -version、echo $JAVA_HOME、id yarn、ls -l $HADOOP_HOME/bin/container-executor。这套对比做下来,十个问题里有六七个会自己冒头。

1.3 一条容器启动链路上的五个检查点

为了后面讲排查时不至于东一榔头西一棒子,我先把容器启动这条链路按顺序捋一遍。NM 接到startContainer之后大致会经历这些步骤:

  1. 创建容器目录:在yarn.nodemanager.local-dirs和log-dirs下建usercache/<user>/appcache/<appid>/container_<id>这样的目录结构。
  2. 资源本地化:把提交时上传到 HDFS 的 jar、配置文件、分布式缓存文件下载到本地容器目录。
  3. 生成 launch 脚本:写出launch_container.sh和container_tokens,设置环境变量、classpath、命令行参数。
  4. 执行容器:由ContainerExecutor(默认DefaultContainerExecutor,生产环境一般用LinuxContainerExecutor)fork 出进程。
  5. 进程运行:JVM 启动,加载 Spark 的CoarseGrainedExecutorBackend或 AM 的ApplicationMaster。

Exception from container-launch主要覆盖第 3 和第 4 步,个别情况下第 1、2 步的失败也会被包装成这个异常往上抛。把这条链路记牢,排查的时候你知道自己在查第几步,就不会瞎转。

2. 现场证据怎么取:日志和容器目录是主战场

排查这类问题,光靠猜是没用的,必须拿到现场证据。我见过太多人直接上来就改yarn-site.xml,改完重启 NM,问题偶尔消失了就以为修好了,结果过两天换台机器又炸。这种做法本质上是碰运气,因为你根本不知道动的是哪根弦。

正确的顺序是:先取证据,再定位,最后才动手改。

2.1 三份日志各管什么,别翻错本子

一个容器起不来,涉及三份日志,职责分明,翻错了就会得出错误结论。

ResourceManager 日志:主要记录容器分配和 AM 上报的容器状态变化。这里能看到Container ... transitioned from RUNNING to FAILED这类记录,以及 AM 回传的 diagnostics 原文。它告诉你"失败了",但基本不告诉你"为什么"。

NodeManager 日志:这是核心。路径一般在$HADOOP_LOG_DIR/yarn-yarn-nodemanager-<hostname>.log。容器启动失败时,这里会有Container launch failed for container_xxx这样的记录,紧跟着就是 Exception 的完整堆栈,包括是哪一行 shell、哪个文件找不到、哪个权限被拒。这是你花时间最多的地方。

ApplicationMaster 日志:包含 Spark 自己的视角,能看到哪个 executor 失联、重试了几次、最终是否超阈值导致作业失败。它能帮你确认"这个容器是在启动时挂的,还是启动成功后运行中挂的",这个区分很重要——启动时挂是 YARN 环境问题,运行中挂才可能是业务代码问题。

我一般在被摘下来的节点上直接 tail NM 日志,按 container id grep:

grep -n "container_e123_1234567890123_0045_01_000007" \ /var/log/hadoop-yarn/yarn-yarn-nodemanager-$(hostname).log | head -50

注意日志文件名里带 hostname,不同发行版封装后路径可能略有差异,有的在/var/log/hadoop-yarn/,有的在$HADOOP_HOME/logs/,自己确认一下。

2.2 钻进容器目录看现场,比日志更直接

日志是事后记录,容器目录是现场残留。容器失败后,如果 NM 没有开启立即清理(yarn.nodemanager.delete.debug-delay-sec大于 0),目录会保留下来,里面有几个文件值得逐个看:

文件作用关注点
launch_container.sh容器启动脚本环境变量是否正确、路径是否存在、是否被赋了执行权限
container_tokens安全认证令牌权限是否 600、属主是否是运行用户
stderr标准错误真正的报错往往在这里,比如Permission denied
stdout标准输出有时能看到 JVM 启动失败的提示
directory.info目录清单记录本地化了哪些资源

容器目录的位置是${yarn.nodemanager.local-dirs}/usercache/<user>/appcache/<appid>/container_<id>/。日志目录是${yarn.nodemanager.log-dirs}/container_<id>/。两者别搞混,前者是运行时的工作目录,后者是日志输出目录。

我强烈建议把yarn.nodemanager.delete.debug-delay-sec设成 3600 或者更大,尤其是在排障期的集群上。默认值 0 意味着容器一结束 NM 就把目录删干净,你想看现场根本没得看。这个参数对磁盘的额外占用很有限,因为只有失败的容器才值得你多留一会儿。

直接看 stderr 往往是最快的:

cd /data/yarn/local/usercache/xxx/appcache/application_1234567890123_0045/container_e123_1234567890123_0045_01_000007 ls -la tail -100 stderr

很多时候tail stderr一敲,No space left on device或者Too many open files就明晃晃地躺在那儿,比你翻半小时日志强多了。

2.3 用 yarn logs 快速拉取,不用登机器

如果容器目录已经被清理了,或者你手里只有 application id,那就用yarn logs:

yarn logs -applicationId application_1234567890123_0045 \ -containerId container_e123_1234567890123_0045_01_000007

这个命令的前提是日志聚合(yarn.log-aggregation-enable=true)开着,否则会提示日志未聚合。聚合没开也问题不大,至少能把 AM 的日志拉下来,先确定是哪个 container、哪台节点。

还有一个容易被忽略的点:yarn logs默认输出可能很长,建议接grep或者重定向到文件再看:

yarn logs -applicationId application_1234567890123_0045 > /tmp/app_0045.log 2>&1 grep -n -A 30 "Exception from container-launch" /tmp/app_0045.log

我习惯先定位到具体的那台节点,然后直接登上去看 NM 日志,因为聚合日志有时会因为聚合延迟或者磁盘满而丢片段,而 NM 本地日志是最原始、最不会骗人的。

3. 七类高频根因,逐个拆开看

这一章是我这些年实际处理过的问题的归纳。我不敢说穷尽,但按经验,Exception from container-launch背后八成的场景都能归到这七类里面。

3.1 磁盘写满或者 inode 耗尽

这是最常见、最没技术含量但也最容易反复出现的原因。表现是容器目录建不出来,或者本地化资源下不下来,异常信息里带No space left on device或者Failed to create directory。

要分两种情况看:空间满和inode 满。很多人只查df -h,看到用了 60% 就以为没事,结果df -i一看 inode 已经 100%。YARN 的本地目录里会产生大量小文件(每个容器好几个,还包括下载的 jar 分片),inode 消耗速度比想象中快得多。

df -h /data/yarn/local /data/yarn/log df -i /data/yarn/local /data/yarn/log

关于容量估算,我给个粗略的算法。单个容器在local-dirs下的占用 = 本地化资源大小 + shuffle 临时数据 + 运行期临时文件。如果把 executor 内存配成 8G,shuffle 阶段中间数据能到几个 G,保守按每个容器 3~5G 算;单节点跑 20 个容器,就是 60~100G 的瞬时峰值。所以local-dirs所在分区,我一般会要求至少留够单节点最大容器数 × 5G的空间,而且要保留 30% 以上的余量做缓冲。

另一个常被忽略的点是log-dirs。日志分区写满同样会导致容器启动失败,因为 NM 要在那里建container_<id>目录写 stdout/stderr。日志轮转策略没配好,几个月就能把盘吃满。

提示:磁盘问题不要只用df -h判断,务必配合df -i。我遇到过两次线上事故,都是空间富裕但 inode 打满,排查时间全浪费在错误方向上。

3.2 container-executor 权限与 setuid 位

生产环境的 NM 一般用LinuxContainerExecutor来隔离用户,这要求$HADOOP_HOME/bin/container-executor这个二进制文件有严格的权限和属主:

ls -l $HADOOP_HOME/bin/container-executor

正确的结果长这样:

-rwsr-s--x 1 root yarn 123456 /opt/hadoop/bin/container-executor

关键在rwsr-s:它有 setuid 和 setgid 位,属主是 root,属组是 yarn(或者 hadoop)。如果哪位同事在部署时用了cp -r或者rsync传输安装包,setuid 位会被抹掉,变成普通的-rwxr-x--x,那么容器启动就会失败,报的正是Exception from container-launch。

更麻烦的是这种行为是节点级的:如果是批量部署脚本漏了这一步,可能导致部分节点正常、部分节点失败,正好对应标题里"某节点"的现象。

修复很简单但必须谨慎:

chown root:yarn $HADOOP_HOME/bin/container-executor chmod 6050 $HADOOP_HOME/bin/container-executor

6050这个数字拆开看:6是属主 root 的读写,0是属组无权限,5是其他用户可读可执行,最后一位0是 setuid/setgid 之外的粘滞位设置位——实际上6050的前导 6 已经包含了 setuid 和 setgid 的语义(在 chmod 数字表示里,setuid 是 4、setgid 是 2,作用在属主/属组位上,所以完整写法是6050,其中第二位 0 对应属组权限为空但 setgid 已置)。这个权限值在官方文档里有明确要求,改错了 NM 重启直接起不来,所以动手前务必确认。

另外yarn.nodemanager.container-executor.class这个配置要和你实际用的执行器对上。如果配成LinuxContainerExecutor但权限没设对,或者反过来配了DefaultContainerExecutor但目录权限没放开,都会在这条链路上翻车。

3.3 ulimit 与进程数限制

这个原因隐蔽性很强,因为报错信息往往不直接说"限制",而是表现为Resource temporarily unavailable、unable to create new native thread或者干脆没有详细报错,就是一个 exit 1。

YARN 容器里跑的 JVM 需要打开大量文件描述符,Spark 的 executor 尤其铺张——每个 shuffle 分区的读写、每个网络连接、每个本地文件句柄都要占一个 fd。如果节点上nofile限制太小,容器启动到一半就挂了。

ulimit -a cat /etc/security/limits.conf

我一般要求nofile至少 655360,nproc至少 65536。为什么是这个量级?算笔账:单节点假设跑 50 个容器,每个 JVM 打开 300~500 个 fd(Spark 在高并发 shuffle 时很容易到这个数),就是 15000~25000 个;再加上 NM 自身、DataNode、其他常驻服务,65536 被打满不算夸张。留一倍余量,655360 比较稳妥。

要特别注意limits.conf里的配置对谁生效。yarn用户的nofile和 root 的不一样,而容器里跑的是提交任务的业务用户,所以那个用户也得配。更隐蔽的是 systemd 托管的服务:/etc/systemd/system/yarn-nodemanager.service里如果没有LimitNOFILE=655360,那limits.conf根本不生效,因为 systemd 不读这个文件。这一条我踩过,改完limits.conf重启服务发现ulimit -n还是 1024,查了半天才反应过来是 systemd 覆盖了。

3.4 本地化资源失败

Spark 提交时会把 jar 和配置文件放到 HDFS 的 staging 目录,NM 启动容器前要先拉下来。如果这个拉取过程失败,同样会抛容器启动异常。

本地化失败的原因主要有几类:HDFS 上文件权限不对(业务用户读不到)、NameNode 连接超时、本地目标目录不可写、下载后校验失败。

定位方法是看 NM 日志里有没有Failed to localize或者Resource ... not found这样的关键字,同时检查directory.info里列出的资源清单和实际下载下来的文件是否对得上。

一个很典型但容易被忽略的场景是:业务用户在 HDFS 上的 staging 目录属主被改了,或者权限从 755 变成了 700,导致 NM 以yarn身份去拉取时读不到。这种问题往往发生在有人手工整理过 HDFS 目录之后,属于人为事故,但排查时很难第一时间想到。

3.5 JAVA_HOME 与环境变量不一致

这个问题在混合部署的集群里特别常见。NM 启动时读的是hadoop-env.sh里的JAVA_HOME,而容器里继承的环境变量可能来自另一份配置或者 shell 的默认值,两边对不上,launch 脚本里的 java 命令就找不到了。

表现是 stderr 里出现/bin/java: No such file or directory或者JAVA_HOME is not set之类的提示。

排查路径:

# 看 NM 进程实际用的环境 ps -ef | grep nodemanager cat /proc/<pid>/environ | tr '\0' '\n' | grep -i java # 对比配置文件 grep -n "JAVA_HOME" $HADOOP_HOME/etc/hadoop/hadoop-env.sh which java readlink -f $(which java)

我建议的做法是在hadoop-env.sh里写绝对路径,别依赖 PATH。比如:

export JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64

同时确认这台节点上这个路径真实存在,而且 JDK 大版本和集群其他节点一致。我遇到过一台机器被人手工升级成了更高版本的 JDK,导致 Spark 的某些类加载失败,报错最后也是包装成容器启动失败,排查绕了好大一圈。

3.6 内存与虚拟内存超配

内存问题导致的失败,严格来说有时候发生在容器启动后不久,但因为进程几乎没干活就被 kill 了,表象上和启动失败很像。

要区分两个检查维度:物理内存和虚拟内存。物理内存由yarn.nodemanager.resource.memory-mb控制,RM 分配时会检查,超了直接分配不到,不会到 launch 这一步。而虚拟内存检查由yarn.nodemanager.vmem-check-enabled控制,默认开启,JVM 启动时预留的虚拟地址空间很容易超过物理内存的 2.1 倍,于是被 NM 判定超限并 kill。

日志里会明确写:

Container [pid=12345,containerID=container_xxx] is running beyond virtual memory limits.

如果你确定业务没有内存泄漏,只是 JVM 预留地址空间大,可以考虑关掉虚拟内存检查(yarn.nodemanager.vmem-check-enabled=false),但这是治标的手段,会掩盖真正的内存问题,我不建议在核心集群上长期这么干。更稳妥的是调大yarn.nodemanager.vmem-pmem-ratio,从默认 2.1 调到 3 或者 4。

还有个容易忽略的点:Spark 的spark.executor.memoryOverhead不够时,executor 起来没多久就被 YARN 干掉。这个值默认是max(384, 0.1 * executorMemory),在堆外内存使用多的场景下(比如大量网络传输、Python UDF)明显偏小,我一般会显式设到 10%~15% 甚至更高。

3.7 时钟漂移与主机名解析

这两个原因放在一起讲,因为它们都属于"基础设施没对齐"这一类。

时钟漂移:YARN 的令牌和 Kerberos 认证对时间敏感,容器节点和 RM 的时间差超过阈值(一般 30 秒以上),认证就会失败。表现往往是"在 A 节点能起,在 B 节点起不来",而且重启一下有时候又能起来,极具迷惑性。

date timedatectl status chronyc tracking # 或者 ntpq -p

我习惯在所有节点上把时间同步服务纳入监控告警,偏差超过 5 秒就报出来。

主机名解析:NM 注册时用 hostname,容器内部通信时也会用 hostname。如果/etc/hosts里没有本机 hostname 对应的记录,或者 DNS 解析结果和实际 IP 不一致,就会导致各种奇怪的超时和启动失败。

hostname hostname -f cat /etc/hosts getent hosts $(hostname)

我见过最典型的情况是新扩容的机器 hostname 写进了 DNS 但没同步到/etc/hosts,hostname -f返回的是一串 IP 或者报错,NM 虽然能起来但容器内通信异常。这种问题在纯 IPv4 环境里相对少,但一旦引入 IPv6 或者多网卡就很容易出。

4. 一次完整的排查推演:从报错到定位

光列原因不够,我把一次典型排查的过程还原出来,你可以对照自己的场景套用。

4.1 第一步:确认范围,先把业务救出来

任务报错后,第一件事不是登机器,而是确认失败范围。看 AM 日志里失败的 container 落在哪几台节点上。如果集中在同一台,直接走"摘节点"流程;如果散落在多台,就得考虑是不是提交端配置或者集群级问题。

摘节点的操作:

# 在 exclude 列表里加上问题节点 echo "worker-017" >> $HADOOP_HOME/etc/hadoop/exclude-nodes # 刷新 yarn rmadmin -refreshNodes # 确认状态 yarn node -list -states UNHEALTHY,DECOMMISSIONED

节点摘掉之后,已经在跑的容器会被迁移,新容器不会再分配上去。业务恢复之后,再从容排查。

4.2 第二步:拿到完整堆栈,锁定失败阶段

我习惯先在 NM 日志里把所有相关行捞出来:

grep -n -B 5 -A 40 "container_e123_1234567890123_0045_01_000007" \ /var/log/hadoop-yarn/yarn-yarn-nodemanager-worker-017.log > /tmp/nm_017.txt

然后重点看三个东西:异常类型、异常消息、以及launch_container.sh是否生成成功。

如果launch_container.sh根本没生成,问题在本地化或者目录创建阶段,去查磁盘和权限。 如果生成了但执行失败,去看stderr里的具体内容。 如果连容器目录都没建出来,那更靠前,去查local-dirs是不是存在、属主对不对。

4.3 第三步:现场体检,跑一套固定动作

在问题节点上,我有一套固定的体检命令,基本能覆盖七成场景:

# 磁盘与 inode df -h /data/yarn/local /data/yarn/log df -i /data/yarn/local /data/yarn/log # 目录权限 ls -ld /data/yarn/local /data/yarn/log ls -ld /data/yarn/local/usercache/* # 执行器权限 ls -l $HADOOP_HOME/bin/container-executor # 资源限制 ulimit -a cat /proc/$(pgrep -f NodeManager)/limits # 环境 echo $JAVA_HOME java -version hostname -f date

这套跑下来,输出贴在一起对比正常节点,差异往往一眼可见。我印象最深的一次,就是df -i显示 inode 使用率 100%,而df -h只显示用了 40%,前后找了两个多小时才想起来查 inode。

4.4 第四步:修复、灰度验证、回归

定位到根因之后,修复动作要小步走。改完先在这台节点上手动触发一个小任务验证,别直接放量:

spark-submit --master yarn --deploy-mode cluster \ --num-executors 1 --executor-cores 1 --executor-memory 1g \ --class org.apache.spark.examples.SparkPi \ $SPARK_HOME/examples/jars/spark-examples_*.jar 10

小任务能跑通,再把这台节点重新纳入调度(从 exclude 列表移除并refreshNodes),然后观察一轮完整业务批次的成功率。这一步别省,我见过改完参数小任务能跑、大批量一上来又挂的情况,原因是小任务并发少,没触发资源限制的边界。

5. 速查表与长期预防:把经验变成机制

排查能力再强,也不如让问题少发生。这一章我把常见现象、可能原因、验证命令整理成一张表,再聊聊怎么做预防性巡检。

5.1 常见问题速查表

现象可能原因验证命令处理方向
异常含No space left on device磁盘或 inode 满df -h、df -i清理旧容器目录、扩容、加日志轮转
异常含Permission denied且指向 container-executorsetuid 位丢失ls -l $HADOOP_HOME/bin/container-executorchmod 6050、chown root:yarn
stderr 含Too many open filesnofile 限制过小ulimit -a、cat /proc/<pid>/limits调大 limits、systemd 加 LimitNOFILE
异常含Failed to localizeHDFS 权限或网络检查 staging 目录权限修权限、检查 NameNode 连通性
stderr 含JAVA_HOME is not set环境变量不一致对比hadoop-env.sh与进程环境统一写绝对路径
日志含running beyond virtual memory limitsvmem 检查过严看 NM 日志关键字调 vmem-pmem-ratio 或加 overhead
任务时好时坏,集中在某几台时钟漂移date、chronyc tracking修复时间同步
容器内通信超时hostname 解析异常hostname -f、getent hosts修/etc/hosts与 DNS

这张表我打印出来贴在工位上过,新人上手时按表走,能省掉大量"从零开始想"的时间。当然表是死的,实际场景往往多个原因叠加,比如磁盘满导致本地化失败,本地化失败又触发了 launch 异常,你得顺着链路往上游找。

5.2 节点巡检脚本思路

人工体检适合排障,不适合预防。我一般会给集群配一个轻量巡检,定时跑,把异常节点提前揪出来。思路不复杂,核心就几项检查:

#!/bin/bash # 简化的节点健康巡检片段 LOCAL_DIRS="/data/yarn/local /data/yarn/log" ALERT=0 for d in $LOCAL_DIRS; do use=$(df -h "$d" | awk 'NR==2 {gsub("%","",$5); print $5}') iuse=$(df -i "$d" | awk 'NR==2 {gsub("%","",$5); print $5}') [ "$use" -gt 85 ] && { echo "SPACE $d ${use}%"; ALERT=1; } [ "$iuse" -gt 85 ] && { echo "INODE $d ${iuse}%"; ALERT=1; } done nofile=$(ulimit -n) [ "$nofile" -lt 65536 ] && { echo "NOFILE $nofile"; ALERT=1; } perm=$(stat -c "%a %U %G" "$HADOOP_HOME/bin/container-executor") [ "$perm" != "6050 root yarn" ] && { echo "EXECUTOR_PERM $perm"; ALERT=1; } skew=$(chronyc tracking 2>/dev/null | awk '/System time/ {print $4}') # 偏差超过 5 秒告警 exit $ALERT

这段脚本很粗糙,但骨架是对的。实际部署时我会把它接到监控系统里,让告警带上节点名和具体指标,值班的人一看就知道该登哪台、查什么。

巡检的频率我一般设成 10 分钟一次,太频繁会浪费资源,太稀疏又起不到预防作用。阈值也要结合集群实际情况调,85% 对高写入的集群可能偏低,会频繁误报,可以放到 90%,但 inode 我建议始终卡在 80% 以下告警,因为 inode 增长往往是非线性的,一旦开始涨就很快见底。

5.3 几个我踩过的坑,写出来给后来人

第一个坑:只看df -h不看df -i。前面提过,但还是值得再说。小文件多的集群,inode 是最先耗尽的资源,而它偏偏是最不直观的。现在我登任何一台节点,df -h和df -i都是连着敲的,形成肌肉记忆。

第二个坑:改完limits.conf忘了 systemd。这个坑让我在一个节点上折腾了整整一个下午。改完配置,su - yarn -c "ulimit -n"显示 655360,一切正常,但 NM 重启后容器还是报Too many open files。后来才想明白,systemd 启的服务读的是 unit 文件里的LimitNOFILE,跟limits.conf没关系。正确做法是在 unit 文件里加:

[Service] LimitNOFILE=655360 LimitNPROC=65536

然后用systemctl daemon-reload && systemctl restart重启生效。

第三个坑:容器目录被秒删,现场没了。默认yarn.nodemanager.delete.debug-delay-sec=0,容器一失败 NM 立刻清目录。我第一次排查这类问题时,日志还没看完,现场已经没了,只能靠重复提交任务来复现,效率极低。后来我把这个参数调到 3600,排障体验天差地别。磁盘代价很小,因为只有真正失败的容器才会留下来,跑得正常的照样被清。

第四个坑:误以为是代码问题,去改 Spark 参数。容器启动阶段失败,改spark.executor.memory、调并行度、加spark.sql.shuffle.partitions全是白费功夫。判断方法很简单:看失败发生在任务开始跑之前还是之后。打开 Spark UI,如果 stage 从来没启动过、executor 列表是空的,那百分之百是 YARN 层的问题,别在业务参数上打转。

第五个坑:修完不验证就放量。前面也提了,小任务能跑不等于大批量能跑。并发一上来,资源限制、磁盘写入、网络连接数这些边界条件才会暴露。我现在修完任何一个节点,都会先放一个中等规模的任务跑一轮完整批次,观察一两个小时的资源曲线,确认稳定了才重新纳入调度。

回到这条报错本身,它其实是个很"友好"的错误——至少它明确告诉你了失败发生在容器启动阶段,把问题范围从"整个 Spark 作业"缩小到了"一台节点的运行环境"。剩下的工作就是把那台节点的磁盘、权限、限制、环境这四样东西逐一比对排除。这个过程枯燥但确定性强,不需要多高深的技术,需要的是耐心和一套固定的检查顺序。我自己是从一次次"改了半天发现方向错了"里磨出这套顺序的,希望你看完能少走几个弯。

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

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

立即咨询