消息队列告警怎么调才不吵:换判据、拆规则,而不是抬阈值
一条队列积压告警,30 天里 721 个小时有 26 个小时超过阈值 —— 平均每天误报一次。到这个程度,值班的人已经不看它了,它等于不存在。
调这类告警时最容易走的一条路是抬阈值。这篇记录的是为什么那条路是错的,以及正确的做法是什么。
结论先放前面:告警吵,通常不是阈值定低了,是判据选错了。抬阈值会在压掉误报的同时,把真故障一起压掉 —— 因为误报和故障的量级往往是重叠的。真正该做的是找一个能把两者结构上分开的判据:换指标、拆规则、或者用持续时间。
1. 现场:一次”看起来很像故障”的误报
某天下午,unacked(已投递未确认的消息数)冲到 2405,而正常水位的中位数是 4。告警响了。
查下来不是故障:
- 单一队列贡献了其中 99.8%;
ready(在队列里排队等待投递的消息数)全程为 0 —— 消息一进队列就被取走,没有一条在排队;- 消费者数从 30 自动扩到了 150;
- 20 分钟后自愈;
- 三个死信队列的峰值和当前值都是 0 —— 2400 条全部处理成功。
这是一次邮件批量突发,系统的表现完全正确:来了大量消息,自动扩容,全部处理完,一条没丢。
而当时告警的 summary 写的是「处理卡死、超时、或消费者崩溃未重连」。在这个场景下,这句话是误导性的 —— 它把一次成功的弹性伸缩描述成了故障。真正的卡死长这样:unacked 高 且 消费者数不变或归零 且 不下降。跟这次的情况恰好相反。
2. 为什么抬阈值是错的
第一反应是把阈值从 300 抬到 3000。看看数据同不同意。
那条队列 30 天的分布:
1 | P50 = 0 P90 = 0 P99 = 1800 最大 = 2821 |
基线是 0,尖峰上千。 这个形状说明:抬阈值到 3000,确实不会再为批量突发报警了 —— 但也意味着”几百条消息卡死”这类真实故障再也不会触发。而几百条消息卡在队列里,恰恰是最需要知道的那种故障。
问题的本质是:误报的量级和故障的量级重叠。任何单纯的阈值,都不可能把它们分开。
早先还试过另一个方向:把 for(持续时间)从 5 分钟拉长到 15 分钟,指望”熬过”突发。这个方向也是错的 —— 那是拿真实故障的发现速度去换误报率。误报应该由更精确的判据消除,而不是让所有告警都变迟钝。后来把 broker 级的 for 调回了 5 分钟,让它退化成一张粗网,精确判定交给队列级规则。
3. 办法一:用持续时间区分(尖峰签名很干净的场景)
回头看那条邮件队列的尖峰,5 分钟粒度下签名非常干净:
- 每次都落在同一个时间窗(UTC 08:10–08:20,北京时间下午 4 点过);
- 持续 10–20 分钟后回到 0;
- 消费者同时从 30 自动扩到 150。
这是一个每天一次的定时批量发信,不是故障。它和真故障的区别不在量级,在持续时间:批量突发 20 分钟内自愈,卡死不会。
所以这条规则单独拆出来,配置反过来调:
1 | 阈值 300 → 100 (更灵敏,几百条卡住也能发现) |
阈值降低、持续时间拉长。 结果是既能抓到小故障,又不会为定时批量报警 —— 因为定时批量撑不过 30 分钟。
这里的前提是尖峰签名足够干净。如果批量作业的时长本身就飘忽不定,这招不成立,得换判据。
4. 办法二:换一个能区分状态的指标
另一条队列画像完全不同,用持续时间分不开,得换判据。
30 天、5 分钟粒度、8641 个样本:
| 指标 | P50 | P90 | P99 | 最大 |
|---|---|---|---|---|
unacked |
2 | 17 | 47 | 2500 |
ready |
0 | 0 | 0 | 14148 |
关键差别在这里:
unacked有稳定水位(P50=2、P90=17),而且它在消费者忙碌时会正常升高。所以unacked高,可能是”在忙”,也可能是”卡住”,它分不开这两件事。ready非零只占 0.76% 的时间。正常情况下消息一进队列就被取走,ready恒为 0。它一旦持续非零,就意味着消息在排队等不到消费者 —— 这才是积压的定义。
用一次真实积压对照:ready=14148、unacked=2500、持续 185 分钟。两个指标都高,但只有 ready 在平时是干净的 0,所以只有它能当判据。
选指标的判据是”平时是否安静”,不是”故障时是否升高”。故障时会升高的指标很多,平时保持安静的很少。
5. 一个 CloudWatch + Grafana 的标签坑
这个坑很具体,但会让队列级告警看不出是哪个队列。
用 CloudWatch 的 SEARCH 表达式一次抓多个队列的指标时,Grafana 会把队列名放进 Series 标签。但是:
1 | 多队列 SEARCH(返回多条序列) Series = 队列名 |
当 SEARCH 只返回一条序列时,Grafana 把 Series 设成了指标名,不是队列名。
后果有三个:
- 告警消息里的对象名显示成
MessageUnacknowledgedCount之类的指标名,看不出是哪个队列; - 按
Series做的静默会失效; - 以后同一指标再加别的单队列规则,
Series值一样,会互相撞上。
修法是给单队列规则显式加一个 queue 标签,不依赖 Series。加标签不改变路由去向(复核过仍然命中原来的接收组),并且写进文档:以后新建单队列规则照做。
顺带还有一个相关的:告警模板里”对象名”的取值链如果没有覆盖 Series 这一级,队列级告警就只显示一个数字。原文是这样的:
1 | [MQ] 死信队列有消息 702.00 ← 哪个队列? |
补上之后:
1 | 702.00 xxx-event.dlq |
告警里必须有对象名。 一个只有数字的告警,收到的人第一件事是去翻看板 —— 那这条告警就没有完成它的工作。
6. summary 是写给凌晨三点的人看的
顺带说一下告警文案。原来那条 summary 是从设计文档里搬过来的,写了一大段可能的原因。前面第 1 节说了,它在批量突发的场景下是误导的。
重写之后包含四样东西:
- 两种情况怎么区分 —— 卡死是
unacked高 + 消费者数不变或归零 + 不下降;批量突发是消费者会扩、会自愈。 - 定位到具体队列要怎么做 —— broker 级指标看不出是哪个队列,得用 CloudWatch 的 Queue 维度。
- 怎么确认有没有真丢消息 —— 查对应的死信队列。
- 一次实测数据当参照 —— 上次批量突发是 2405/20 分钟/自愈。
有参照数据这一条最有用。凌晨三点看到”2405”,不知道这算多还是不算多;知道”上次 2405 是正常的批量突发”,判断就快得多。
7. 一个没有采纳的方案,和它的代价
一度考虑把 CloudWatch 那套换成 Prometheus,用 cloudwatch-exporter 统一到现有的监控栈里。算完账放弃了:
- 加上队列维度之后,单次抓取从 45 秒涨到约 220 秒;
- 而
scrape_timeout是 90 秒,超时会连带丢掉这个 exporter 已有的全部指标 —— 为了加新指标把老指标搞挂,得不偿失; - 费用还反过来涨了(CloudWatch API 调用量),每月约 160–200 美元。
所以这套队列级告警直接用 CloudWatch 当数据源,是这个 Grafana 里的首例。统一技术栈是有价值的,但不是无限价值的;当代价是”抓取超时把现有监控一起拖垮”,就该停下。
8. 小结
- 告警吵,先怀疑判据,再怀疑阈值。 抬阈值会同时压掉误报和小故障,因为两者量级重叠。
- 拉长
for来压误报要小心:那是拿故障发现速度换的。误报该由精确判据消除。 - 尖峰签名干净(固定时段、固定时长)时,用降阈值 + 拉长持续时间,既灵敏又不吵。
- 选指标看平时是否安静:
unacked有水位且忙碌时会升,分不清”在忙”和”卡住”;ready平时恒 0,非零就是真排队。 - CloudWatch SEARCH 只返回一条序列时,Grafana 的
Series会变成指标名 —— 单队列规则要显式加queue标签。 - 告警里必须带对象名,只有数字的告警等于没写完。
- summary 里放一条上次的实测数据,比写一堆可能原因有用。