做监控的同学几乎都有过这种感觉:node_exporter的指标铺天盖地,node_cpu_seconds_total、node_memory_MemAvailable_bytes、node_filesystem_avail_bytes……每一项看起来都可以用,可真到要把它们变成生产环境里能信得过的告警规则,门道比想象中多得多。我从裸奔式“只挂个 node_exporter 看面板”,到后面把告警规则拆了重写三轮,前后踩了不少坑。这篇就聊聊,怎么从 node_exporter 的海量指标里提炼出真正能救命的几条规则,以及规则上线之后,怎么避免自己被误报短信打成筛子。
这套思路不挑监控栈。只要你用的是 Prometheus 生态,不管告警最后是进钉钉、企业微信还是邮件,骨架都能直接用。
1. 告警规则设计的第一步:先想清楚什么值得告警
1.1 告警的本质是“决策”,不是“通知”
很多团队把告警规则写成了“指标变化通知”:CPU 高一点就报,内存涨一点就报,磁盘多写几 MB 也报。结果 Grafana 面板上的确花花绿绿,但值班同学点开一看全是“高负载”,根本没有动作可以执行。告警规则真正的价值在于触发人的决策:这条告警出现之后,值班同学需要知道现在系统处于什么状态、影响范围多大、下一步应该看哪块数据、找哪个团队。如果你设计的规则触发后,人只能“哦”一声然后关掉,那这不是告警,是弹窗广告。
生产级规则的判断标准不是“能不能识别故障”,而是“能不能在正确的时间,把正确的信息推给正确的人”。所以从 node_exporter 原始指标出发之前,先要从这个目标倒推,每一类指标能告诉你的状态是什么,失去这个状态时谁会受影响。否则后面写出来的规则,基本都是给自己添堵。
1.2 三类必须淘汰的规则
回顾我见过和写过的告警规则,能稳定制造噪声的基本就三类。
第一类叫“无阈值的状态广播”。典型写法是node_boot_time_seconds > 0或者“进程存在性告警”,这类规则绝大多数时间恒为 true,对人没有任何新鲜信息量。它唯一的作用是让你在告警疲劳之后,把真正重要的消息一起忽略掉。
第二类是“单点指标恐慌”。只看单个瞬间值、不看趋势和持续时间。一个node_load1 > 8的瞬时表达式,在定时任务集中跑批的凌晨几乎天天误报,但机器其实没有健康问题,只是负载在几秒内冲高然后回落,这类误报会把运维的注意力锁死在无意义问题上。
第三类是“把平均值当真相”。avg(node_cpu_seconds_total)这种写法,在多核机器上常常把一个核跑满的真实故障摊平成“整体还行”。CPU 告警的正确姿势是按mode="user"或mode="system"拆分,看单核最大值与多核均值的偏离度。
1.3 给 node_exporter 指标做“可告警性”分级
我每接入一台新机器,会先把指标分成三个层级,再决定要不要写规则。
第一层是“必须告警的硬指标”。比如内存耗尽、磁盘只读、根分区快满、时钟跳变、node_exporter 自身抓取失败。这类指标一旦异常,系统通常已经处于不可用边缘,需要分钟级介入。
第二层是“趋势告警的软指标”。比如 CPU 单核长时间 user 占用超过 90%、可用内存持续下降、磁盘 IO 等待时间持续走高。单独看某一条可能不致命,但如果持续时间拉长,意味着容量规划需要关注了。
第三层是“不写规则的参考指标”。比如网络流量瞬时峰值、文件描述符当前数量。它们更适合放面板做趋势观察,价值在于“事后复盘”,而不在于“实时打断人”。
分完级你会发现,真正需要写告警规则的 node_exporter 指标可能只有十几条。这个数量是健康的。告警规则不是展览品,写得越多,平均单条可信度越低。
2. 核心指标解构与阈值设计:从原始 metric 到告警表达
2.1 内存指标:千万别用 MemFree,用 MemAvailable
内存告警是初学者最容易写错的第一道规则。直接写(node_memory_MemTotal_bytes - node_memory_MemFree_bytes) / node_memory_MemTotal_bytes * 100算出“内存使用率 95%”,然后就报警,这是典型的 Linux 内存模型知识缺位。
MemFree只统计了完全空闲的物理内存页,而 Linux 内核的 page cache、文件系统缓存、内核 slab 都会占用内存,但这些内存大多可以在业务需要时被回收。你看一台刚启动完数据库的机器,MemFree往往只有几百 MB,但系统跑得毫无压力,因为页缓存把磁盘读过的数据都留着,随时可以让出来。真正反映“给应用还能剩多少内存”的指标是node_memory_MemAvailable_bytes,它已经把可回收缓存算进去了。所以我的基础规则这样写:
( node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes ) / node_memory_MemTotal_bytes * 100 > 90这只是半边。另外半边要关注剩余内存绝对值。对一台 64GB 的机器,剩 5% 可能还有 3GB,业务不一定马上挂;但对 8GB 的小机器,剩 5% 就是 400MB,GC 和 OOM 可能就在几分钟内接踵而至。生产经验是:比例阈值和绝对值阈值同时保留,任何一个触发都值得看一眼。举个例子:
( 100 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes * 100) ) > 90 unless node_memory_MemAvailable_bytes > 2 * 1024 * 1024 * 1024这条表达式的意思是“总内存使用率超过 90%,但可用内存绝对值大于 2GB 时不算告警”。用unless把“大内存机器上的钝感”显式表达出来,误报率会低很多。
2.2 磁盘指标:挂载点过滤与容量阈值
磁盘容量规则看起来简单,写起来全是坑。node_filesystem_avail_bytes会把挂载点下面所有文件系统都列出来,包括tmpfs、overlay、squashfs这些,如果不加过滤,你会在容器节点上收到一堆/var/lib/docker/overlay2/xxx的告警——这些挂载点的空间其实共享同一个宿主机磁盘,发出 N 条重复告警毫无意义,属于典型的“看起来精确、实际没信息量”。
我一般用一组取值白名单约束设备类型和挂载点。生产规则通常是:
( node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs|ramfs"} - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay|squashfs|ramfs"} ) / node_filesystem_size_bytes{fstype!~"tmpfs|overlay|squashfs|ramfs"} * 100 > 85但这只是第一层。真正生产环境里,容量的阈值要按分区职能分开:根分区/是系统盘,一般只跑系统日志和安装包,85% 就算高;/data是业务数据盘,如果业务有自动清理机制,95% 依然可控。所以我更推荐把挂载点写进 rule group 的标签里,而不是用一条大而全的表达式套所有机器。例如:
- alert: RootFilesystemSpaceLow expr: | node_filesystem_avail_bytes{mountpoint="/", fstype!~"tmpfs|overlay|squashfs|ramfs"} / node_filesystem_size_bytes{mountpoint="/", fstype!~"tmpfs|overlay|squashfs|ramfs"} * 100 < 10 for: 10m labels: severity: critical annotations: summary: "根分区剩余空间不足 10%,当前剩余 {{ $value | humanizePercentage }}"还有一类和容量相关但容易漏掉的指标:node_filesystem_readonly。它代表文件系统是否已经是只读状态。很多老牌 Linux 机器磁盘发生硬件错误时,内核会把文件系统重挂载为只读以防止数据进一步损坏,这个信号比“容量满了”严重得多,一旦出现,基本意味着这个节点上的写入型业务已经停摆。建议用node_filesystem_readonly == 1单独做一条 critical 告警,甚至可以考虑路由到更高优的通知渠道。
磁盘 IO 压力则要看node_disk_io_time_seconds_total的 rate。它反映磁盘有多少时间在忙,可以近似看成“排队程度”。如果磁盘 io 占比长时高于 80%~90%,但容量指标还健康,大概率是磁盘吞吐或 IOPS 到了瓶颈,扩容磁盘不做,业务迟早出问题。
2.3 CPU、负载与网络:聚合维度定错了就是在看假数据
CPU 告警的常见错法是用avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))判断“整体空闲”。但现代物理机动不动就几十核,一个计算密集型的线程打满单核,avg算出来可能只有 3%,告警完全不会触发。可业务线程如果绑定在那一核上,延迟可能已经飙升了。所以我通常先把各核的空闲率算出来,再取最小值:
( 1 - min by (instance, cpu) (rate(node_cpu_seconds_total{mode="idle"}[5m])) ) * 100 > 95表示“至少有一个核心在 5 分钟窗口内几乎打满”,这种规则对排查单线程性能瓶颈更有现实意义。如果想找整体负载,可以看node_load1,但需要和 CPU 核数做对比,而不是任取一个固定值。业界常用做法是算“load1是否超过核数乘以某个系数”“load1 与 load15 的差值是否拉大”,前者看水位,后者看趋势。
网络指标要特别注意方向。node_network_receive_bytes_total是入向流量,node_network_transmit_bytes_total是出向流量,漏掉网卡过滤条件时,lo回环网卡会把本机进程间的流量也算进来,数字大得离谱但没有参考价值。我在网络类告警里一定会加device!~"lo|docker.*|veth.*"过滤,否则 Pod 网络方案活跃的机器上,虚拟网卡列表每天都是变化的,规则稳定性会很差。
2.4 时间同步、systemd 与进程级指标:容易被忽略的生产信号
除了 CPU、内存、磁盘这些“老三样”,node_exporter 里还有几个指标在生产环境非常值得盯,但很多人压根没写规则。
第一个是时间同步。node_time_seconds和node_timex_offset_seconds能一起说明问题。机器时钟漂移超过几百毫秒时,对分布式系统是灾难性的:日志时间线错乱、分布式事务超时判断出错、TLS 证书校验偶发失败。我见过一次跨机房应用超时比例升高,排查到后面发现是一个节点的时钟快了 2 秒,导致大量基于时间的请求处理判断颠倒。写规则时建议使用:
abs(node_timex_offset_seconds) > 0.5node_timex_offset_seconds表示系统时钟与硬件时钟/NTP 源的偏移,绝对值超过 0.5 秒基本说明 NTP 同步已经出了问题。
第二个是 systemd 单元状态。node_systemd_unit_state{state="failed"} == 1这个表达式能帮你抓出“服务进程还活着但 systemd 认为它启动失败”的边角情况。尤其是有多实例、多副本的场景,一个服务的某个实例崩溃后 systemd 拉不起来,如果业务侧没有做健康检查,这条告警可能是你唯一能感知到的渠道。我甚至会在关键单元上单独加一个类似“node_systemd_unit_state{name="xxx.service", state="failed"} == 1”的精确规则,比扫所有 unit 更聚焦,也更不容易被无关服务刷屏。
第三个是文件描述符与进程数。node_filefd_allocated达到node_filefd_maximum的 80% 以上时,通常意味着某些进程疯狂创建 socket 或读写文件而不释放,虽然不是所有业务都触发,但一旦触发就是上游问题的重要旁证。这类指标我建议给一个 warn 级别,由后台系统自动跟踪,不必劳烦值班人。
2.5 for 参数:抖动的缓冲器,而不是报警的延迟器
Prometheus 的for字段经常被当成“让告警晚点发”的工具,这是误解。它的真正作用是确认“瞬时高值”是否延续成了“稳定异常”。for: 5m意味着查询表达式连续 5 分钟都满足条件,才会把 pending 状态翻成 firing。这能过滤掉大量由 GC 停顿、定时任务跑批、批量导入造成的瞬间毛刺。
但for也不是越大越好,尤其是内存或磁盘这类“慢变量”:
- 内存耗尽类,
for: 2m基本务实,因为可用内存从 10% 降到 0 的速度可能很快; - 磁盘容量类,用
for: 10m甚至更长也没问题,磁盘不会在几分钟内从 80% 涨到爆满,除非有异常的日志写入; - CPU 单核打满类,建议
for: 15m以上,因为单核抖动频繁,很多批量任务在几十秒内就会结束,短 for 只会让告警变成日常背景音。
如果拿不准,我宁可把for设长一点,同时把阈值收紧,也不要反过来:阈值放宽 +for缩得很短。后者的告警会像楼下便利店的门铃,一直在响,但每次都不是你家的事。
3. 生产级告警规则的工程化落地
3.1 规则文件组织:一个 group 只讲一件事
node_exporter 规则刚起步时,我习惯把所有规则塞进一个node_rules.yml,后来发现每次调整都极度痛苦:想改磁盘规则,要在一个几百行的文件里反复上下滚动,还容易把某个标签写串。后面我按“子系统”拆成多个 group,文件名对应报警主题。如果你管理的机器按照角色分(数据库、网关、业务容器节点),可以更进一步:网络和基础系统类的规则放通用 group;和数据库相关的放database_specific_rules.yml。
每个 group 内部,规则顺序也有讲究,把 critical 高的规则放前面,warn 级的放后面,Alertmanager 在渲染报警列表时,人能一眼看到最严重的部分。类似这样的组织方式:
groups: - name: node-basic-critical rules: # 内存耗尽、磁盘只读、时钟偏移这类直接破坏可用性的规则放这里 # for 短、severity=critical、路由到 on-call 组 - name: node-basic-warning rules: # 磁盘容量趋势、CPU 长时间高负载、网络异常增长等信息 # for 长、severity=warning、合并发送单个 group 不要超过 10 条规则,这是我在线上踩出来的舒适区边界。超过这个数字时,大概率你混入了不适合做告警的“观察型指标”。
3.2 告警文案是半个运维百科
很多人写规则时只写expr,剩下全靠 Alertmanager 通用模板。结果值班同学收到一条消息:“[FIRING] node_filesystem_avail_bytes ...”,只能自己去 Prometheus UI 里翻表达式,现场混乱程度直接翻倍。
我写的每条 production 规则,annotations 都必须包含四件事:现象、影响、排查路径、参考依据。不要嫌字多,告警信息本身就是一个操作手册的起点。比如磁盘告警的 summary 不要只写“磁盘空间不足”,可以写成:
节点 A 的根分区 / 可用空间低于 10%(当前剩余 6.2GB)。可能导致系统日志无法写入、临时文件清理失败,进而影响容器重建。请先使用 df -h 确认是否为大文件堆积;如果没有明显大文件,再用 du -x --max-depth=1 / 从根目录逐层确认占用来源。
没有人会在被唤醒后喜欢阅读五千字小说,但这些要点式的文案让他在 5 分钟内能形成有效判断。另外我习惯把“恢复条件的预期”也写进runbook_url或 metrics 的标签里,比如恢复阈值是 15% 不是 10%,否则你修复后反复横跳,只会加倍值班同学的血压。
3.3 promtool:让规则在进生产前先过一遍测试
规则文件里 PromQL 表达式语法很坑,一个括号位置错了,告警规则直接静默失效。更麻烦的是,Prometheus 在加载规则时即使发现语法错误,也只会把整条规则标记为不健康,不会立刻打断你已经跑起来的旧规则,于是你很容易在“不知不觉中丢了一条告警”。
我每次改完规则,都会先跑一遍本地校验:
promtool check rules node_rules.yml这一步能把 YAML 结构和表达式语法问题先拦住。Prometheus 新版本还支持promtool test rules,可以用单元测试的方式断言“在预设的样本数据下,这条告警是否应该触发”。我的建议是,宁可多写几条样本断言,也好过让规则像待拆炸弹一样躺进生产环境。至少给容量类和内存类规则各配一个测试用例,一个是“确实快满时应触发”,一个是“瞬时尖峰不应触发”,这能逼你想清楚规则的实际语义。
如果没有测试文件,上线后也至少观察一周的告警事件。Prometheus UI 的 Alerts 页面可以看到 pending 与 firing 状态。如果一个 warning 级别的规则从未 firing,不代表它没在工作,反而可能说明阈值偏松;如果一个 critical 规则天天规律地在某几个时间点触发,大概率是周期任务导致的“伪故障”,需要重新调整 for 或阈值。
3.4 Alertmanager 路由、抑制与去重
规则写得再好,如果 Alertmanager 的路由配置一团糟,依然会收到一堆孤立告警。一条生产事故往往是“根因坏了,连带一堆指标异常”。最典型的是数据库节点宕机:你会同时收到进程不可用、磁盘 IO 异常、该节点上的应用接口超时等告警。如果没有抑制关系,值班人会在同一时间收到十几条消息,真正需要处理的那一条反而被淹没在海量事件里。
Alertmanager 的抑制配置可以解决这个问题。比如当某个实例出现 critical 级的磁盘只读事件时,可以抑制该实例所有 warning 级的容量告警。规则的价值是让人把注意力集中在根因上,而不是做告警的搬运工。
路由上也建议按团队职责切分,而不是按“集群名”切分。用severity=critical的标签第一时间走高优通知渠道(电话或短信);severity=warning默认静默累计到工作时段统一看板;带team=db标签的规则自动路由到数据库值班组。这比让所有人收同样消息科学得多。
4. 误报排查与上线调优实录
4.1 典型误报一:内存可用率告警挂了一晚上,主机却没事
最早我写的内存规则是“剩余 10% 告警”,上线第一天晚上就疯了。盘点下来,根因是机器上有大量的页缓存被node_memory_MemFree_bytes计算误解了。当时表达式没考虑MemAvailable,导致一台跑着 Redis 的机器频繁把“有缓存但可用”的状态误判为“内存不足”。我后来把表达式全部切换成MemAvailable后,同样的机器一周最多触发一次,而且触发时业务侧确实在报 OOM 或 GC 频繁。
排查这类问题时,不要只看 Grafana 面板的平均曲线,直接到那台机器上执行:
cat /proc/meminfo | grep -E 'MemTotal|MemFree|MemAvailable|Buffers|Cached|SReclaimable'观察MemAvailable与MemFree的差值。如果差值很大,说明回收缓存足以支撑突发内存需求,内存告警大概率是阈值算法的问题。如果差值很小,这时告警才真正可信,可以把焦点放到应用程序的内存泄漏或 maxmemory 设置不当上。
4.2 典型误报二:磁盘告警里全是 overlay 和 tmpfs 的噪声
容器化普及之后,给 Node 节点写磁盘规则,几乎必然遇到overlay和tmpfs刷屏的问题。第一次踩到的时候,我在集群里收到几十条来自不同容器挂载点的告警,人差点崩溃。后来才理解,node_exporter 对挂载点的枚举是“见什么报什么”,而容器运行时会在宿主机上创建大量虚拟挂载,它们共享的是同一个宿主文件系统空间,但每个挂载点都对应一条独立的node_filesystem_size_bytes序列,导致重复告警看起来像出了几十次故障。
解决办法是白名单式过滤,而不是黑名单式追加。我的通用规则写了fstype!~"tmpfs|overlay|squashfs|ramfs"还不够,某些环境还有autofs、tracefs、configfs,推荐用运行时动态确认:
df -hT | awk 'NR>1 {print $2}' | sort -u然后把这台机器实际会挂载的文件系统类型放进白名单。生产环境中,我只对ext4|xfs这类真实数据盘做容量告警,其余类型一律不进入告警通道,放面板观察就够了。
4.3 典型误报三:for 周期设得太短,毛刺当成了故障
有一类毛刺来自计划任务:每天凌晨 2 点数据仓库会跑全量重算,节点的 load 和 CPU 会在十分钟内飙升,但通常不会导致业务失败。当时我给 CPU 单核打满设了 5 分钟持续条件,结果每天早上都会收到一波“单核打满”的告警,排查后没发现任何异常,只能一个个确认“这是跑批任务”。
后来我把这类任务执行时间窗口的指标与基础告警规则做联动,直接用一个时间变量排除固定维护窗口。PromQL 里没有跨时间段的排除语法,但可以在 alert rule 的 expr 中加上unless on () hour() >= 2 and hour() < 4,或者直接把 for 调整为 15 分钟以上,让规则跑批后再判断是否仍然异常。两招搭配,告警数量直接降了一个量级。
这种问题尤其容易出现在“用测试环境的节奏写生产规则”的场景里。测试环境的指标曲线比较干净,不需要 for 也能稳定触发;生产环境有各种定时任务、批量脚本、外部流量突刺,for 周期和噪声特征必须先摸清再定。
4.4 快速排查建议与规则维护节奏
如果新规则上线后误报频发,我建议做三件事:第一,打开 Prometheus 的 Rules 页面,找到目标规则,点击“Query”查看当前表达式的实际数值,确认阈值与真实曲线是否匹配;第二,在 Alertmanager 的 Silences 里给这条规则设置一个短期静默,等待人工分析,不急着在 Prometheus 里删规则;第三,把告警事件和当时的指标快照贴进复盘文档,连续观察两周后回看,决定是调节阈值、调整 for 还是直接删除。
我这里还会用一张简单的表格维护常见误报案例,时刻提醒自己别再重蹈覆辙:
| 误报现象 | 根因 | 解决方向 |
|---|---|---|
| 内存剩余不足误报 | MemFree 未考虑 page cache | 改用 MemAvailable 计算可用内存 |
| 磁盘重复推送 overlay 告警 | 文件系统过滤条件不完整 | 用 df -hT 确认实际 fstype 后做白名单 |
| 凌晨 CPU 高负载误报 | 批量任务导致的瞬时毛刺 | for 调大或排除计划任务窗口 |
| node_exporter 自身挂掉导致无数据 | 抓取任务静默失败 | 配置 up == 0 的全局探活告警 |
| 告警恢复后立刻再次触发 | 恢复阈值靠近触发阈值 | 引入 hysteresis,恢复阈值与触发阈值拉开距离 |
5. 写好规则之后再往前走一步
规则上线不是终点,持续的维护才是。我个人的习惯是按季度检查一次规则列表,把半年都没触发过、且没有承载风险预期的规则下掉或者降级为记录规则,避免随着业务演进,旧规则变成“永远存在但没有意义的红色指示灯”。
另外一个小技巧是,如果有条件,把 node_exporter 的指标另存一份到长期存储,和告警事件打通。这样每次做容量规划时,不用拍脑袋说“我们好像经常磁盘满”,直接按季度查询磁盘增长斜率,就能判断是不是要给某类节点提前升配了。告警只是抓手,真正有价值的还是把数据还原成系统演进的轨迹。我在实际工作中最大的感受是:会写 node_exporter 告警规则的人不少,能把规则的误报率压到很低并对业务真正有用的人不多,而这种差距,往往就藏在阈值设计、指标语义和文案细节的颗粒度里。