事故是最好的教材
这一篇没有新知识,只有七起「别人已经替你踩过」的坑。每起事故三段式:现象、根因、修复。读的时候请对号入座——像不像你上周写的代码?
事故一:异步发送吞异常,三千张发票凭空消失
现象:月底对账,3000 笔订单没开发票,日志里一片干净。根因:异步发送的 onException 回调是空的,网络抖动时消息无声丢失(第 5 篇死法二)。修复:回调异常落库重试 + 告警;核心链路上本地消息表。
事故二:自动提交 offset,批处理「被完成」
现象:Kafka 消费者重启后,一段消息彻底没了。根因:enable.auto.commit=true,poll 拉到消息就按周期自动提交,业务处理到一半崩溃,位点已经飞了(第 6 篇的坑)。修复:关自动提交,处理完成后手动 commitSync;Rebalance 监听器里补一次提交。
事故三:Redis 幂等失效,优惠券发成双份
现象:用户投诉券发了两张。根因:用 setnx 做幂等开关,但 Redis 与数据库不是原子:setnx 成功后业务落库前 Redis 宕机重启,key 丢了,重投的消息畅通无阻(第 7 篇警告过)。修复:去重表与业务同事务,Redis 降级为纯加速层。
事故四:Rebalance 风暴,重复扣款 400 笔
现象:发布期间支付通知消息大量重复消费。根因:单批消息里有一条调外部接口要 8 秒,一批 500 条处理超过 max.poll.interval.ms,Kafka 误判消费者死亡触发全组 Rebalance,位点回退重放(第 14 篇)。修复:max.poll.records 降到 50、外部调用设 2 秒超时、启用静态成员;扣款逻辑补上幂等键。
事故五:毒消息卡死顺序队列两小时
现象:某 Topic 八个队列里有一个消费 TPS 为零,其余七个正常。根因:顺序消费遇到坏消息原地挂起重试,没有上限也没有告警(第 8、11 篇的雷)。修复:重试上限 + 死信转出 + 队列级消费延迟监控。
事故六:一张 base64 图片打瘫整个集群
现象:Broker 发送耗时全面上涨,磁盘 IO 打满。根因:开发把 4MB 的商品图 base64 塞进消息,CommitLog 大文件混写被大消息搅乱,顺序写退化(第 13、15 篇)。修复:图片走对象存储,消息只传 URL;客户端限制单条消息上限。
事故七:手滑重置位点,旧消息灌进新系统
现象:凌晨告警,百万条历史消息涌入消费端,下游数据库连接打满。根因:值班同学排障时误把消费位点重置到最早(earliest)。修复:位点重置操作收权 + 双人复核;消费端限流保护;事后靠幂等与对账把数据对平。
根因归类表
| 根因类别 | 对应事故 | 系统防线 |
|---|---|---|
| 异常/确认被吞 | 一、二 | 手动提交、回调必处理、本地消息表 |
| 幂等不彻底 | 三、四 | 同事务去重表、状态机 |
| 异常路径无上限 | 五、七 | 重试上限、死信告警、操作收权 |
| 消息体失控 | 六 | 体积限制、引用代替内容 |
MQ 代码评审清单
- 发送:没有 oneway?异步回调处理了异常?核心链路有本地消息表?
- 消费:自动提交关了?业务成功后才 ACK?消费逻辑幂等吗?幂等是同事务的吗?
- 顺序:真的需要顺序吗?毒消息有上限和告警吗?
- 体积:消息里塞了不该塞的大对象吗?
- 兜底:对账任务有吗?死信有人管吗?
小结
七起事故,四类根因,每一起的修复都指向前面十几篇讲过的基本功。事故不源于知识盲区,源于「知道但没做」——清单的价值就是把它变成必答题。
下一站,收官:一张消息队列作战地图,20 篇内容收进一张表,出事时直接翻。
评论 (0)