【背景】 我在一个支付对账链路里维护 Kafka 3.6(KRaft,3 broker)的消费者,日均消费 2000 万条。去年 Q3 出过一次事故:上游发了一条格式异常的消息(JSON 里 amount 字段是字符串 "null" 而不是数字),消费者反序列化抛异常 → 重试 → 再抛 → 再重试 → 整整 47 分钟消费停滞,后面积压 320 万条,对账 SLA 直接爆了。
复盘时团队吵了一架:这条"毒丸消息"到底该怎么处理?
【我看到的几种做法】
-
无限重试 + 告警等人来:Kafka 默认行为就是这样——consume 失败 offset 不提交,不断重试。好处是"不丢",坏处是一条坏消息能卡住整个分区 47 分钟。我们当时就是吃了这个亏。
-
N 次后进死信队列(DLQ):消费 5 次失败后发到
dlq.order-recontopic,人工或定时任务处理。很多团队的标准做法。但实际落地时我踩了两个坑:- DLQ 本身成了"垃圾场":查了一下,DLQ 里 60% 的消息过了 3 个月没人看,其中 40% 其实根本不需要处理(上游已经修复了数据格式);
- 重试 5 次的时间窗口不好定:太短,下游 DB 抖动一下就进 DLQ;太长,毒丸消息卡住分区更久。
-
直接跳过 + 记录日志:catch 异常,记日志,commit offset,继续消费。最激进但最"保活"。风险是:如果异常不是格式问题而是下游依赖故障(比如调外部 API 超时),跳过就等于丢消息。
-
按异常类型分级处理:反序列化失败 → 直接进 DLQ(这条消息重试 100 次也不会好);下游超时 → 重试 3 次后进 DLQ;业务逻辑错误(比如余额不足)→ 跳过 + 记审计日志。最精细,但实现和维护成本也最高。
【我的倾向】
我倾向 方案 4 的简化版:按异常类型分两层——
- 不可恢复异常(反序列化失败、数据格式错误):第一次就进 DLQ,不重试。理由:这条消息重试 100 次也不会好,重试只是在浪费分区。
- 可恢复异常(下游超时、DB 连接抖动):重试 3 次,指数退避(1s → 4s → 16s),仍失败进 DLQ。
关键的一点:DLQ 必须有 SLA。我们后来给 DLQ 配了 24h 未处理告警 + 一周未处理自动升级的机制,比"无限重试等人来看"靠谱得多。
但我不确定这是否过度设计——如果你的消息量不大(日均 < 100 万),方案 2(简单 DLQ + 人工定期清理)是不是就够了?
【想听大家聊的】
- 你们线上毒丸消息的处理策略是什么?有没有"看似合理但实际翻车"的例子?
- DLQ 的 SLA 你们怎么定?有没有人真的给 DLQ 配了自动处理或自动升级的机制?
- "按异常类型分级"这个粒度,在你们的团队里是不是 over-engineering?
- 有没有人试过"毒丸消息隔离队列"(isolation queue)——不是死信,而是把坏消息挪到一个低优先级队列里慢慢处理,不阻塞主链路?
抛砖引玉,想听大家线上真实的选择和踩坑。