让两个告警系统在真实流量上对赌:影子灰度跑出的第一批数据
我有两套告警系统。一套是跑了 8 个月的主力(WebhookWise,此前写过介绍);另一套是新写的一组小工具(hookstack:管道 hookrelay + 判官 hookjudge)。新系统测试全绿、演示流畅——但”敢不敢让它碰生产”这个问题,测试回答不了。
我的做法是让它们对赌:主力照常干活,同时把每一条真实告警复制一份喂给新系统,让它在旁边判、只记账、不通知任何人。两边的判定落在同一本账上,谁对谁错,数字说话。
结论先放前面:影子上线两天、35 条可比判定,一致率 88.6%——而且所有分歧都指向同一个 bug:恢复消息的语义在跨系统边界上丢了,断点有三层,每一层单独看都是正当设计。修完之后活跃流量的一致率是 100%。这次对赌最值钱的产出不是”新系统判得准”,而是:一致是廉价的确认,分歧行才是影子灰度真正的产品。
影子怎么接
主力系统里加一条转发规则:匹配所有告警,目标指向影子管道。于是每条真实告警在正常通知人类的同时,多走一条路:
1 | 真实告警 ──▶ WebhookWise 主链路 ──▶ 分析、抑制、通知人(一切照旧) |
影子侧刻意做了三个”不”:
- 不配通知通道——能打扰人的影子就不是影子;
- 不接回传——判官的结论停在自己的账本里,不回流成任何事件;
- 不触发深度调查——调查员已经在为主链路服务,影子再触发一遍只会让模型账单翻倍,不产生新信息。
这个设计还有一个免费的副产品:主力在转发信封里带上了自己的判定(重要度),判官收到后写下自己的判定——每一行账都自带”两个系统对同一条告警的两个结论”。不用人工标注,生产流量以生产的速率替你积累对比数据。
第一批数字
两天后拉账本:
| 可比判定 | 35 条 |
| 一致率 | 88.6% |
| 主力判 high 的 29 条 | 判官全部判 high ✅ |
| 主力判 medium 的 1 条 | 判官同判 ✅ |
| 主力判 low 的 5 条 | 判官 4 条判了 high ❌ |
成本侧也值得记一笔:同期主力 59 次判定只付费调了 1 次模型(排除名单 + 相似缓存扛掉了其余),判官 31 次判定付费 15 次(48.4%,单条约 $0.00026)。有意思的是交叉验证:主力用零成本规则判 high 的那些告警,判官花钱调模型得到的也是 high——便宜的判定路径没有冤枉告警,这个结论本身就值回影子的成本。
真正的问题在那 4 条分歧上。它们长得一模一样:主力说 low,判官说 high,路由都是”复用”。点开一看,全是恢复消息——告警解除的那条 🟢。
一个 bug,三层正当设计
主力对恢复消息判 low(条件已解除,不值得打扰),判官却当成了新告警。排查下来,恢复语义在跨系统的路上丢了三次,而每一层单独看都有充分的设计理由:
第一层:提取模板丢了字段。 主力的转发信封里明明白白写着 is_recovery: true,但影子管道的提取模板只认 title / body / level / fields 四个键。那把它塞进 fields 行不行?恰恰不行——fields 构成告警的身份(identity),而恢复标志在 firing 和 recovery 之间必然翻转,放进去会把一对告警劈成两个身份,恢复就永远找不到它所属的触发。这条禁忌在配置注释里写得清清楚楚,是对的;只是它的推论——“恢复标志必须走身份之外的通道”——当时没人实现。
第二层:账本列白名单蒸发了它。 管道是先落库再投递的(要重试、要死信,这是可靠投递的正确做法),投递时消息从数据库列重建。就算第一层修好、提取出了标志,账本表里没有这一列,它照样在门口和通道之间蒸发。修复是一个三态列:NULL 表示”上游没说”(下游保留自己的判断力),0/1 表示上游明确说了。
第三层:关键词嗅探被复用摘要骗过。 判官自己有恢复检测——在 title/body 里嗅”已恢复 / resolved / cleared”这些词,设计哲学也是对的:”是否恢复是告警的事实,不该问模型”。但主力的恢复卡片有个实现细节:恢复复用其触发告警的分析结果,摘要原文里一个恢复词都没有,嗅探必然落空。修复是让明说的事实优先于嗅探:信封带了显式标志就信它,没带才回退关键词。
三层修完,用签名的合成对打进影子门口验证(firing → 付费判定,recovery → 零成本复用触发时的结论),再等真实流量过境确认。之后的活跃流量,一致率 100%。
这个故事对我最大的提醒是:跨系统 bug 的每一层都可以是局部正确的。提取模板不进 fields 是对的,账本列白名单是对的,嗅探哲学是对的——三个”对”叠在一起,恢复消息就变成了新告警。单元测试在每一层都会通过,只有让真实流量走完全程的影子,才能把它钓出来。
一条两边都没错的分歧
修完之后账本里又出现一条分歧,点开是个好东西:Grafana 的混合通知——[FIRING:1, RESOLVED:1],同一条消息里一个实例还在触发、另一个已经恢复,状态字段写着 firing。
主力按状态判”非恢复”,没错;判官复用了同身份的上一个判定,也不算错——上游语义本来就是模糊的。这条被记录在案而不是被”修复”:影子的分歧行里,有些是 bug,有些是领域里真实存在的灰色地带。分不清这两者的降噪系统,才是危险的降噪系统。
让对赌变成常驻数字
首批分析是我手工对的账。既然两个判定本来就在账本的同一行里,一致率就该是个随时可看的数字——判官的 /status 现在带一个 agreement 块:可比条数、一致率、判定矩阵、最近的分歧样本,看板上多了第五张卡。
两个统计口径的细节,比数字本身更值得写下来:
- 恢复被排除在一致率之外——它的重要度是从触发告警继承的(设计如此),计入会制造判官从未表达过的”一致”;
- 分歧样本直接进接口——一致率跌了,第一眼就该看到是哪几行、什么形态,而不是再去翻账本。
另外给影子对配了监控告警。理由和一般服务不同:影子挂了不影响任何通知,它静默地停掉的是对比数据的积累——这正是影子系统独有的故障模式:死了没人疼,疼的是三周后想看数据的你。
收尾
影子灰度不是新发明,做完这轮我对它的理解具体了很多:
- 一致是廉价的确认,分歧才是产品。 88.6% 那个数字本身没什么信息量,4 条形态一致的分歧行直接指向了一个三层 bug 和一个边界案例。
- 对比数据是免费的。 只要转发信封带上上游自己的判定,每条生产告警就是一条免标注的对比样本。
- 验证要能不打扰人。 “只记账、不通知”让我敢把全部真实流量喂给一个新系统——如果影子需要小心翼翼地选流量,它能钓到的 bug 也会小心翼翼。
新系统什么时候敢接真正的通知?等分歧行连续安静得足够久,而且每一条历史分歧都有解释。在那之前,让它继续对赌。
两套系统都是开源的:WebhookWise(大而全的告警网关)与 hookstack(小而专的管道/判官/调查员三件套)。文中规则名已脱敏。