跳到正文

生产故障场景处置 ​

一句话:线上问题归成四类——积压、丢失、重复、卡住;每类都按「现象 → 命令 → 判断 → 处置」走,先定位再动手。

适用版本:4.9 / 5.3 / 5.5。命令与参数一致,5.x 管理端也可用 Dashboard 替代部分 mqadmin 操作。

为什么需要 ​

故障现场最容易犯的错是「先重启」。重启会触发 Rebalance、丢本地缓存、改变位点,把可观测性一起清掉,问题反而更难查。

处置的前提是分清四类故障的因果:积压是消费速度问题,丢失是链路上某一环没落住,重复是至少一次语义的正常代价,卡住是消费线程或队列被阻塞。它们的命令和动作完全不同。

核心机制 ​

一、积压:生产大于消费 ​

现象:consumerProgress 的 diff 持续上涨,业务延迟变大;Producer 正常。

命令:

bash
# 每队列 broker offset / consumer offset / diff
sh bin/mqadmin consumerProgress -n 127.0.0.1:9876 -g order-consumer
# Topic 的 min/max offset 与最近写入时间
sh bin/mqadmin topicStatus -n 127.0.0.1:9876 -t order-topic
# Broker 是否在线、版本、TPS
sh bin/mqadmin clusterList -n 127.0.0.1:9876

判断:

观察结论
Consumer TPS 正常,但生产 TPS 更高突发流量或容量不足,扩消费或扩队列
Consumer TPS 很低,下游 RT 高下游变慢,先治下游
Consumer TPS 为 0,实例在线消费卡住,转第四类
Consumer 实例数 < Queue 数并行度没用满,先扩到 Queue 数
只有部分 Queue 积压热点 key 或 Queue 分配不均

处置:先扩 Consumer 到 Queue 数;仍不够就建一个 Queue 更多的新 Topic,用转发消费者搬积压消息,清零后切回。下游故障时先降级消费核心字段,不要直接丢弃。不要盲目重启 Consumer——Rebalance 会让消费暂停,积压更严重。

预防:Queue 数按目标 Consumer 数的 2 倍规划;监控 diff 和消费 RT;线程池、超时、批量参数按下游容量设置。

二、丢失:三个环节加一个保留期 ​

环节怎么丢处置
Producer → Broker异步发送没回调、超时没重试、发送成功但实际失败同步发送 + 重试 2 次 + 失败落库/outbox
Broker 内存 → 磁盘异步刷盘,宕机丢最后约 500ms同步刷盘,或异步刷盘 + 同步复制
Broker 磁盘 → 消费积压超过 fileReservedTime(默认 72h)被清理扩消费、调大保留时间、监控积压
Consumer 处理位点已提交,业务失败但异常被吞成功后再提交;失败返回 RECONSUME_LATER

命令:

bash
# 按业务 key 查消息是否到了 Broker(发送时必须 setKeys)
sh bin/mqadmin queryMsgByKey -n 127.0.0.1:9876 -t order-topic -k order-12345
# 按 msgId 查(从发送结果或日志里拿)
sh bin/mqadmin queryMsgById -n 127.0.0.1:9876 -i <msgId>
# 队列最大位点 / 消费位点
sh bin/mqadmin topicStatus -n 127.0.0.1:9876 -t order-topic
sh bin/mqadmin consumerProgress -n 127.0.0.1:9876 -g order-consumer

判断:

  • queryMsgByKey 查不到且发送方日志失败 → 生产端丢失。
  • 查不到但发送方显示成功 → 查是否发到别的 Topic/Queue、Broker 是否在刷盘前宕机。
  • 查得到但 consumer offset 已越过 → 消费端「消费了但业务没成功」,查异常吞噬和事务边界。
  • 查得到且位点没越过,但消息时间超过 72h → 已被保留期清理,属于积压导致的丢失。

兜底:本地消息表 / outbox

关键是业务写入与 outbox 写入在同一个数据库事务里,消息发送失败不会造成「业务成功但消息没了」。定时补偿是兜底,不是主路径;扫描要读从库、按 ID 分片,避免压主库。

对账:每天比对「生产侧业务单号集合」与「消费侧已处理集合」,差集补发。它不依赖中间件承诺,是最后一道防线。

三、重复:至少一次语义的代价 ​

RocketMQ 承诺至少一次,不承诺恰好一次;并发消费默认重试 16 次、累计约 4 小时 46 分,这段时间内都可能重复。重复来源:Producer 超时后重试(第一次可能已写入)、消费失败重试、Rebalance 后位点回退、人工重置位点。

幂等三件套:

  1. 数据库唯一约束:业务唯一键(订单号 + 操作类型)建唯一索引,重复插入捕获冲突并视为成功。
  2. Redis 去重:SET key 1 NX EX 3600,key 用业务唯一 ID;必须设过期时间。
  3. 状态机:只允许合法状态迁移(待支付 → 已支付),已处理状态直接返回。

处置:先统计重复范围(按业务键分组计数),修复消费端幂等,再谨慎重放;不要用「删除重复数据」代替幂等修复。

四、卡住:不报错但不动 ​

现象:diff 上涨,Consumer 进程存活、CPU 不高、日志没有异常。

常见原因:消费线程池被慢调用占满;数据库行锁/表锁等待;Full GC;顺序消费的前一条消息一直失败;Rebalance 未完成,Queue 没有归属。

指令:

bash
jstack <pid> | grep -A 20 "ConsumeMessageThread"   # 线程卡在哪
jstat -gcutil <pid> 1000                           # GC 频率与停顿
sh bin/mqadmin consumerConnection -n 127.0.0.1:9876 -g order-consumer
sh bin/mqadmin consumerProgress -n 127.0.0.1:9876 -g order-consumer

数据库锁等待要用对应数据库自己的视图查(PostgreSQL 的 pg_stat_activity、MySQL 的 information_schema.innodb_trx),并标明环境;不要在生产直接执行未确认的 SQL。

处置:下游超时 → 调小超时、加熔断、扩连接池;数据库锁 → 找到阻塞事务,确认影响后终止;GC → dump 堆,临时重启要配合流量摘除;顺序消费阻塞 → 给 maxReconsumeTimes 设上限,让坏消息进 DLQ 后放行,修复后从 DLQ 重发。

不要用「空消息替换失败消息」放行。这会制造假消息、丢失真实数据,还掩盖失败原因。正确做法是让失败消息进 DLQ,或停掉该 Queue 修复后重放。

最小上手 ​

bash
# 1. 记录积压基线
sh bin/mqadmin consumerProgress -n 127.0.0.1:9876 -g order-consumer
# 2. 确认消息是否存在
sh bin/mqadmin queryMsgByKey -n 127.0.0.1:9876 -t order-topic -k order-12345
# 3. 确认队列位点
sh bin/mqadmin topicStatus -n 127.0.0.1:9876 -t order-topic
# 4. 重置位点(高风险,先停消费或确认可重复)
sh bin/mqadmin resetOffsetByTime -n 127.0.0.1:9876 \
  -g order-consumer -t order-topic -s "2026-10-24#08:00:00"

resetOffsetByTime 的边界:只重置原 Topic 上该消费组的位点,不处理 %DLQ%;影响组内所有队列,回退后会产生大量重复;时间格式通常是 yyyy-MM-dd#HH:mm:ss;消费端在跑时内存位点可能覆盖重置结果,稳妥做法是先停消费、重置、再启动。

生产动作 ​

  • 告警分级:diff 超阈值、DLQ 增长、发送失败率、Broker 磁盘 75/85/90% 分别告警。
  • 每个场景有 runbook:命令、判断分支、回滚方式写清楚,避免现场临时发挥。
  • 发布前检查消费幂等:重复不是异常,是常态;没有幂等不要上线。
  • 保留窗口与积压联动:积压接近 fileReservedTime 时优先扩消费,不要调大保留时间掩盖问题。
  • 切换/重放后对账:以业务单号为准,不只看 MQ 位点。

边界 ​

  • 重启不是处置手段。它只清状态,不解决下游慢、锁等待、坏消息。
  • 重置位点不是重投 DLQ。DLQ 消息要重发回原 Topic,靠控制台或专门的搬运消费者。
  • 「至少一次」不能通过配置变成「恰好一次」。事务消息也只保证生产侧,消费侧仍要幂等。
  • 对账不能替代实时可靠性,但它是发现「以为没丢」的最后手段。
  • 不要在生产执行未标环境的 SQL,也不要用空消息、假消息让队列「看起来通了」。

讲清楚 ​

线上故障先分四类。积压看 diff 和消费 TPS:TPS 正常就是容量不够,先扩 Consumer 到 Queue 数;TPS 很低就是下游慢;TPS 为 0 就是卡住。丢失要沿链路查:消息在不在 Broker、位点有没有越过它、保留期是不是过了;生产端用同步发送加重试加 outbox,Broker 端用同步刷盘或同步复制,消费端成功后再提交位点。重复是至少一次的正常现象,靠唯一索引、Redis 去重和状态机兜住。卡住就看线程栈、GC、数据库锁和顺序消费的队头阻塞。

处置时最忌讳两件事:一是不看指标就重启,二是用空消息或假消息把队列「放行」。前者会触发 Rebalance、清掉现场,后者直接制造数据错误。正确顺序永远是先定位、再动手、事后对账。

把一门技术讲透到既懂也会用