为什么你的告警群天天在「狼来了」
给个人站搭监控,装 Prometheus 和 node_exporter 只花十分钟,但真正让运维崩溃的是后面这一步:告警配置。默认规则没配好,结果就是半夜三点手机被震醒十几次,打开一看全是「一台服务器 CPU 超过 80%」的重复消息;等真出事(比如磁盘写满、站点 502)的时候,消息反而淹没在刷屏里被忽略掉了。
这不是 Prometheus 的问题,而是几乎所有人都会踩的坑:把 Alertmanager 当成一个「顺手转发邮件的组件」,忽略了它真正干的事情——把海量原始告警聚合成少数几条人看得懂的通知。这篇文章就专门讲 Alertmanager 的收敛机制:分组(group_by)、抑制(inhibit_rules)、静默(silence)和路由树(route),并给出一套个人站长可以直接抄走的配置。
先理清角色:Prometheus 判「有没有事」,Alertmanager 判「要不要吵你」
很多新手把这两个职责搞混,导致规则写得又长又难维护。正确的分工是:
- Prometheus(或 VictoriaMetrics、Mimir 等兼容组件):负责按
rule_files里的表达式判断「for 多久持续满足条件」,一旦满足就产生一条 firing 状态的告警,周期性地推给 Alertmanager。它只关心指标,不关心你手机响几次。 - Alertmanager:负责接收这些告警,然后按 分组、抑制、静默、路由 四套规则决定:哪些合并成一条、哪些根本不用发、发给谁、用什么渠道发。
换句话说,重复告警、告警风暴、半夜被吵,全都是 Alertmanager 该解决的问题,不是靠堆 Prometheus 规则去「压制」。理解这一点,后面配置才有主线。
分组 group_by:一台机器挂了别发 20 条告警
假设一个节点宕机,Prometheus 会同时产生 CPU、内存、磁盘、网卡、进程存活等一连串告警。如果不分组,你就收到 20 条通知。分组的本质是——把「属于同一逻辑对象」的告警合并成一条通知发出去。
Alertmanager 的分组由路由树里的 group_by 决定,默认是 [alertname, cluster, service]。对个人站最实用的配置通常是按「主机 + 告警大类」聚合:
route:
receiver: 'default-email'
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h这里三个时间参数是收敛的关键,别照抄网上的默认值就完事:
group_wait:新一组告警产生后,先等 30 秒再发。这一小段等待是为了把同一波事件里的兄弟们「凑齐」一起发,避免刚发出去又来一条补一刀。group_interval:同一组告警在后续有新成员加入时,最长多久发一次更新。别设太短,否则会持续刷屏。repeat_interval:一条已经发过的告警,如果一直没恢复,多久重发一次提醒。个人站设 4h 到 12h 都合理;设成 1h 你就会白天黑夜都被同一件事吵。
有个常见误区:把 group_by 设成 ['...'](空数组),意思是「所有告警合并成一条」。这在小型个人站里看起来很爽,但会带来一个隐蔽后果——不同紧急度的告警被合到一条通知里,你无法一眼判断哪台机器真出事了。建议至少保留 instance。
路由树 route:让不同告警走不同渠道
路由树是 Alertmanager 配置的骨架,它是一棵「标签匹配」的树:从上往下匹配,命中子路由就继续往下走,否则走当前节点的 receiver。
一个实用的个人站路由配置长这样——严重告警走电话/短信,普通告警走邮件,纯信息类的只进汇总:
route:
receiver: 'email-low'
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = "critical"
receiver: 'phone-sms'
group_wait: 10s
repeat_interval: 30m
- matchers:
- severity = "warning"
receiver: 'email-normal'
group_wait: 1m
repeat_interval: 2h要注意两点:
- 较新版本 Alertmanager(0.22 起)推荐用
matchers(支持=、!=、=~、!~),老的match是精确相等,match_re是正则,二者不能混用同一条。 - 路由匹配不会回溯:一旦命中某个子路由,就不会再尝试它的兄弟节点。所以越具体的规则要放越前面,兜底规则(
receiver: email-low)放父节点。
抑制 inhibit_rules:有根因告警时,别让衍生告警刷屏
抑制是 Alertmanager 里被低估最深的功能。它的作用是:当某条「根因型」告警已经 firing 时,把由它引起的「衍生型」告警静音掉。
最经典的例子:如果整台主机已经 NodeDown(不可达),那么这台机器上的 CPU、内存、磁盘告警全都毫无意义——机器都没了还谈什么 CPU 使用率。配置如下:
inhibit_rules:
- source_matchers:
- alertname = "NodeDown"
target_matchers:
- severity =~ "warning|info"
equal: ['instance']
- source_matchers:
- alertname = "DiskFull"
target_matchers:
- alertname =~ ".*WriteFail.*"
equal: ['instance']关键的 equal 字段表示:只有 source 和 target 在 equal 列出的标签上取值完全一致时,抑制才生效。这里用 ['instance'] 表示「同一台机器的 NodeDown 才能抑制同一台机器的警告」。如果不写 equal,它会被理解为「任意一台机器 NodeDown 就抑制所有机器的告警」,那会掩盖真实故障,非常危险。
个人站最值得加的抑制规则一般是这几条:主机不可达抑制其上所有告警;磁盘写满抑制相关写入失败告警;出口网络断抑制所有抓取失败的 Up == 0。
静默 silence:计划内维护的「一键免打扰」
静默和抑制的区别要分清:抑制是自动的(基于标签条件),静默是手动的(基于时间窗口)。当你要给服务器换内核、重启 MySQL、迁移机房时,提前建一个 silence,就能让这段时间的告警不打扰你,而不用去改 Prometheus 规则。
静默可以通过 Web UI(Alertmanager 默认 9093 端口)点几下就建好,也可以用 API 脚本建,方便写进运维流程:
curl -X POST http://127.0.0.1:9093/api/v2/silences \
-H 'Content-Type: application/json' \
-d '{
"matchers": [
{"name": "instance", "value": "10.0.0.5:9100", "isRegex": false}
],
"startsAt": "2026-10-15T22:00:00Z",
"endsAt": "2026-10-15T23:00:00Z",
"createdBy": "ops",
"comment": "内核升级维护窗口"
}'注意 startsAt 和 endsAt 必须是 RFC3339 的 UTC 时间,写成本地时间会差几个时区,静默窗口对不上,结果维护期间照样被吵醒——这个坑很常见。
amtool:命令行验收你的告警配置
配置写完了,别急着上线,用官方提供的 amtool 先做两件事:校验配置和模拟路由。
# 1. 校验配置文件语法(能提前发现缩进、字段名拼错)
amtool check-config /etc/alertmanager/alertmanager.yml
# 2. 模拟一条告警,看它最终会走哪个 receiver
amtool config routes test \
--config.file=/etc/alertmanager/alertmanager.yml \
severity=critical alertname=NodeDown instance=10.0.0.5:9100第二条命令会打印出这条告警经过路由树后命中的 receiver 名字。这是排查「为什么我的严重告警走了邮件而不是短信」最快的方法——直接看它匹配到了哪个节点,而不是靠猜。
另外可以用 amtool alert query 查看当前所有正在 firing 的告警:
amtool alert query --alertmanager.url=http://127.0.0.1:9093一套可以直接抄的个人站完整配置
把前面的片段组合起来,就是下面这份最小可用配置。它假设你只有一台 VPS 加一个站点,告警分 critical 和 warning 两级:
global:
resolve_timeout: 5m
smtp_smarthost: 'smtp.example.com:587'
smtp_from: 'alert@example.com'
smtp_auth_username: 'alert@example.com'
smtp_auth_password: 'YOUR_TOKEN'
smtp_require_tls: true
templates:
- '/etc/alertmanager/templates/*.tmpl'
route:
receiver: 'email-normal'
group_by: ['alertname', 'instance']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers:
- severity = "critical"
receiver: 'phone-sms'
group_wait: 10s
repeat_interval: 30m
inhibit_rules:
- source_matchers:
- alertname = "NodeDown"
target_matchers:
- severity =~ "warning|info"
equal: ['instance']
receivers:
- name: 'email-normal'
email_configs:
- to: 'me@example.com'
send_resolved: true
- name: 'phone-sms'
webhook_configs:
- url: 'http://127.0.0.1:8060/sms'
send_resolved: true配置改动后热加载,不需要重启进程(Alertmanager 支持 SIGHUP 重载):
amtool check-config /etc/alertmanager/alertmanager.yml
curl -X POST http://127.0.0.1:9093/-/reloadwebhook 接收端:把告警接进你的机器人
个人站很多时候不想用短信,而是想接进企业微信、钉钉或者飞书群。Alertmanager 本身不内置这些渠道,做法是起一个中间的 webhook 小服务,接收 Alertmanager 推送的 JSON,再转发成机器人能识别的消息格式。核心就是配置里的 webhook_configs,它会把告警以固定 JSON 结构 POST 给你的服务:
{
"version": "4",
"status": "firing",
"alerts": [
{
"status": "firing",
"labels": {"alertname": "NodeDown", "instance": "10.0.0.5:9100"},
"annotations": {"summary": "主机不可达"},
"startsAt": "2026-10-15T22:10:00Z"
}
],
"groupLabels": {"alertname": "NodeDown"},
"commonLabels": {"severity": "critical"}
}你的 webhook 只要读 status 决定是「告警」还是「恢复」,再从 alerts[].annotations.summary 里取人话标题就够了。注意这里是 POST 请求,而且 Alertmanager 会等你的服务返回 2xx,超时就会重试,所以 webhook 服务必须轻量、快速返回,别在里面做耗时操作(比如同步调外部 API),否则会拖垮 Alertmanager 的推送队列。
最容易踩的五个坑
- 忘记
equal导致抑制过度:一台上游设备异常,把所有机器的告警都压掉了,出了事你完全不知道。所有抑制规则都要有equal,且想清楚「相等」到底该在哪个标签上。 - 路由没有兜底 receiver:如果某条告警没匹配到任何子路由,会走父节点的 receiver;但如果父节点也没写 receiver,Alertmanager 直接拒绝启动或丢告警。
repeat_interval设太短:有人为了「不漏告警」设成 5 分钟,结果一条持续了一夜的告警发了 100 多遍,最后你在手机上把它静音了,等于自废武功。- 静默时间用了本地时区:API 只认 UTC,差 8 小时窗口就完全错位。
- 没有
send_resolved:故障恢复了却没有「已恢复」通知,你还以为一直没修好,会白折腾很久。
小结
告警系统做得好不好,标准不是「能收到消息」,而是「出事时你能被准确叫醒,没事时它完全不打扰你」。Prometheus 负责发现异常,Alertmanager 负责把异常翻译成人能消化的一条条通知。把分组、路由、抑制、静默这四套机制按本文的思路配好之后,你会发现告警群从「天天狼来了」变成了「响一次就一定是真的有事」——这才是监控真正该有的样子。