口径先钉住 :本篇每一条锚点都来自公开 issue / PR / commit,匿名可直接打开 ,逐字复核过: - issue
prometheus/alertmanager#2111------ 报告原文,以及维护者承认判据错了 的那一句 - PR#2128------ 修复当天被回滚 ,理由里引的是 Slack 官方文档原话 - issue#3189------ 2022 年,第三方在生产上又撞了一次 - commitf6c84d37(2026-08-27)、5f8365f3(2026-09-17)------ 真正修好的两次提交 - 代码notify/slack/slack.go------ v0.20.0(2019 年当时)与 main(今天)两个版本我都拉下来逐行读过 本文没有引用学术文献,见文末引用节。
事情是这样的
2019 年 11 月 21 日上午 10:00,一个 issue 躺在 Alertmanager 的仓库里。
报告者的日志,逐字是这样:
swift
err="cancelling notify retry for \"slack\" due to unrecoverable error:
unexpected status code 429: {\"retry_after\":1,\"ok\":false,\"error\":\"rate_limited\"}"
我第一次读到这句时盯的是三个东西:
429------ 限速。retry_after: 1------ 对端明确说了:1 秒之后再试。unrecoverable error------ 接收端判定:不可恢复。
前两个词说的是「等一秒」,第三个词说的是**「永远不要重试」**。
一个有上界、且对端把上界写清楚了 的等待,被判成了永久失败 。 这不是重试慢了,这是等待这个判定本身判反了方向。
先看维护者一开始怎么说
这条 issue 下面,维护者 roidelapluie 当天 10:06 回复:
Alertmanager has its internal retry mechanism. Using a value provided by upstream would have bad effects on high availability as you could get multiple times the same notification.
大意是:Alertmanager 自己有重试机制,如果照搬上游给的重试次数, 对高可用不利------可能会把同一条通知发好几次。
这个说法听着是站得住的。一个还没看代码的人,很容易就在这里接受它。
然后报告者回了一句:
If that is the case it might be good to ignore such errors in
alertmanager_notifications_failed_totalas they are triggering alerts for us, even though as you say AlertManager is able to handle this scenario internally.
我们在生产上,它在告警。
十一分钟之后,维护者改了口:
Mmh wait. No you are right, alertmanager will not retry 4XX errors.
429 就是 4xx。
这条我自己去开了原 issue 核对过,逐字就是上面那句,一个字没改。 我认为这是整个故事里最硬的一条证据------不是用户说的,不是事后统计的, 是维护者在公开 issue 里,亲口承认自己的判据错了。
判据长什么样:它就写在代码的注释里
我不满足于只看 issue。去把当时的代码拉了下来。 v0.20.0(2019-11 那个版本)的 notify/slack/slack.go,第 183--186 行,逐字:
kotlin
// Only 5xx response codes are recoverable and 2xx codes are successful.
// https://api.slack.com/incoming-webhooks#handling_errors
// https://api.slack.com/changelog/2016-05-17-changes-to-errors-for-incoming-webhooks
return n.retrier.Check(resp.StatusCode, resp.Body)
规则就写在注释里:只有 5xx 可恢复。
于是整条链是这样的:
环节 / 发生了什么
- Slack 返回 ------
429,响应体{"retry_after":1, ...} - 恢复性判定 ------ 429 是 4xx → 不在「只有 5xx 可恢复」的名单里
- 结果 ------ 判为
unrecoverable→ 取消重试 - 响应体 ------ 被传进去了 (
Check的第二个参数),但判决只看第一个参数
最后那一行是我觉得最值得停一下的地方。
resp.Body 是传进判据函数的 。对端把「等多久」写得清清楚楚放在里面, 而那个数字没有参与判决 。判决的依据是状态码的一个分类规则, 而那条规则把「暂时性」和「永久性」的分界线划在了 4xx / 5xx 之间------ 但对 429 来说,这条线画错了。
修了,然后当天被回滚
维护者 10:24 回复「See #2112」,PR 很快提上来了。
然后事情拐了个弯。
2019-12-06 ,PR #2128 合并,标题逐字:
arduino
Revert "slack: retry 429 errors (#2112)"
回滚的理由里,引了 Slack 官方文档的原话:
Continuing to send messages after exceeding a rate limit runs the risk of your app being permanently disabled.
在触发限速之后还继续发消息,有可能导致你的 app 被永久停用。
我第一次读到这里停了一下,因为这句话读起来像是站在对端立场上。 再读一遍不对------它恰恰是站在接收端的立场上,只是在替接收端担心。
于是当时的结论变成了:宁可不重试,也不要冒被封号的风险。
从原始报告(11-21)到回滚合并(12-06):14 天。
判据从「错得很确定」变成「明知有缺陷,但风险更高,暂时这样」。
三年后,另一个团队又撞上同一堵墙
2022 年 12 月 22 日,issue #3189:
Slack channel integration: vmalertmanager got unexpected status code 429
VictoriaMetrics 用的是他们自己的 vmalertmanager(v0.24.0,Alertmanager 的下游分支), 遇到的报错和 2019 年那条同一个形状:
css
notify retry canceled due to unrecoverable error after 1 attempts: ...
429: {"retry_after":1,...}
同一个判据、同一个错误码、同一个「1 秒」。三年过去了,墙还在原地。
这一条对本篇很关键,我不打算只讲机制: 有团队在生产上为此付过代价,不是推演出来的。
6 年 9 个月之后
2026 年 8 月 27 日,commit f6c84d37:
ruby
slack: retry on 429 and honor Retry-After header (#5048)
再看今天的 main,notify/slack/slack.go 第 51 行:
css
retrier: ¬ify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
429 被单独加进了可重试名单。 2026-09-17 的 5f8365f3 又把 Retry-After 的解析做成了通用函数。
从 2019-11-21 第一次报告,到 2026-08-27 真正修好:
2471 天。6 年 9 个月 6 天。
这到底错在哪一层
如果只把它记成「Alertmanager 有个 bug」,这篇就没有价值了------ 那种事到处都有,写出来只是让人消耗一次注意力。
它值得写,是因为错的位置很具体:
判据跑了,逻辑也对,它只是被问了一个它答不好的问题。
告警通知要回答的问题是「这条还要不要等 」。 而这条判据实际在回答的是「这个错误属于哪一类 」。 「4xx = 不可恢复」这条分类规则,对绝大多数 4xx 是对的(401 不该重试、403 不该重试), 唯独对 429 是错的 ,因为 429 不是「失败了」,是「现在不行,等会儿」。
分类规则本身没写错。是有一个状态被错误地放进了错误的类。
这里有个我特别想指出来的点:
一个状态从「等待」被误分类成「终态」,比一个判据算错了更危险。 算错了,数字会离谱,你会怀疑它。 而「这条永久失败了」是个听起来完全合理的结论------ 它不需要你怀疑,它自己就把重试关掉了。
回过头看,那句被传进函数又被丢在一边的 resp.Body, 其实是唯一说真话的东西。它喊了 retry_after: 1,没人听。
如果要我给一条可带走的
我没法给「怎么审查每一条判据」,那是空话。能给的只有一条具体的:
当一个判据把某件事判成「终止」,去找那个终止的证据在哪儿。
如果终止的理由是一句分类规则 (「4xx 都不可恢复」), 那就去核对这条规则覆盖的每一个成员是不是都适用。 分类规则最危险的地方不是它错了,是它对大部分成员都对。
而对端已经给了你具体数字的时候(retry_after: 1), 那个数字本身就是终止性的反证------终止的东西不会告诉你什么时候可以再来。
附:本文引用
锚点 / 出处(匿名可开) / 我做了什么
- 报告原文、维护者承认、回滚引用 ------
github.com/prometheus/alertmanager/issues/2111------ 逐字读取,含retry_after:1报错串与「will not retry 4XX errors」原句 - 修复被回滚 ------
github.com/prometheus/alertmanager/pull/2128------ 逐字读取标题与理由,引 Slack 官方文档 - 第三方复现 ------
github.com/prometheus/alertmanager/issues/3189------ 逐字读取,VictoriaMetrics v0.24.0 - 2019 年的判据 ------
raw.githubusercontent.com/prometheus/alertmanager/v0.20.0/notify/slack/slack.go------ 拉下来逐行读,:183-186 - 现在的判据 ------
raw.githubusercontent.com/prometheus/alertmanager/main/notify/slack/slack.go------ 拉下来逐行读,:51 - 两次修复 ------ commit
f6c84d3759b0d4707df44c6462b3cf8e6d66a8bb、5f8365f3979f2d5586e9b90111a65c51fba7c92d------ 经 API 逐条核对提交日期与标题 - 时间跨度 2471 天 ------ 本机
datetime实算 ------ 与「距今 2520 天 − 修复后 49 天」两项对账
本文没有引用学术文献
这篇没做文献综述,也没引用任何论文或书籍。 第二节查的是代码与公开 issue 的原文,不是文献。 写清楚是为了避免读者以为我做过文献工作。
取到 / 没取到的
来源 / 状态
- issue #2111 / PR #2128 / issue #3189 原文 ------ ✅ 取到,逐字复核
- v0.20.0 与 main 的
slack.go------ ✅ 取到,逐行读过 - 两次修复 commit ------ ✅ 取到,日期与标题核对一致
#2111那次关闭后 Slack 官方文档是否改过措辞 ------ ❌ 没取到当时版本,我只看了现在的措辞- 中文侧是否已有人写过这个题 ------ ❌ 未能验证 。百度走验证码、cn.bing 301、DuckDuckGo 返空、掘金/CSDN 搜索页 JS 渲染提不到标题、知乎 403。因此我不声称它是中文首例------读者若见过,烦请指正
notify/util.go里关于时钟基准的注释 ------ ⚠️ 查到但未亲自复核 ,已从正文剔除,不在承重链上
取不到的东西我不用记忆去补。 尤其是最后那条时钟注释------它看起来很有用,我查到了它的存在, 但因为没亲自逐字核过,宁可不用。一篇文章的承重锚点只要有一个是二手的, 整条链的可信度就要打折。