CPU 打满 99% 却不告警:node_exporter 过载失联(up==0)导致阈值告警静默失明
CPU 打满 99% 却不告警:node_exporter 过载失联(up==0)导致阈值告警静默失明
以下涉及的主机名、IP、域名等均已替换为占位值,照搬到你自己的环境时换成实际值即可。
先把结论摆前面:基于阈值的告警,隐含了一个你没意识到的前提——“数据还在”。 一旦机器过载到把采集器(node_exporter)自己也拖死,指标会直接从时间序列里消失;这时 > 85% 这类规则不是“没达到阈值”,而是根本没有数据可判,于是它静默地什么都不报。结果就是:一台 CPU 跑满 99%、持续一小时四十分钟的机器,全程零告警。
这次排查里,真正咬死问题的不是某条 PromQL,而是拿一个不依赖被监控方的数据源去交叉核对。下面是完整过程,包括我中途两个被数据直接推翻的判断。
一、现象:node_exporter 说这台机器常年很闲
起因是有人问:app-server-01 这台机器今早 CPU 是不是有问题,怎么没收到告警?
第一反应是去监控里看它的 CPU 曲线。node_exporter 采集、按惯用公式算使用率:
(1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle", instance="192.168.1.1:9100"}[5m]))) * 100
拉近 24 小时,曲线很干净:基线常年 3%~4%,最高一个尖峰也才 38%,没有任何一刻接近 85% 的告警线。看起来这台机器闲得很,没告警完全正常。
但提问的人很确定今早它“卡过”。现象和数据对不上——矛盾就此开始。
二、交叉核对:换一个数据源,CPU 其实钉死在 99.8%
这一步是整件事的转折:node_exporter 是装在机器内部的 agent,如果机器本身出问题,它的数据就不可信。 必须找一个在机器外部、不依赖 guest 存活的数据源来对照——云厂商的 agentless 云监控(从宿主机/虚拟化层采集 CPU)正好合适。
拿同一台机器、同一时间窗去查云监控的 CPU:
07:05 3% → 开始爬升
07:25 64%
07:35 95%
07:40 98.9%
07:45–09:15 99.8% ← 钉死,持续约 1 小时 40 分
09:20 67% → 回落
09:25 4% → 恢复正常
云监控看到的是:CPU 从 07:35 起被打满 ~99.8%,一直到 09:15,约 1 小时 40 分钟。 而 node_exporter 那条“常年 3%”的曲线,在这段时间里根本不是低,而是没有数据——我被“曲线很平”骗了,平是因为没点。
两个数据源同一台机器同一时刻,一个说 3%、一个说 99.8%。不是某一方错了,而是它们看的根本不是同一层。
三、第一个被推翻的判断:以为是采集延迟
我最初猜 node_exporter 那段缺数据是“上报延迟/积压”。去查它的 up 指标——Prometheus/VM 对每个抓取目标自动打的健康标记,抓得到=1,抓不到=0:
up{instance="192.168.1.1:9100"}
结果不是延迟,是彻底失联:从 07:02 起,这台机器的 up 连续 40 分钟+ 全是 0。机器被 CPU 打满到连 node_exporter 的 HTTP 抓取都响应不了——采集器自己被饿死了。
这就解释了第二节那条“假的低 CPU 曲线”:不是机器闲,是 exporter 在过载时段压根没产出数据,时间序列出现空洞,画出来就成了一条人畜无害的平线。
四、根因:阈值规则在“无数据”时静默
到这里核心机制就清楚了。那条高 CPU 告警规则长这样:
(1 - avg by (instance) (rate(node_cpu_seconds_total{mode="idle"}[2m]))) * 100 > 85
它依赖 node_cpu_seconds_total。而在过载时段,这台机器的 node_cpu_seconds_total 和 up 一起消失了——没有新样本,rate() 算不出值,整条表达式对这个 instance 求值为“无数据”。
关键点:阈值告警的触发条件是“表达式有结果且越线”。当表达式连结果都没有时,它既不触发、也不报错,就是静默。 这台机器最需要告警的 1 小时 40 分钟,恰恰是它从监控视野里消失的 1 小时 40 分钟。
一句话:
> 85%永远等不到,因为越是过载、数据越缺;阈值规则天生看不见“把自己采集器拖死”的那种故障。
那它最后怎么“恢复”的?09:18 这台机器重启了(大概率是 OOM/卡死后的自愈或人工重启),CPU 随之回落。当天这台机器唯一触发的告警,是事后一条“机器发生重启”的事件——也就是说,系统是在事故结束后,才间接告诉你“刚才出过事”。
五、第二个被推翻的判断:以为兜底规则在跑
“高 CPU 看不见可以理解,但机器掉线(up==0)总该有兜底告警吧?” 我去翻,果然有一条全局规则:
up{job="node", env!="test"} == 0
覆盖两百多台、包含这台机器,按理 up 连续 0 了 40 分钟一定会响。可当天事件里它一条都没有。
查规则状态——它被禁用了。某次清理时有人把它关掉了(多半是因为有几台长期下线的“僵尸目标”一直刷屏),从此这条唯一的全局掉线兜底就失效了。替代它的是几条按业务线拆分的小规则,而这台机器的角色恰好落在所有小规则的覆盖缝隙里。
于是闭环坐实:高 CPU 告警因“无数据”静默 + 掉线兜底被禁用 → 一台跑满 99% 一个多小时的生产机,零告警。
六、修复:补“数据消失即报”的兜底
阈值告警的盲区,不能用另一条阈值告警去补——因为它们都依赖“数据在”。要补的是一类“指标消失本身就是异常”的规则:
1. 重新启用并收敛 up==0 兜底。 它不依赖 CPU/内存数值,只看“这个目标还在不在被抓到”。过载、宕机、断网、exporter 崩溃,全都会让 up 变 0:
up{job="node", env!="test"} == 0
启用前先把那几台长期 up==0 的僵尸目标处理掉(要么从抓取目标里摘除,要么在选择器里 insname!~"..." 排除),否则一开就刷屏——这正是它当初被禁用的原因。
2. 用 absent() 兜更彻底的“整类指标消失”。 当某 job 的所有目标都没了,up 连序列都不存在,这时 absent() 才报得出来:
absent(up{job="node"})
3. agentless 与 agent 两条腿。 像这次,最终是云厂商的宿主机侧 CPU 指标(不依赖 guest 存活)才看清真相。关键机器的 CPU/内存,值得在 agent 采集之外,再叠一层 agentless 云监控侧的同类规则——当 exporter 被拖死时,它是你唯一还睁着的眼睛。
七、复盘:这次真正的收获
技术根因是“无数据即静默”,但更值得记的是方法上的两条:
- 告警没响 ≠ 系统没事。 阈值告警隐含“数据在”的前提;遇到“某机器明明出过事却零告警”,先怀疑数据在事发期间是不是根本没采到,而不是默认“没告警就没越线”。
- 判断机器真实状态,要用不依赖它自己的数据源。 node_exporter 装在 guest 内,机器一挂它的数据就不可信。宿主机侧/云监控这种 agentless 视角,才是“机器自己说自己没事”时的裁判。
- 能自圆其说的判断也要验证。 我两次被打脸——先猜“采集延迟”(其实是彻底失联)、再猜“兜底规则在跑”(其实被禁用了),都是去查了真实数据/真实配置才否掉的。判断的价值在于被证实或推翻,不在于听起来合理。
快速参考
排查“高负载却不告警”的通用步骤:
- 别信单一 agent 数据源。
node_exporter曲线“很平/很低”时,先确认是真的低,还是没数据。 - 查
up{instance="..."}在事发时段是不是 0——过载/宕机会让 agent 失联,曲线空洞画出来像平线。 - 拿 agentless 数据源(云厂商宿主机侧 CPU/内存)交叉核对真实使用率。
- 检查阈值规则依赖的指标(如
node_cpu_seconds_total)在事发期间是否缺失——缺失则规则“无数据静默”,不会触发。 - 确认
up==0/absent()兜底规则存在且启用。
“数据消失即报”的兜底规则写法:
# 单个目标掉线(过载/宕机/断网/exporter崩溃都会触发)
up{job="node", env!="test"} == 0
# 整类指标消失(所有目标都没了,up 序列都不存在)
absent(up{job="node"})
# 关键机器:再叠一层 agentless 云监控侧 CPU 规则(不依赖 guest 存活)
设计铁律:
- 阈值告警(
>/<)天生看不见“把采集器自己拖死”的故障,必须搭配“指标消失即报”的兜底(up==0/absent())。 - 兜底规则不要轻易禁用;要禁用前先治理噪音源(清理僵尸抓取目标),别因为几台脏数据把整条全局兜底关掉。
- 关键机器的核心指标,agent 采集之外叠一层 agentless 云监控——两条腿走路,过载时才不至于全盲。
- “告警时间/告警有无”都不等于事故真相,以不依赖被监控方的可信数据源为准。

浙公网安备 33010602011771号