一次Prometheus告警风暴拖垮邮件与企业微信告警通道的排查实录

一、问题背景

周四下午 14:20 左右,公司内部运维群突然开始”刷屏”——每秒钟都有十几条告警消息从企业微信机器人推出来,密密麻麻的红色 ERROR 几乎把群聊淹没。我当时正在处理一个普通的工单,看到群里”炸了”的第一反应是某个核心服务挂了,但仔细扫了几条告警内容后又觉得不对:大量”Redis 慢查询”、”接口 P99 突增”、”磁盘 IO 升高”这种零散的二级告警,没有一条提到核心业务异常。

更诡异的是,我试着登录 Grafana 看核心仪表盘,结果返回 502;打开邮件客户端想看原始告警,登录后邮件列表一直转圈加载不出来。这一刻我才意识到——告警本身把告警通道打爆了。

我们的监控栈是典型的”Prometheus + Alertmanager + 企业微信/邮件”组合,部署在 2 节点的 VM 上,Prometheus 单实例抓取大约 280 个 target,覆盖 12 套业务系统。按理说应该是个很稳的架构,那天下午发生的事情彻底改变了我们对”告警治理”的认知。

二、故障现象

故障爆发后,我开始从外到内逐层观察,记录下的关键现象如下:

1. 企业微信群告警刷屏

群机器人从 14:20 开始以大约 12 条/秒的速率推送告警,告警内容主要分两类:

  • HighRequestLatency P99 延迟大于 1s(业务接口 50+)
  • RedisSlowQuery 慢查询大于 100ms(Redis 实例 12+)
  • ContainerCPUThrottled CPU 限流(Pod 30+)
  • NodeDiskIOHigh 节点磁盘 IO 高(节点 8+)

但所有告警的 severity 标签都是 warning,没有任何 critical 级别的告警出现。

2. 邮件系统卡死

公司内部邮箱(基于 Postfix + Dovecot 自建)登录后无法加载新邮件,运维组的公共收件箱 [email protected] 在 10 分钟内收到了 18,742 封邮件。我 ssh 到邮件服务器上看了一眼:

1
2
$ postqueue -p | tail -5
-- 18472 Kbytes in 18742 Requests.

队列里堆了 1.8 万封待发邮件,Postfix 的 qmgr 进程 CPU 占用飙到 380%(多核),active 队列达到 smtp_destination_concurrency_limit 的上限。

3. Grafana 不可达

通过 curl -I http://grafana.internal:3000 测试返回 502。Grafana 后端依赖的 Prometheus 数据源因为查询请求堆积而响应缓慢,Grafana 默认的超时时间是 30 秒,大量仪表盘请求堆积导致前端一直 502。

4. 真正致命的故障被淹没

事后复盘发现,在 14:15 左右(也就是告警风暴前 5 分钟),核心订单数据库的主从复制其实已经中断,从库 Seconds_Behind_Master 飙到 NULL,意味着 IO 线程已经停了。但因为告警风暴期间 Alertmanager 的处理队列被压垮,这条 critical 级别的告警根本没有被发送出来。最终这个故障是业务部门反馈”订单数据不准”后我手动查数据库才发现的,业务受影响时长接近 2 小时。

三、排查过程

我按”告警链路 → 告警源头 → 告警通道”三段式展开排查,整个过程大约用了 40 分钟。

3.1 紧急止血:先让告警通道活过来

第一件事是阻止告警继续刷屏。我直接 ssh 到 Alertmanager 节点,先停掉企业微信机器人的 webhook 投递:

1
2
3
4
5
6
7
8
9
$ systemctl status alertmanager
● alertmanager.service - Alertmanager
Active: active (running) since ...

# 临时禁用webhook
$ vim /etc/alertmanager/alertmanager.yml
# 在webhook_config前加 - name: 'disabled'
# 注释掉 receivers 段中所有 webhook
$ systemctl reload alertmanager

然后去邮件服务器上把堆积的队列清掉,保留最近的 200 封邮件作为证据:

1
2
3
4
5
6
# 查看队列
$ postqueue -p | head -20
# 把队列里老于30分钟的邮件全部退信
$ postsuper -d ALL deferred
# 或者更精细:只删除30分钟前的
$ find /var/spool/postfix/deferred -type f -mmin +30 -delete

止血操作大约花了 5 分钟,群里的刷屏停了,邮件系统开始慢慢恢复。Grafana 在 2 分钟后也能正常打开。

3.2 查告警源头:为什么这么多告警同时触发?

止血之后开始查根因。打开 Alertmanager 的 Web UI(http://alertmanager:9093),在 Alerts 页面看到 Active 状态的告警有 2,347 条。这显然不正常——我每秒钟大概只有 5~10 个真实异常指标会被触发,绝大部分都是抖动。

我抽样了几条告警,看它们的 Active SinceLabels

告警名称 触发数 Active Since severity
HighRequestLatency 487 14:18:42 warning
RedisSlowQuery 156 14:19:03 warning
ContainerCPUThrottled 312 14:19:18 warning
NodeDiskIOHigh 88 14:19:31 warning

所有告警的 Active Since 都集中在 14:18~14:19 这 1 分钟内。这说明不是”业务真的出了这么多问题”,而是某个时间点发生了一次”集体抖动”。

接着我去查 Prometheus 的告警规则文件 /etc/prometheus/rules/,发现一个被忽视的配置:

1
2
3
4
5
6
7
8
9
# rules/business.yaml
- alert: HighRequestLatency
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[1m])) by (le, service)) > 1
# 缺失 for 字段!
labels:
severity: warning
annotations:
summary: "接口 P99 延迟高"

for 字段是告警持续多久才真正发送的关键参数。我之前为了”新告警规则快速生效”,把 for 字段都删掉了,意图是”指标一超标就告警”,结果就是任何一次 1~2 秒的网络抖动、一次 GC 暂停、一次容器重启,都会被立刻当成告警发送。

我去查了那 1 分钟内发生了什么事,从 Kubernetes 事件里看到:

1
2
3
4
5
6
$ kubectl get events --sort-by=.lastTimestamp | head -20
14:18:35 Normal ScalingReplicaSet deployment/order-service
14:18:38 Normal Pulling pod/order-service-7d8f9-xnz2j
14:18:40 Normal Scheduled pod/order-service-7d8f9-abcde
14:18:42 Normal Created pod/order-service-7d8f9-abcde
14:19:15 Normal Started pod/order-service-7d8f9-abcde

14:18 左右,业务团队发布了一版新服务,Deployment 滚动更新导致 30+ Pod 同时重启、镜像拉取、初始化,Prometheus 在这 1 分钟内采集到了大量瞬时高 CPU、高延迟、慢查询指标,所有缺 for 的告警规则同时被触发

3.3 查 Alertmanager 配置:为什么告警没有被合并?

光解释清楚了告警源头还不够——为什么 2347 条告警没有按”业务系统”、”告警类型”分组到一起,而是被一条条推出去?

打开 /etc/alertmanager/alertmanager.yml,看到路由配置是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
route:
group_by: ['alertname'] # 只按告警名分组!
group_wait: 5s
group_interval: 10s # 太短!
repeat_interval: 1h
receiver: 'ops-default'

receivers:
- name: 'ops-default'
webhook_configs:
- url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx'
email_configs:
- to: '[email protected]'

问题一:group_by: ['alertname'] 意味着所有同名告警会被分到一组,但每条告警的 servicepod 标签还是独立的,会作为多条记录在组里。所以 HighRequestLatency 告警虽然合并成了 1 条群消息,但群消息里包含 487 个 service 实例,消息体超大,企业微信 API 直接报错

**问题二:group_interval: 10s**,意味着 Alertmanager 每 10 秒就重新评估并发送一次”组更新”消息。结果就是这 2 分钟内,Alertmanager 给企业微信机器人推送了 12 次/秒 的频率。

问题三:企业微信机器人限流,官方限制是每分钟最多 20 条消息。当 Alertmanager 推得比限流还快,HTTP 429 响应导致消息被丢弃,但 Alertmanager 的 retry 机制又会重试,进一步加剧了堆积。

问题四:邮件更是无差别全发,Postfix 没有针对 [email protected] 这个收件人做 rate limit,所以 1.8 万封邮件在 10 分钟内全部进队列。

3.4 查根因告警:为什么 critical 告警没发出来?

这是最让我后怕的部分。我去 Alertmanager 的日志里翻:

1
2
3
4
5
6
$ journalctl -u alertmanager --since "14:00" | grep -i "notify"
... 14:21:18 notify_loop ... rate limited
... 14:21:19 notify_loop ... rate limited
... 14:21:20 notify_loop ... queue full, drop
... 14:21:21 notify_loop ... queue full, drop
... 14:21:22 notify_loop ... queue full, drop

queue full, drop 才是关键。Alertmanager 的内部通知队列是有上限的(默认 10000 条),当它自己处理不过来时,新来的告警直接被丢弃。也就是说,critical 的 DB 告警不是被”挤到后面”,而是被”丢掉”了。这是最严重的告警可靠性问题。

四、解决方案

针对上面四个层面的问题,我做了一次系统性的整改。

4.1 告警规则层:加 for 持续时间和分级

把所有 warning 级别的告警都加上 for 字段,按业务特征选择不同持续时间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# rules/business.yaml(整改后)
- alert: HighRequestLatency
expr: |
histogram_quantile(0.99, sum(rate(http_request_duration_seconds_bucket[5m])) by (le, service)) > 1
for: 5m # 持续5分钟才告警
labels:
severity: warning
category: latency
annotations:
summary: "接口 {{ $labels.service }} P99 延迟 {{ $value }}s"

- alert: RedisSlowQuery
expr: |
rate(redis_commands_duration_seconds_sum[2m]) / rate(redis_commands_duration_seconds_count[2m]) > 0.1
for: 3m
labels:
severity: warning
category: cache

# critical级别单独写,for 较短但需要更严格的指标
- alert: MySQLReplicationBroken
expr: |
mysql_slave_status_slave_io_running == 0
for: 1m
labels:
severity: critical
category: database

注意 expr 里的 rate 窗口也从 1m 改成了 5m,进一步平滑瞬时抖动。

4.2 Alertmanager 层:精细化路由 + 抑制 + 分级

重写 alertmanager.yml,核心改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
global:
resolve_timeout: 5m

route:
receiver: 'ops-default'
group_by: ['alertname', 'cluster', 'service'] # 加多维度分组
group_wait: 30s # 首次等30s再发,让同组告警合并
group_interval: 5m # 组更新间隔拉到5分钟
repeat_interval: 4h
routes:
# critical 走独立通道,避免被warning淹没
- matchers:
- severity = "critical"
receiver: 'ops-critical'
group_wait: 10s
group_interval: 1m
repeat_interval: 1h
continue: false # critical 走完后不再往 default 走

# 业务发布期间静音
- matchers:
- category =~ "latency|cache"
receiver: 'ops-business'
group_interval: 10m
repeat_interval: 24h
active_time_intervals:
- business_hours

# 抑制规则:critical 告警触发时,抑制相关的 warning
inhibit_rules:
- source_matchers:
- severity = "critical"
target_matchers:
- severity = "warning"
- category =~ "latency|cache|io"
equal: ['cluster', 'service']

receivers:
- name: 'ops-critical'
webhook_configs:
- url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=CRITICAL_KEY'
send_resolved: true
email_configs:
- to: '[email protected]'

- name: 'ops-default'
webhook_configs:
- url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=DEFAULT_KEY'
# 加重试和限流配置
max_alerts: 50

- name: 'ops-business'
webhook_configs:
- url: 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=BIZ_KEY'

4.3 告警通道层:企业微信分群 + 邮件限流

企业微信机器人的限流是硬性 20 条/分钟,靠 Alertmanager 自己节流不够,必须在通道层做隔离:

  • 创建 3 个机器人:critical、business、default,各自独立 webhook
  • 创建 3 个群:核心告警群(仅 critical)、业务告警群(warning+)、综合告警群
  • 邮件也分收件人ops-critical@ops-business@ops-default@,分别给 oncall 工程师、业务负责人、邮件组订阅

邮件系统层面,给 Postfix 加上发件限流:

1
2
3
4
# /etc/postfix/main.cf
smtp_destination_concurrency_limit = 20
default_destination_rate_delay = 1s
smtp_extra_recipient_limit = 50

4.4 可靠性层:告警持久化 + 自监控

为了防止”告警队列满后丢告警”再次发生,做了三件事:

  1. 启用 Alertmanager 集群模式:部署 3 个 Alertmanager 节点,Gossip 同步告警状态
  2. 加告警自监控:在 Prometheus 里配置一个 meta-alert,监控 alertmanager_notifications_failed_total 是否在涨、alertmanager_notification_queue_capacity 是否接近满
  3. 关键告警双通道:critical 告警同时走邮件 + 钉钉 + 电话,缺一不可

五、根因分析

事后复盘总结,根因有四个层面,由浅到深:

1. 告警规则设计缺陷(最直接)——所有 warning 告警缺失 for 持续时间,把”瞬时抖动”当成”持续故障”处理,这是导致 2347 条告警同时触发的直接原因。for 是 Prometheus 告警体系里最容易被忽略、但又最关键的一个参数。

2. Alertmanager 分组策略过粗——group_by: ['alertname'] 看似在合并,实际只是合并了告警名,每条告警的实例标签还是作为多条独立记录存在,结果就是”组很大、消息很胖”。企业微信 API 对单条消息体大小是有限制的(20KB 左右),超长消息会被截断甚至拒绝。

3. 告警通道无分层——所有 severity 都走同一通道,告警风暴时 critical 和 warning 互相挤占资源,企业微信 20 条/分钟的限流被 warning 消息吃光,critical 消息根本发不出去。

4. 缺乏告警自监控——没有监控”告警系统本身是否健康”,导致 Alertmanager 队列满、丢告警这种”次生故障”完全不可见。这是最致命的,因为告警系统一旦失效,整个监控体系等于瞎了。

六、预防措施

针对这次故障,我梳理出 5 条长期预防措施:

1. 告警规则审计常态化

把告警规则检查纳入 PR Review 流程,CI 里加一个 promtool check rules 的检查,确保每条告警都有合理的 for 字段。同时每月做一次告警规则审计,清理长期不触发或永远在触发的”僵尸告警”。

2. 告警分级通道永久隔离

critical / warning / info 必须在 Alertmanager 配置里走完全不同的 receiver 和 webhook,禁止混用通道。任何 critical 告警必须同时配邮件 + 至少一种 IM 通道,且 receiver 配 send_resolved: true 确保恢复时也能收到通知。

3. 告警降级与抑制规则

完善 inhibit_rules,critical 告警触发时自动抑制同服务同 cluster 的 warning 告警,避免”父故障触发一堆子告警”。同时为不同业务系统配置”维护窗口”,在主动发布期间自动静音相关告警(用 mute_time_intervals)。

4. 告警系统自身监控

必须给监控组件本身也加监控,至少包括:

  • alertmanager_notifications_failed_total 增长率
  • alertmanager_notification_queue_size / queue_capacity 比值
  • prometheus_tsdb_head_series 增长趋势
  • prometheus_target_sync_failed_total

这四个指标任何一个出问题,本身就是 critical 告警。

5. 定期演练”告警风暴”

每季度做一次”告警风暴演练”——人为在测试环境触发一个能产生 1000+ 告警的故障,验证告警系统的承载能力、通道隔离效果、抑制规则是否生效。我后来在测试环境复现了这次的场景,发现 1000 条告警经过新配置后只发出去 23 条,群里完全可控。

七、总结

这次故障给我的最大教训是:告警系统的健壮性是 SRE 工作中最容易被忽视的一环。我们花了大量精力建设监控覆盖度,却很少花时间思考”告警系统本身挂了怎么办”。

几个值得长期践行的原则:

  • 告警是产品质量,不是越多越好。一条没人看的告警比没有告警更糟,因为它会消耗人对告警的敏感度。
  • for 字段是告警规则的灵魂。没有 for 的告警规则就是定时炸弹,迟早会在某次发布或重启时引爆。
  • 告警通道必须分层。critical 告警是”救命”的通道,绝不能和 warning 混用。
  • 监控监控本身。这是 SRE 领域一个朴素的哲学问题——你必须能监控到你正在监控这件事,否则就是盲人骑瞎马。

故障后我们重写了所有告警规则和 Alertmanager 配置,并补齐了告警自监控。半年后再回头看,告警群里基本只剩下真正需要人介入的告警,告警疲劳感大幅降低,运维同事的”告警响应时间”也从平均 25 分钟缩短到了 6 分钟。投资在告警治理上的时间,绝对是回报率最高的那部分。