不知道你有没有经历过这种场面:半夜告警群里突然开始刷屏,几十上百条告警消息像子弹一样连成片弹出来,值班工程师揉着眼睛一看,源头其实只有一个——某台核心机器宕了,其余全是它牵连出来的连锁故障。刚开始大家还绷着神经一条一条看,到后面干脆麻木,把群消息设成免打扰。真正的关键故障混在告警海洋里,反而成了最后被注意到的那一个。告警策略如果只是停留在“能发出去”的阶段,那Alertmanager充其量是个通知转发器;它真正值钱的地方,是那套告警降噪能力。
这套能力全部藏在Alertmanager的配置里。今天我就用Prometheus生态最常见的组合,把告警策略从路由、分组到抑制、静默逐个拆开讲清楚,最后给出几套我踩过坑之后才沉淀下来的告警降噪最佳实践。不管你是在公司里刚搭好Prometheus、正被告警轰炸搞到失眠的运维新人,还是已经在用Alertmanager但总觉得通知又乱又吵的“老手”,这篇内容都能让你把配置文件改成看得见的效果。
1. 告警链路拆解:Alertmanager到底在什么位置
1.1 从规则触发到通知送达,一条告警要过几道关卡
一条告警从产生到你手机上收到推送,中间要经过好几道关卡。首先,Prometheus按照rule文件里定义的表达式周期性地进行计算,当结果持续满足阈值,并且维持了for字段设定的持续时间后,告警状态才会从pending进入firing。注意这个设计很关键,它本身就是一个天然的低通滤波器,用来拦截瞬时抖动带来的假告警。
接下来,Prometheus把处于firing状态的告警通过HTTP接口推送到Alertmanager。这里需要在prometheus.yml里显式声明Alertmanager的地址,通常是alerting.alertmanagers配置段,指向一个或多个host:port。很多人配置完之后发现告警没出来,第一步就在这查:Prometheus是不是真的把告警发出去了。
Alertmanager拿到告警后,并不会直接原样转发,它要依次执行几件事:先查静默规则,看看这批告警是不是被计划内维护的窗口临时屏蔽了;再走路由树匹配,确定这批告警该去哪个接收者;然后再跑抑制检查,把那些已经被更高级别故障“代表”的衍生告警压下去。最后,根据匹配到的接收者配置,渲染通知内容,通过Email、Webhook、Slack、PagerDuty等渠道发出去。任何一个环节断掉,你看到的现象都是同一个:该响的没响。
所以排查告警问题时一定要有这个全局视角。我曾经见过一个团队,Prometheus的Alertmanager地址写错了端口,Prometheus那边的ALERTS指标都已经是firing了,但因为推送失败,告警压根没进Alertmanager,最后折腾了一整天,根因就是一行配置。链路思维越清晰,排查方向就越准。
1.2 别指望它替你写规则:先说清Alertmanager的职责边界
很多人刚开始用Alertmanager时会有一个误解,觉得它应该负责生成告警。这是最需要扭转过来的认知:Alertmanager不评估指标,不判断阈值,不决定“到底是不是有问题”。它只处理Prometheus已经判定为firing的告警。你完全可以把Prometheus和rule文件想象成哨兵,Alertmanager则是那个坐镇后方的总指挥,负责把战况整理成值得回报的消息。
同样,Alertmanager也不是历史告警存储。它的界面里能看到当前活跃的告警、静默记录,但它不会长期保存已经解决的告警明细。想要做历史告警统计和趋势分析,你得靠Prometheus里的ALERTS指标,或者引入其他支持长期存储的组件。别指望在Alertmanager里找到“上周总共发了多少条”这种问题,它的职责是处理后立即通知,不是归档。
把边界划清楚,再回来看它的配置就清爽很多。你需要关心的核心配置块无非四个:route、receivers、inhibit_rules,以及通过UI或amtool管理的silences。route决定告警去哪里,receivers决定怎么发,inhibit_rules决定哪些告警要被闭嘴,silences则是对部分告警临时按暂停键。接下来的几个部分,我就按照这四个块逐个展开,每块都配上实际场景解释,比单纯贴一份默认配置有用得多。
2. 路由与分组:告警怎么找到人,又怎么合并成一条
2.1 路由树:告警该发给谁,由matchers和continue决定
route是Alertmanager配置的灵魂,它结构上是一棵树。最外层是根路由,子路由放在它的routes列表下面,子路由下面还可以继续嵌套子路由。Alertmanager拿到一条告警后,从根开始自上而下匹配matchers,匹配规则有几个必须记住的要点。
第一,子路由优先。如果某个子路由匹配上了,就会使用这个子路由上定义的接收者;如果它还有更深的子路由,还会继续往下一层找。第二,当所有子路由都没有匹配成功时,会回退到当前这个父路由的接收者。这个回退机制很容易踩坑:很多人只在根路由上写了receiver,又在下面挂了两三个子路由匹配不同团队,结果没匹配到任何子路由的告警,最终还是从根路由发出来,看起来就像“我明明没想让所有人收到,怎么还是发了”。实际上逻辑没有错,只是你没意识到回退的存在。
第三,continue字段决定了告警匹配到一个子路由之后,是停下来还是继续找下一个平级路由。默认是false,也就是匹配到就算数,后面的平级路由不再看。如果你希望同一条告警既发给值班组,又发给对应业务线,就在子路由上开continue: true。这东西使用要克制,开得越多,重复通知越多,降噪效果就越差。
下面是我实际在用的一个路由骨架:
route: group_by: ['alertname', 'cluster'] group_wait: 30s group_interval: 5m repeat_interval: 4h receiver: 'default' routes: - matchers: - 'severity = "critical"' receiver: 'pager' continue: false - matchers: - 'team = "infra"' - 'alertname =~ "NodeDown|DiskFull|HighCPU"' receiver: 'infra-webhook'新版Alertmanager推荐用matchers,用=表示等于,=~表示正则匹配。旧版配置里的match和match_re字段不要再用,迟早要迁移。另外,路由树的层级不要堆得太深,生产环境一般两层到三层就够,层级越多,排查“这条告警为什么发给了A而不是B”的难度就越大。
2.2 分组参数:把散弹枪的子弹装进同一个信封
告警分组是降噪的绝对核心。可以把它理解成寄快递:group_by就是按什么地址打包,group_wait是门口集合等人齐的时间,group_interval是发出下一批包裹的间隔,repeat_interval是包裹送达之后,每隔多久再催你一次。
group_by是最容易影响告警量的一个参数。假设有10台机器的进程都因为一台数据库宕机而失败,Prometheus会生成10条ProcessDown告警。如果group_by: ['alertname'],这10条告警会合并到同一个组里,最终发送一条通知,里面列出所有受影响的实例;如果你把instance也放进group_by,每台机器就会各自成组,10条告警拆成10组,告警群瞬间被刷屏。有人为了看到每台机器的明细,把instance加进group_by,结果反而亲手制造了一场小型告警风暴。
group_wait默认30秒,意思是当一个新分组里出现第一条告警后,先等30秒再发送,目的是把差不多同时到达的同类告警凑在一起,发一条合并通知。如果你的网络抖动频繁,想进一步聚合同一波短促故障,可以适当调到1到2分钟,但别设得太长,否则关键告警会延迟很久才到人手里。
group_interval默认5分钟,它控制的是分组内出现新告警时的发送频率。比如组里已经有3条告警,第5分钟又进来1条,如果此时距离上次通知超过group_interval,就会再发一条合并更新。这个值太短会频繁骚扰,太长则可能导致组内新告警信息滞后。
repeat_interval默认4小时,它负责处理那些一直没恢复、组内也没有新增告警的情况。每过4小时,Alertmanager会重新把当前组内的告警汇总通知一遍,相当于“提醒你还挂着这个事”。我建议值班制团队至少从4小时起步,重要告警可以缩短到1到2小时,但不要设成30分钟,否则你会被同一条告警反复磨到麻木。
参数整理成表格更直观:
| 参数 | 默认值 | 作用 | 经验取值 |
|---|---|---|---|
| group_by | alertname | 按哪些标签合并告警组 | alertname加业务维度,如cluster、app |
| group_wait | 30s | 新组等待时间,用于聚合同波告警 | 30s到2min |
| group_interval | 5m | 组内新告警再次通知的最小间隔 | 5m到10m |
| repeat_interval | 4h | 组内无变化时重复通知的周期 | 4h起步,重要告警1到2h |
2.3 接收者与模板:通知内容决定人要不要理你
receivers这一块没有太多玄学,但选型和配置直接影响告警是否真正被看见。除了Email、Slack、PagerDuty这些内置类型,国内团队用得最多的其实是webhook,通过它对接飞书、钉钉、企业微信的机器人。Alertmanager负责把选中的告警渲染成一条结构化消息POST到Webhook地址,后面的@人员、跳转链接、自动建单,交给中间层去处理。
我建议所有接收者都打开send_resolved: true,否则告警恢复了人还不知道,值班人员只能一遍遍刷新页面确认。配置大概长这样:
receivers: - name: 'infra-webhook' webhook_configs: - url: 'http://172.16.0.10:8080/hook' send_resolved: true通知模板这件事很容易被忽略,但它决定了告警到达手机后,人能不能在10秒内判断出该不该立刻处理。我要求团队在Prometheus规则的annotations里至少写明三件事:这个故障影响的是什么、你现在该去哪里查看现场、完整的runbook文档链接。模板用Go template语法渲染,核心思路是:summary必须用一句话说清故障对象和影响,description才用来展开细节。别把一坨日志全文塞进告警消息里,手机屏幕放不下,人也根本不会读。
3. 抑制与静默:把噪音告警掐在源头
3.1 抑制规则:让连锁告警闭嘴
抑制解决的是“这个告警已经被另一个告警代表”的问题。最经典的场景是主机宕机:一台机器挂了,上面的所有服务自然不可用,于是ProcessDown、PortDown、HTTP5xx高这些告警会一起冒出来。如果全都发到群里,值班人看到的是一堆重复信息,真正的根因反而被淹没。
inhibit_rules就是为了掐断这条连锁反应链而存在的。它的逻辑是:当存在一个满足source_matchers的活跃告警时,那些满足target_matchers的告警会被暂时压住,除非两者在equal字段指定的标签上值不同。
我这里有一个真实的配置示例:
inhibit_rules: - source_matchers: - 'alertname = "NodeDown"' - 'severity = "critical"' target_matchers: - 'severity = "warning"' equal: - 'instance'意思是:只要某台机器上有NodeDown且级别是critical的告警在活跃状态,那么同一台实例上所有severity=warning的告警先不通知。equal: ['instance']保证了抑制只影响同一台机器,不会因为跨实例把所有机器都压掉。
这里最容易犯的错,就是equal字段里的标签名对不上。比如你在某个告警规则里把机器标识写成host,在另一个规则里写成instance,哪怕它们语义上都是同一台机器,抑制也不会生效,因为标签字面值就不一样。标签命名标准化不只是为了好看,它是抑制规则能不能工作的前提。
还有一个细节:抑制的源和目标都必须是活跃告警。如果源告警恢复了,被抑制的目标告警如果还在firing,会恢复通知。另外抑制只作用于通知阶段,不代表告警消失了,你在Alertmanager UI上依然能看到这些告警处于suppressed状态。这正是降噪的正确姿势:消息层面藏起来,事实层面留痕。
3.2 静默:给计划内维护按暂停键
静默和抑制是两回事。抑制是自动规则,静默是人工临时操作。最常见的用法是计划内维护:你凌晨要重启机器升级内核,提前在Alertmanager里挂一条针对这台机器的静默,时间窗口内它的告警全部不通知,避免半夜把值班人喊起来处理一场本来就知道要发生的故障。
操作方式有两种:Web UI和amtool命令行。UI适合单次点选,命令行适合写进运维手册和脚本。我习惯把常用静默命令整理成团队文档,这样临时有人接替操作也不会手忙脚乱:
amtool silence add 'instance="10.0.0.5"' --duration=3h --comment="例行升级维护" amtool silence list静默必须写comment和expire过期时间。没有comment,一周之后没人记得当时为什么屏蔽;不设置过期时间,就相当于给这批告警判了无期,等它失效时,故障可能已经悄悄长成一个大坑。Alertmanager的静默到期会自动过期,这是设计上的自我保护,别去突破它。
建议团队定一个规矩:所有静默都要关联一个工单号或者变更单号,写进comment里。这样审计时能追溯,也能避免一个人挂了静默、另一个人不知道,最后互相猜疑“为什么告警没响”。
4. 告警降噪最佳实践:从规则源头到故障现场的完整闭环
4.1 源头降噪:for持续时间与告警分级才是根子上的事
Alertmanager再厉害,也只能在Prometheus送来的一堆告警里做文章。如果源头每秒都在产生垃圾告警,后面再怎么分组抑制都是亡羊补牢。真正的降噪,第一刀必须砍在Prometheus rule上。
最有价值的一个参数是for。它表示表达式结果持续为真的时间。比如磁盘使用率超过95%这件事,IO尖峰可能瞬间冲到97%,下一秒又掉回80%,这种瞬时抖动根本不该打扰人。加上for: 10m,只有使用率持续95%以上达到10分钟才触发告警,绝大多数假告警在源头就被过滤掉了。一个成熟的告警规则,几乎每条都应该配for。
其次是标签和分级。我建议每一条rule都带上severity和team两个标签,取值保持全国统一:critical表示需要立即处理,warning表示需要关注但不至于半夜把人喊醒,info只做记录通知。Alertmanager的路由和抑制规则都围绕这两个标签做文章,命名一旦混乱,后面所有配置都会跟着乱。
第三个点是表达式不要一杆子打一片。比如检查内存使用率时,用by (instance)做聚合,避免一个分组里的任何实例超标就把整组状态都判定为异常。尽量让每条告警都能定位到具体对象,这样路由和抑制才有正确的标签可用。
示例规则:
groups: - name: node-exporter.rules rules: - alert: NodeHighMemoryUsage expr: (1 - (node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes)) > 0.95 for: 10m labels: severity: critical team: platform annotations: summary: "{{ $labels.instance }} 内存使用率超过95%" runbook: "https://runbook.example.com/memory"这里的for: 10m已经把绝大多数瞬时抖动过滤掉了,送到Alertmanager那里的告警量自然就少了一大截。
4.2 一个故障场景的降噪推演:从60条轰炸到1条有效通知
光讲参数太抽象,我用一个具体故障场景把前面所有机制串一遍。
假设现在是上午10点,数据库节点DB-1宕机,影响到了30个微服务实例。如果完全不配置降噪策略,Prometheus会产生30条ProcessDown、30条HTTP5xx高,再加1条NodeDown,总共60多条告警在几秒内涌进Alertmanager。默认配置下,它们可能被按alertname分组,但processdown和http5xx是两组,总共会发出去至少两条大消息,加上恢复时又是一轮,值班群基本就被刷屏了。
加上合理的策略之后,流程是这样的。第一层,Prometheus侧给所有规则都配了for,那些只抖动了十几秒的假告警已经没了。第二层,Alertmanager里把NodeDown判定为critical,走pager接收者,把ProcessDown和HTTP5xx设置成warning,走普通Webhook。第三层,inhibit_rules里配置了源告警NodeDown抑制目标告警severity=warning,且equal字段用instance关联,所以前面那60条warning告警全部被压住,一条都不会往外发。第四层,剩余的critical告警经过group_wait的30秒聚合窗口,把同一波故障里的几条关键消息合并成一组。
最终值班手机收到的,可能只有一条:NodeDown,DB-1不可达,附带runbook链接。恢复之后,再收到一条DB-1已恢复。这才是有效告警该有的样子:信息密集到人可以直接做决策,而不是让人在60条消息里做阅读理解。
降噪不是把告警藏起来,而是把信息密度压缩到人只需要看点不多的比例。告警数量多不代表重要,一条精炼的告警往往比一百条描述性信息更能推动处理。
4.3 告警治理:定期给告警规则过节食
告警降噪不是配置一劳永逸的事。系统在变,流量在变,业务在变,半年前定下的阈值和规则,半年后大概率有一半已经不合身了。我建议每个月做一次告警体检,把当前还在firing或频繁触发的告警拉出来排个序,看看谁是最能刷存在感的“话痨”。
可以在Prometheus里直接用这个查询,按告警名统计当前活跃数量:
sum by (alertname) (ALERTS{alertstate="firing"})把排名前20的告警拿出来,逐条过三关:这条告警触发后,值班人有没有按预期去处理?没有,说明它是噪音,或者阈值设得太敏感。有,但处理动作和告警内容对不上?说明标注和描述还不够清晰,要继续打磨。它是不是已经被另一条高等级告警覆盖了?如果是,就应该顺手把这条低频的降级或者删掉,而不是让它继续重复打扰人。
在一个业务稳定、值班压力可控的团队里,告警规则的养护应该和代码重构一样,进入定期的迭代清单,而不是上线之后就再也不管。我自己每个季度至少会做一次全量规则审计,把那些“当初设置时很有道理、现在完全没人在意”的告警清出去。告警集合越小、越准,每一条发出来的通知才越有分量。
5. 常见问题排查:告警不响、重复轰炸的现场实录
5.1 告警没送达:链路好几段,先查自己家
“告警没收到”是我被问得最多的问题。排查思路一定要沿着链路走,不要上来就猜。
第一步,先确认Prometheus侧是不是真的触发了。去Prometheus的Rules页面看这条告警当前是什么状态。如果连pending都不是,说明规则本身都没满足,那跟Alertmanager完全没关系,回去查阈值和表达式。如果已经是pending但迟迟不转firing,那就是for还没到时间,属于正常现象。
第二步,确认告警有没有推送成功。在Alertmanager的UI上,Status页面能看到它接收到的告警以及当前处理状态。如果你看到alertmanager_alerts相关的指标没有增长,就要回到prometheus.yml检查alertmanagers配置段的地址和端口,最常见的错误就是端口写成了Prometheus自己的9090,而不是Alertmanager的9093。
第三步,检查是不是被静默或抑制吞掉了。在Alertmanager的Silences页面里看一圈,有没有那种特别宽泛的静默规则,比如匹配severity=~".*"的,它会把所有告警都静默掉。抑制规则如果写得太宽,也会导致告警在UI上显示suppressed,消息却一直出不去。在UI上看到状态是suppressed,基本就可以锁定是这里的问题。
最后,如果你确实想知道每一步发生了什么,把Alertmanager加上--log.level=debug重新启动,日志里会详细记录路由匹配、分组等待、接收者调用的过程。日志读起来确实啰嗦,但排查问题的时候,它比任何猜都可靠。
5.2 告警反复轰炸:参数、路由和抑制规则逐个排查
告警轰炸通常逃不出几个原因,我整理成一个速查表:
| 症状 | 常见原因 | 解决方向 |
|---|---|---|
| 同一条告警每隔几分钟重复发 | repeat_interval太短 | 调大到4小时或以上 |
| 一个故障刷出十几条 | group_by粒度太细,instance被放进去 | 按alertname或业务标签聚合 |
| 一个故障同时从多个渠道收到多条 | continue: true被滥用,或路由匹配多个receiver | 非必要不要开continue,检查接收者配置 |
| 恢复通知和故障通知一起轰炸 | send_resolved在多处重复开启 | 统一在接收者配置里控制,确认不重复 |
排查时要记住一个反直觉的点:group_by聚合之后,组内告警不会一条条单独通知,而是以组为单位。比如组里有10条告警,恢复了5条,Alertmanager可能发一条合并的恢复通知。这是分组机制的正常行为,不是丢告警。如果团队不习惯这种粒度,可以通过调整group_interval和group_by的组合来改变体验,但很难做到既合并又逐条清晰。
另一个常见的隐性坑是路由continue: true。我见过有人为了让critical告警同时发给值班组和业务组,在子路由上开了continue,结果每个receiver对应的Webhook都处理了一次,同一条消息被飞书、钉钉、企业微信各发一遍,形成三倍噪音。出现这种情况,先检查是不是continue被开得太随意。
5.3 用promtool和amtool做配置体检,比重启验证靠谱
改配置最怕的是改完不生效,或者在原配置基础上一层层堆旧逻辑,最后连自己都看不懂。Alertmanager生态里有两个工具,能把这种风险压到最低。
promtool可以检查Prometheus的规则文件和配置,推荐每次改完rule都跑一遍:
promtool check rules rules.yml promtool check config prometheus.ymlamtool则负责检查Alertmanager自己的配置:
amtool check-config alertmanager.yml这两个工具过了再reload,基本能挡住百分之九十的低级错误。注意,reload之前别忘了一个动作:把原来的配置备份一份。改坏了还能秒回滚。
我还会在测试环境里专门做告警验证。比如临时加一条测试用的rule,表达式永远为真,label写成test=true,然后跟踪它在Alertmanager里的路由匹配和通知发送结果,确认每条路径都符合预期后再删掉。这套验证方法花不了多少时间,但它能避免你直接在线上试验,把故障时间窗口变成自己的“告警实验舱”。
如果你用的amtool版本较新,它还有一些可以直接查看当前路由树、静默记录的子命令,具体名字以你环境里的amtool --help输出为准。花五分钟看看这些命令,排查的时候能省不少事。
最后说一个我自己踩过好几次的坑。一开始为了让告警少一点,我在inhibit_rules里只写了source_matchers和target_matchers,没有加equal,结果一台机器宕了,凡是目标规则里severity=warning的告警全被压住,范围远超那一台机器,很多其实有价值的告警在悄悄消失。排查了很久才发现是equal字段缺失,加上equal: ['instance']之后才恢复正常的抑制范围。降噪的本质不是把告警全部变成零,而是保证送到人面前的那一条,值得马上处理。把规则维护成一个小而准的集合,比追着数字把告警量压到零,要健康得多。