【背景】
我负责一条日处理 2000 万+ 消息的订单履约链路(Kafka 3.5 + Spring Kafka 3.1,16 分区 × 4 consumer),去年做了一次消费链路审计,发现三件让我后背发凉的事:
- 死信队列(DLT)里积压了 47 万条消息,最早一条是 2 年前发的。告警规则写的是「DLT 消息数 > 100 触发」,但阈值被一个误操作的配置变更改成了 100000,之后再没人动过。
- 有 3 类消费异常(下游 API 返回 5xx、DB 锁超时、序列化失败)被 catch 后直接
return,既没重试也没进 DLT——静默吞消息,且日志级别是 INFO。 - 团队里能说出「哪类异常该重试、哪类该直接进 DLT」的人,只有写最初那版代码的同事。他上个月转岗了。
这件事让我重新审视了一个我做了 8 年但从来没认真定义过的问题:消费失败的边界到底在哪,兜底策略到底谁拍板、谁执行、谁兜底?
【常见分歧 / 几种做法】
我调研了一圈(包括站内几篇 Kafka 相关文章的评论区),大致能归纳成四种流派:
流派 A:有限重试 + 死信 重试 3 次(间隔 1s / 5s / 30s),仍失败则投 DLT。DLT 配告警,人工介入。
- 优点:实现简单,Kafka / RocketMQ 原生支持;
- 坑:「3 次」是拍的;DLT 建了没人看(我上面的事故);可重试和不可重试异常混在一起,序列化失败也重试 3 次纯属浪费。
流派 B:区分异常类型,分路径处理 可重试异常(网络超时、锁冲突、下游 5xx)→ 指数退避重试 N 次;不可重试异常(参数校验失败、数据格式错误)→ 直接进 DLT + 告警。
- 优点:语义清晰,不浪费重试窗口;
- 坑:异常分类需要每个消费者自己判断,跨服务不统一;新同学写消费者时经常漏掉分类逻辑,退化成流派 A。
流派 C:无限重试(或极大重试窗口)+ 告警
不建 DLT,消费者 catch 后 throw 让框架自动重投,配一个很长的 max.interval 或 RocketMQ 的 16 级延迟队列。
- 优点:不丢消息;
- 坑:毒消息(poison pill)会卡住分区,整个分区消费停滞;重试风暴打爆下游;和流派 A 一样依赖告警,但告警的粒度更粗(你只知道「有消息一直没消费成功」,不知道是哪类)。
流派 D:不自动重试,全部走人工补偿 消费失败直接记一条补偿任务(DB 表 / 工单),由运维或值班同学手动处理。
- 优点:永远不会出现「重试风暴」;
- 坑:7×24 不可行;人工处理没有 SLA;2000 万/天的量级下,哪怕 0.01% 失败率也是 2000 条/天,人工根本兜不住。
【我的倾向与理由】
我目前推的是流派 B 的加强版,核心原则是「每条消费失败的消息,必须有且仅有一个确定性的终态」:
-
异常分类下沉到框架层,不是每个消费者自己写 try-catch。我们在 Spring Kafka 的
ErrorHandler里统一拦截,按异常类型走不同路径:TransientException(网络超时 / 锁等待 / 下游 5xx)→ 指数退避重试,上限 5 次,间隔 2s → 32s(full jitter);FatalException(反序列化失败 / 参数校验 / 业务规则不满足)→ 不重试,直接进 DLT;- 兜底:未知异常按 Transient 处理(宁可多重试几次,不可静默丢)。
-
DLT 必须有消费方,不是「扔进去就完事」。我们的 DLT 消费者每天跑一次,按异常类型聚合,生成日报推运维群;超过 7 天未处理的自动升级。这一步不是技术活,是 SOP + 值班制度,没有制度的 DLT 等于没有 DLT。
-
可观测性:每次消费失败打一条结构化日志(messageId / topic / partition / offset / exceptionType / retryCount / finalState),接 ELK 做看板。我们现在的 SLO 是「DLT 存量 < 1000 且 7 天清零率 100%」,违反了就触发 P2 工单。
-
Poison Pill 防护:重试超过上限后,在 offset 元数据里打一个
poison=true标记,后续 rebalance 时跳过这条消息(不卡分区),同时触发告警。这个逻辑是我们自己实现的,Spring Kafka 原生没有。
但我承认这套东西重,一个 5 人小团队可能维护不起。所以我想听大家的。
【想听大家在什么场景下怎么选】
- 你的消息量级是多少(万/天 vs 百万/天 vs 千万/天)?量级不同,流派 D 的可行性天差地别。
- 你们用 Kafka 还是 RocketMQ 还是 RabbitMQ?不同中间件对 DLT / 延迟重试的原生支持差异很大,你们是怎么补齐的?
- 你们有没有遇到过「DLT 建了但没人看」的情况?最后怎么解决的——是加了值班制度,还是干脆把 DLT 消费方做成自动补偿?
- 异常分类这件事,你们是框架层统一做,还是每个消费者自己写?踩过什么坑?
- 你团队的消费失败 SOP 是写在 wiki 里、还是嵌在代码 review checklist 里、还是……根本没有?
我特别想听「量级不大(< 10 万/天)但业务关键(资金 / 风控)」的团队怎么做的——这种场景下流派 C 和 D 的边界最模糊,也最容易出事。