告警噪音治理:把每天几十条压到每周两条

一、告警最贵的成本不是漏报,是没人看

我自己的站点只有两台机器,一开始也给告警做得很起劲:磁盘超 80% 发、CPU 超 70% 发、Nginx 5xx 超过 10 条发、备份失败发、证书快到期发。三个月后回头看,那个 Telegram 频道里有四千多条消息,其中真正需要我动手的不超过二十条。剩下的是深夜三点 CPU 因为日志切割冲到 72%、每月一次磁盘因为备份临时涨到 81%、以及某个爬虫触发的 502 尖峰。于是我养成了一个坏习惯:看到告警先划掉,等有空再看。这个习惯形成的那一天,整套告警系统就已经失效了——因为当真正的那一条出现时,它和四千条噪声长得一模一样。

告警治理的核心不是「加更多规则」,而是「让每一条发出来的消息都值得被看见」。衡量指标只有两个:一是告警触发次数与真实故障次数的比值,二是从告警到有人响应的时间。前者高说明在制造噪声,后者高说明通道或分级有问题。这篇讲的是我用一个周末把两台机器的告警从「每天几十条」压到「平均每周两条、每条都要动手」的具体做法。

二、先把最近一周的告警导出来数一遍

治理不能凭感觉,要先把事实摆出来。如果你的告警走的是 Telegram 或钉钉,把最近七天的消息导成文本,按规则名做一次计数:

awk -F'|' '{print $2}' alert_history.log | sort | uniq -c | sort -rn | head -20

输出会非常直观:排在前三的规则通常占了总量的七成以上。我这次的统计结果是:磁盘空间 142 条、5xx 错误率 88 条、CPU 使用率 61 条、备份完成通知 30 条,而真正的故障只有两次(一次 MySQL 连接数打满、一次证书续期失败没 reload)。也就是说,四条规则产生了 321 条消息,其中 319 条不需要任何动作。

按这个清单给每条规则做分类,只有三种归宿:第一条是「保留但必须加条件」,比如磁盘检查从 80% 改成「预测 24 小时内写满」;第二条是「降级为日报」,比如备份完成通知,成功不需要实时知道,失败才需要;第三条是「直接删掉」,比如成功类通知、以及所有「以防万一」加上的阈值告警。

三、第一类改造:用「预测写满时间」替代阈值

磁盘超 80% 之所以吵,是因为它在一个月里会连续响十几天,每天响一次,而实际上你只要在它满之前清理就行。真正有信息量的不是「现在 82%」,而是「按最近 24 小时的增长速度,还有几天写满」。Node Exporter 的 predict_linear 就是干这个的:

predict_linear(node_filesystem_avail_bytes{mountpoint="/"}[6h], 86400) < 0

这条表达式的意思是:用最近六小时的斜率外推一天,如果可用空间会变成负数,也就是一天内写满,才告警。换成这个之后,磁盘告警从每月一百多条变成了一季度两次,而且每次都是真事——一次是日志轮转配置写错导致 access.log 无限增长,一次是备份没有按期清理。两个都确实需要人工处理。

如果你的告警不走 Prometheus 而是自己写的 shell 巡检,同样的逻辑用两条 date 和 df 的输出算差值即可:把六小时前和现在的可用字节数相减,得到每小时净增长,再用当前可用量除一下。写成一个每天跑一次的小脚本,成本极低,效果一样。

四、第二类改造:给告警加「持续时间」和「静默窗口」

CPU 超 70% 这类瞬时指标天然不适合立即通知。日志切割、备份压缩、一次批量导入都会让 CPU 冲到 90% 以上,几十秒后自己回落。解决办法是给规则加 for 子句:

- alert: HighCPU
  expr: avg by (instance) (rate(node_cpu_seconds_total{mode!="idle"}[5m])) > 0.9
  for: 10m
  labels:
    severity: warning

for: 10m 要求条件连续满足十分钟才触发,绝大多数尖峰就此消失。同理,5xx 错误率也应该用「连续五分钟错误率超过 1% 且请求量大于 50」这种带基数门槛的表达式,而不是「五分钟内出现 10 条 5xx」——后者在流量低谷时一条 bug 就能触发,在流量高峰时又会漏掉真正的 5%。

静默窗口是另一半。已知的维护操作,比如升级内核要重启、备份要占 I/O、证书续期要 reload,都应该在操作前用 silence 把相关告警压住。这里的关键是「有结束时间」:silence 必须带一个明确的到期时刻,绝不能允许「永久静默」。我给自己定的规矩是最长四小时,超时后即使还在维护也要重新确认一次,这条规矩救过我一次——某次重启后 PHP-FPM 没起来,如果静默是永久的,站点会一直 502 到用户投诉为止。

五、第三类改造:把成功通知全部改成日报

备份完成、证书续期成功、发布成功、重启完成,这些消息的共同点是「一切正常」,而一切正常是不需要实时通道的。它们的正确归宿是一个每天早上九点发一次的汇总,内容只有四项:昨天备份是否全部成功、磁盘当前水位与七天后预测、证书剩余天数最小的那一张、过去 24 小时 5xx 总数。四条一行,扫一眼就知道昨晚安稳不安稳。

汇总的价值不止是减少噪声,还在于它提供了一条「基线」。看惯了「每天 5xx 二十条」之后,某天变成二百条你立刻会觉得不对劲,而这种变化在单条告警的淹没下根本发现不了。我现在判断一个站点健康不健康,主要就看这份日报的四个数字有没有偏离平时。

六、分级:每条告警必须能回答「我现在要不要起床」

告警分两级就够了:P1 是「用户已经受影响或者在几分钟内会受影响」,比如站点打不开、数据库不可写、证书已过期、磁盘写满;P2 是「有问题但有时间窗口」,比如磁盘预计三天后满、单机 CPU 持续偏高、某条备份链断了。P1 走能叫醒人的通道(电话、短信、带声音的即时消息),P2 只进日报和早间推送。

分级的难点不在技术,而在于敢不敢把大量规则降级。我最初把「5xx 超过阈值」也放在 P1,理由是「用户会看到错误」。但实际情况是,个人站的大部分 5xx 来自爬虫打到不存在的路由,用户根本感知不到,而站点真正挂掉时通常是 Nginx 整个不可达,走的完全是另一条告警路径。敢降级之后,P1 通道一年只响了六次,每一次都是真的,从此我看到 P1 会立刻拿起手机。

七、runbook:让告警消息本身就带着处置步骤

告警治理到最后会发现一个更大的成本:凌晨被叫醒时,大脑是空白的,明明上周才处理过同类问题,此刻却要重新翻笔记。解决办法是把处置步骤直接写进告警消息里。做法很简单,在告警规则的注解里加一段 runbook_url 或者干脆把前三步写进 description:

annotations:
  summary: "{{ $labels.instance }} 磁盘将在 24 小时内写满"
  description: "当前可用 {{ $value | humanize }}B。先查大文件:du -xh / 2>/dev/null | sort -rh | head -10;重点看 /var/log 与 /data/backup;清理后确认 df -h 有 20% 余量。"

这条消息有几个好处:被叫醒的人不需要先想「该跑什么命令」;处置步骤在写规则的时候就被验证过一次(你得真的跑通才敢写上去);后来接手的人也能照着做。我的经验是,一条写清楚了处置步骤的告警,平均恢复时间能缩短一半以上,因为省掉的正是「从睡醒到想起来该做什么」的那几分钟。

八、收敛与复盘:让告警数量自己往下走

治理不是一次性的。我给自己定了一个固定动作:每月最后一天看一次告警统计,只回答三个问题。第一,这个月哪条规则响得最多,它对应的故障是不是真故障,不是的话就按前面三类分法处理掉。第二,有没有哪次故障是用户先发现、告警后知道的,那说明缺一条规则,补上并确认它有 for 和分级。第三,P1 响过几次,每次从发出到处理用了多久,超过三十分钟的要看看是不是通道问题。

这个循环跑四个月之后,我的告警从每天几十条降到每周两条左右,规则总数反而从十一条增加到十四条——因为删掉的是噪声规则,补上的是真正会命中的维度(比如 MySQL 主从延迟、Nginx 上游超时率、证书剩余天数)。真正健康的告警系统,长这样:平时安静得让你怀疑它是不是坏了,响的时候每一条你都记得住。

九、小结

告警治理的全部内容,可以压缩成一句话:每一条发出去的消息,都必须能让人做出一个动作。做不到的,要么加条件(预测式替代阈值、for 抑制尖峰)、要么降级(成功类进日报)、要么删除(以防万一类)。再加上两级分级、消息内嵌处置步骤、每月一次的收敛复盘,一个两人以内的小团队就能把告警维护在「可信」的状态。告警系统真正的价值不在于它覆盖了多少指标,而在于当你看到它的那一刻,你会相信它。

Last modification:November 5th, 2026 at 01:25 pm

Leave a Comment