返回文章列表

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook

系统梳理 RocketMQ 可观测性、Metrics、Logs、Traces、mqadmin 排查、消息生命周期诊断、Reset Offset、SLI/SLO、告警、生产 Runbook 和事故复盘。

第 15 章:RocketMQ 可观测性、故障诊断、应急处理与生产 Runbook

技术基线:本章以 Apache RocketMQ 5.x 为主要背景,同时指出 4.x 常见运维差异。截至 2026 年 6 月,Apache RocketMQ 最新正式版本为 5.5.0,发布于 2026 年 4 月 10 日。生产操作必须使用与服务端版本匹配的管理工具,并以本地执行 mqadmin help <command> 输出为最终依据。(RocketMQ)

本章去重边界与跳转

本章是可观测性、故障诊断和生产 Runbook 主讲章节。它不重新解释所有 RocketMQ 机制,而是把前面章节的机制落到指标、日志、Trace、排查顺序和应急动作上。

重复主题本章处理方式
Producer、Consumer、Broker、Proxy 的职责本章只用它们定位故障边界;组件模型看 第 2 章:整体架构、核心组件与领域模型
消费积压、Lag、Reset Offset 和 Rebalance Storm本章讲排查和应急;位点机制看 第 6 章:Rebalance、消费位点、负载均衡与消息积压
端到端可靠性、DLQ、Poison Message 和幂等本章讲告警和处理闭环;语义看 第 8 章:端到端消息可靠性
容量、压测和性能瓶颈本章讲线上定位;容量规划看 第 14 章:性能优化、流控、压测与容量规划
高可用、安全和灾备演练本章给 Runbook 入口;HA 看 第 13 章,安全灾备看 第 16 章

15.1 可观测性不是“装一个 Dashboard”

RocketMQ 故障往往跨越多个边界:

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook flow 1

“消息没有被处理”可能意味着:

  1. 业务根本没有调用发送接口;
  2. Producer 调用了接口,但没有取得路由;
  3. Broker 已存储消息,但 Producer 因响应超时认为失败;
  4. Broker 已存储,但 Consumer 没有拉取;
  5. Consumer 已拉取,消息仍在本地缓存中等待处理;
  6. Listener 已执行,但业务数据库写入失败;
  7. 业务成功,消费确认却失败,导致重复投递;
  8. 消费多次失败后进入重试主题或死信队列。

因此,可观测性要回答的不是“机器活着吗”,而是四个问题:

  • 现在发生了什么?
  • 影响了哪些 Topic、Group 和业务?
  • 消息生命周期停在哪一段?
  • 采取操作后,数据是否真正恢复正确?

15.1.1 Metrics、Logs、Traces 的分工

维度主要作用典型问题局限
Metrics发现趋势、异常和容量风险成功率是否下降、积压是否增长、磁盘何时写满聚合后缺少单条消息细节
Logs定位错误上下文哪个请求码失败、哪个 Group 订阅不一致数据量大,字段不规范时难检索
Traces串联单条消息生命周期消息何时发送、存储、投递和消费有额外网络、存储和查询成本
业务审计数据判断最终业务结果订单是否落库、账户是否记账需要业务自行建设,RocketMQ 无法代替

一个成熟系统必须把四类证据关联起来:

  • Metrics 用于发现事故
  • Logs 用于缩小范围
  • 消息轨迹和消息查询用于追踪消息
  • 业务幂等表、状态表和对账任务用于确认最终结果

15.2 指标体系设计

RocketMQ 指标应分成三层:

  1. 业务结果层:订单事件成功率、结算延迟、通知到达率;
  2. 消息生命周期层:发送、存储、投递、处理、重试、死信;
  3. 组件资源层:CPU、内存、磁盘、网络、线程池和复制状态。

只监控第三层会产生一种常见假象:所有机器 CPU 都不高,但业务消息已经延迟半小时。

15.2.1 Producer 指标

指标推荐定义诊断价值
逻辑发送成功率明确获得成功确认的逻辑消息数 ÷ 逻辑发送数反映 Producer 对业务提供的可用性
未知结果率超时、连接中断等无法判断 Broker 是否接受的消息数 ÷ 逻辑发送数发现可能产生重复消息的模糊失败
发送延迟从调用发送接口到取得最终结果的耗时,观察 P50/P95/P99/P999发现路由、网络、Broker 排队和刷盘异常
超时率发送超时次数 ÷ 逻辑发送数区分慢请求与明确拒绝
重试率发生过至少一次重试的逻辑消息数 ÷ 逻辑发送数识别网络抖动、Broker 局部异常
平均尝试次数总发送尝试次数 ÷ 逻辑发送数发现重试放大
限流率被客户端、Proxy、Broker 或业务网关限流的次数 ÷ 请求数判断是否达到保护阈值
消息体大小平均值与分位数大消息会放大网络、内存和磁盘压力
路由刷新失败率获取或更新 Topic 路由失败次数 ÷ 尝试次数定位 NameServer、Proxy 或配置问题

必须区分“逻辑消息”和“发送尝试”。一条业务消息重试三次,只能在业务发送量中计为一条,否则重试风暴会把 TPS 指标人为抬高。

RocketMQ 官方原生指标包含 rocketmq_send_cost_time,可以按 Topic、客户端及调用结果观察生产 API 延迟;但业务仍应自行补充超时、重试、限流、业务标识和未知结果等客户端指标。(RocketMQ)

15.2.1.1 Producer 日志建议字段

cluster、endpoint、topic、producer_group、client_id、
business_id、keys、message_id、queue_id、queue_offset、
attempt、result、response_code、latency_ms、trace_id

不要默认记录完整消息体、AccessKey、SecretKey、身份证号、手机号等敏感字段。

15.2.2 Consumer 指标

指标说明
拉取 TPSSDK 从 Broker 拉取消息的速率
业务成功 TPSListener 完成业务处理并准备确认成功的速率
处理耗时Listener 业务逻辑耗时的 P50/P95/P99
本地等待耗时消息进入客户端缓存到开始执行业务逻辑的时间
Ready 消息数Broker 中已经可消费、尚未被客户端拉取的消息数
Inflight 消息数已拉取但尚未提交消费进度的消息数
Lag 时间最早未完成消息距离当前时间的时长
本地缓存条数和字节数PushConsumer 客户端缓存中的消息规模
重试率进入失败重试的消息占比
DLQ 增量新进入死信队列的消息数
依赖失败率数据库、缓存、HTTP/RPC 等下游错误率
消费实例数Group 当前在线客户端数量
Rebalance 次数队列分配变化次数及持续时间

官方定义中:

  • Inflight 数量约等于“最新拉取位点减去最新提交位点”;
  • Ready 数量约等于“最大位点减去最新拉取位点”;
  • rocketmq_consumer_queueing_latency 表示 Ready 消息的排队时间;
  • rocketmq_consumer_lag_latency 表示最早未完成消息的延迟;
  • 客户端侧还包括 rocketmq_process_timerocketmq_consumer_cached_messagesrocketmq_consumer_cached_bytesrocketmq_await_time。(RocketMQ)

15.2.2.1 数量 Lag 与时间 Lag

仅监控积压条数是不够的。

假设两个 Group 都积压十万条:

  • Group A 每秒消费五万条,最老消息延迟 2 秒;
  • Group B 每秒消费一百条,最老消息延迟 40 分钟。

两者业务影响完全不同。因此生产告警应优先使用:

  1. 最老消息延迟;
  2. 积压增长速度;
  3. 预计清空时间;
  4. 业务时效性 SLO。

预计清空时间可近似计算为:

[ T_{\text{drain}}= \frac{\text{当前积压量}} {\text{消费速率}-\text{生产速率}} ]

当消费速率不大于生产速率时,理论上无法清空积压。

15.2.3 Broker 指标

RocketMQ 5.1.0 起提供的原生指标包括消息流入流出数量、字节吞吐、Ready/Inflight、DLQ、RPC 延迟、未分发字节、未刷盘字节和线程池排队水位等。官方页面目前明确标注这组原生能力“仅支持 Broker”,不能仅因指标标签中出现 Proxy 或 NameServer 就假设所有组件都提供同样的指标集。(RocketMQ)

类别必须监控的指标
流量写入/读取消息数、写入/读取字节数、消息大小分布
请求发送、拉取、查询、心跳等 RPC 的吞吐、错误率和延迟
排队Send/Pull/Query 等线程池队列长度、队首等待时间、拒绝次数
存储磁盘使用率、inode、写延迟、未刷盘字节、未分发字节
Page Cachemajor page fault、回收压力、swap、磁盘读延迟、映射文件访问抖动
JVMHeap、Metaspace、直接内存、GC 次数、停顿时间、线程数
主机CPU、load、内存、网络、文件描述符、上下文切换
复制主从位点差、复制字节差、复制时间延迟、同步副本集合健康度
数据保留实际消息保留时间、删除速度、剩余磁盘可用时间
元数据Topic 数量、ConsumerGroup 数量及创建失败率

Page Cache 不应简化为一个“命中率”。RocketMQ 大量使用 mmap 和操作系统页缓存,排查时要联看:

  • Broker JVM 是否发生 Full GC;
  • 系统是否开始 swap;
  • major page fault 是否激增;
  • 磁盘读延迟是否突然上升;
  • 热数据是否因其他进程挤占而变冷;
  • CommitLog、ConsumeQueue 和索引文件访问是否同时变慢。

15.2.4 NameServer、Proxy 和 Controller 指标

15.2.4.1 NameServer

  • Topic 路由查询成功率及延迟;
  • Broker 注册数量和最后心跳时间;
  • 路由表规模及异常变化;
  • TCP 连接、文件描述符、CPU、内存和 GC;
  • 从 Producer 所在网络执行的路由查询探针。

15.2.4.2 Proxy

  • gRPC/Remoting 请求量、错误率和延迟;
  • 活跃连接、活跃流、请求并发;
  • 路由刷新失败;
  • 访问 Broker 的错误率和耗时;
  • 线程池排队、拒绝数;
  • Heap、直接内存、GC、CPU 和网络;
  • 不同协议、请求码和响应码的错误分布。

15.2.4.3 Controller

  • Controller Leader 是否存在;
  • 多数派是否可用;
  • Leader 选举次数和耗时;
  • 元数据提交延迟;
  • Broker 主节点选举次数、失败率;
  • Controller RPC 延迟和错误率;
  • Controller 与 Broker 的网络连通性。

这些组件应以“进程指标+主机指标+功能探针”补齐监控,不能只做端口存活检查。


15.3 Dashboard、Exporter 与原生 Metrics

15.3.1 能力对比

工具适合用途优点注意事项
原生 MetricsBroker 高频监控、告警路径短、语义较新、支持 Prometheus 和 OTLP官方当前标注主要支持 Broker
RocketMQ Exporter兼容旧集群、补充 Broker/客户端管理指标通过 MQAdminExt 获取较丰富的信息有采集延迟、标签缓存和管理请求开销
Dashboard人工查询、临时诊断和管理查看集群、Topic、Consumer、消息和消费位点不应作为唯一监控和告警系统
mqadmin精确查询和变更操作功能直接、适合 Runbook属于高权限工具,误操作风险高
消息轨迹查询单条消息生命周期能关联发送和消费事件存储、网络、IO 和敏感数据成本

原生 Broker Metrics 可通过 metricsExporterType=OTLP_GRPC 上报至 OpenTelemetry Collector,也可设置为 PROM 后由 Prometheus 拉取;官方默认 Prometheus 端口为 5557。(RocketMQ)

RocketMQ Exporter 通过 MQAdminExt 请求集群数据,再规范化后从 /metrics 暴露;这意味着其结果存在采集周期,并会给 NameServer、Broker 和客户端查询带来额外负载。(RocketMQ)

Dashboard 支持集群、Broker 运行信息、Topic 路由、Consumer 状态、消息详情、消息轨迹和重置消费位点等功能,但它更适合人工诊断,而不是替代 Prometheus 告警。(RocketMQ)

截至 2026 年 6 月,官方 Dashboard 最新发布版为 2.1.0;RocketMQ Exporter 仓库显示的最新发布版为 v0.0.2。升级前应结合服务端版本进行兼容性验证。(GitHub)

15.3.2 消息轨迹的价值、成本与安全

经典消息轨迹默认写入 RMQ_SYS_TRACE_TOPIC,Broker 需启用 traceTopicEnable;也可以配置自定义轨迹 Topic。对于轨迹量很大的场景,官方文档提出可以使用独立 Broker 隔离普通消息和轨迹数据的物理 IO。(RocketMQ)

消息轨迹适合回答:

  • Producer 是否执行发送;
  • Broker 是否记录发送事件;
  • 哪个 Consumer 实例取得消息;
  • 消费结果是成功还是失败;
  • 消息是否经过重试。

它不能替代:

  • 业务数据库审计;
  • 跨服务的完整分布式调用链;
  • 对每条消息永久、零缺失的法律审计。

主要成本包括:

  1. 额外轨迹消息写入;
  2. 额外网络和磁盘 IO;
  3. 轨迹 Topic 的保留空间;
  4. 索引和查询压力;
  5. 高基数 Key、客户端地址和业务标识泄露风险。

生产上应采取采样、缩短保留期、独立权限、字段脱敏和访问审计。不要把完整消息体写入轨迹或普通日志。


15.4 mqadmin 常用排查命令

以下命令中的地址、Topic、Group、BrokerName 和消息标识均为占位符:

# 查看集群和 Broker
sh mqadmin clusterList -n nameserver:9876
sh mqadmin brokerStatus -n nameserver:9876 -b broker-host:10911

# 查看 Topic 路由和队列位点
sh mqadmin topicRoute -n nameserver:9876 -t order_event
sh mqadmin topicStatus -n nameserver:9876 -t order_event

# 查看 Consumer 连接、进度和内部状态
sh mqadmin consumerConnection -n nameserver:9876 -g order_consumer
sh mqadmin consumerProgress -n nameserver:9876 -g order_consumer -s
sh mqadmin consumerStatus -n nameserver:9876 -g order_consumer

# 按消息标识查询
sh mqadmin queryMsgByKey -n nameserver:9876 -t order_event -k order_10086
sh mqadmin queryMsgById -n nameserver:9876 -i OFFSET_MSG_ID
sh mqadmin queryMsgByUniqueKey -n nameserver:9876 -t order_event -i UNIQUE_MSG_ID

# 按队列和位点查询
sh mqadmin queryMsgByOffset -n nameserver:9876 \
  -t order_event -b broker-a -i 3 -o 928731

官方管理文档区分了 offsetMsgId 与 unique message ID:前者使用 queryMsgById,后者使用 queryMsgByUniqueKeyqueryMsgByOffset-b 参数要求填写 Broker 名称,而不是地址。consumerStatus 可以检查订阅关系和客户端 ProcessQueue 状态。(RocketMQ)

15.4.1 查询优先级

  1. 业务 Key:适合订单号、支付流水号等稳定业务标识;
  2. Unique Message ID:适合客户端记录了消息唯一标识的情况;
  3. OffsetMsgId:适合已有发送结果中的物理定位信息;
  4. Topic+时间区间:只适合缩小范围,容易返回大量候选消息;
  5. BrokerName+QueueId+Offset:最精确,但依赖完整位点信息。

业务 Key 应尽量唯一、可检索且不包含敏感明文。


15.5 判断消息停在哪个阶段

15.5.1 故障诊断流程图一:消息“消失”

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook flow 2

15.5.2 四段式证据链

阶段判定证据
没发送无 Producer 调用日志、无业务事件记录、无消息 Key
没存储Producer 没有明确成功,Broker 查询不到,路由或网络异常
没投递Broker 能查到,但 Group 位点不前进、没有拉取或轨迹事件
业务失败Consumer 已取得消息,但 Listener 异常、重试、DLQ 或业务表未更新

特别注意:发送超时不等于 Broker 没存储。响应可能在回程网络中丢失,而 Broker 已经接受消息。此时立即无限重试会制造重复消息。


15.6 核心故障诊断流程

15.6.1 故障诊断流程图二:Producer 超时与 No route info

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook flow 3

15.6.2 故障诊断流程图三:Consumer 不消费或积压

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook flow 4

同一 ConsumerGroup 内的所有实例必须具有完全一致的 Topic 和 Tag 订阅关系;不一致可能造成消费混乱,甚至消息遗漏。(RocketMQ)

15.6.3 故障诊断流程图四:Broker 磁盘、CPU 与 Full GC

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook flow 5

15.6.4 故障诊断流程图五:复制、Proxy 与 Rebalance

RocketMQ 第 15 章:可观测性、故障诊断、应急处理与生产 Runbook flow 6

15.7 故障矩阵

现象可能原因验证方法处理动作
Producer 超时网络抖动、Broker 排队、磁盘慢、同步复制等待查发送 P99、RPC 延迟、Key、线程池、磁盘延迟先确认消息是否已存储,再限速、切流或恢复 Broker
No route info地址错误、Topic 不存在、Broker 未注册、环境混用topicRoute、连通性探针、Broker 注册日志修正 endpoint/namespace,恢复注册或创建 Topic
Consumer 无在线实例启动失败、配置错误、权限失败、进程退出consumerConnection、应用启动日志修复配置并恢复实例
Consumer 在线但不消费订阅不一致、过滤错误、Offset 异常、RebalanceconsumerStatus、订阅表达式、位点变化统一订阅,稳定实例,核对 Offset
积压持续增长处理慢、并行度不足、下游限流、热点 QueueLag 时间、处理耗时、本地缓存、依赖延迟先恢复下游,再增加有效并行度
重试激增业务异常、超时设置过短、毒消息消费错误分类、相同 Key 重复失败隔离毒消息,修复业务,限制重试放大
DLQ 激增持续不可恢复错误、最大重试耗尽DLQ Topic、错误码、消息样本停止盲目回放,修复后按批次补偿
Broker 磁盘满流量增长、删除落后、保留期过长、异常文件文件系统、inode、写入/删除速度限流、扩容、迁移;禁止手工删除 CommitLog
Broker CPU 高流量突增、压缩、查询、GC、日志请求码、GC、线程栈、系统 CPU限流、降低查询、修复热点或扩容
Full GC堆设置不当、对象滞留、Exporter/日志高基数GC 日志、Heap、对象分配和线程数降低流量,修复泄漏,调整容量后滚动重启
复制延迟从节点磁盘慢、网络拥塞、主节点写入突增主从位点差、网络和磁盘延迟限制写入、修复副本,谨慎执行切换
Rebalance Storm实例反复重启、网络抖动、频繁扩缩容成员数和队列分配频繁变化稳定实例,修复健康检查和发布策略
Proxy 请求失败Proxy 过载、路由过期、Broker 不可达Proxy 错误码、直连探针、下游延迟扩容或旁路故障 Proxy,恢复路由和 Broker
Dashboard 查不到消息标识类型错误、时间窗口错误、索引过期分辨 Key、Unique ID、OffsetMsgId使用正确查询命令并扩大合理时间范围

15.8 安全执行 Reset Offset

Reset Offset 本质上是在改变“从哪里继续消费”。可能产生:

  • 重复消费;
  • 跳过消息;
  • 下游写入放大;
  • 顺序语义变化;
  • 大规模历史回放压垮数据库;
  • 当前处理中的消息与新位点交错。

15.8.1 标准操作流程

  1. 创建变更单,写明 Cluster、Topic、Group、目标时间和原因;
  2. 确认消息仍在保留期内;
  3. 导出当前每个 Queue 的消费位点;
  4. 确认业务幂等和下游容量;
  5. 暂停或缩容 Consumer,避免位点并发变化;
  6. 用消息查询确认目标时间附近的消息;
  7. 优先使用 -f false,避免意外向前跳过数据;
  8. 执行后立即再次检查位点;
  9. 小流量启动 Consumer,观察重复率、TPS、失败率和下游负载;
  10. 完成业务对账并归档操作记录。
sh mqadmin resetOffsetByTime \
  -n nameserver:9876 \
  -g order_consumer \
  -t order_event \
  -s '2026-06-18#10:00:00:000' \
  -f false

官方说明中,-f false 只允许回溯 Offset;-f true 可不考虑目标时间位点与当前消费位点的相对关系。生产操作前必须执行同版本命令帮助,确认时间格式和语义。(RocketMQ)

RocketMQ 5.5.0 还包含 FIFO POP Consumer 和顺序消费 Reset Offset 相关修复,说明位点重置行为具有版本敏感性。涉及 POP、FIFO、顺序消费或混合客户端时,应先在同版本预发环境验证。(RocketMQ)


15.9 SLI、SLO 与告警

15.9.1 推荐 SLI

[ SLI_{\text{send}}= \frac{\text{明确发送成功的逻辑消息数}} {\text{逻辑发送消息总数}} ]

[ SLI_{\text{consume}}= \frac{\text{业务处理成功消息数}} {\text{进入业务处理的消息数}} ]

[ L_{\text{e2e}}= T_{\text{业务处理成功}}-T_{\text{业务事件产生}} ]

还应包括:

  • 最老 Ready 消息时间;
  • DLQ 新增速率;
  • 消息缺失率和重复率;
  • Producer 到 Consumer 的金丝雀消息成功率;
  • 积压恢复时间;
  • Broker、Proxy 和路由查询的可用率。

15.9.2 SLO 与告警示例

下列数值仅是示例,不是 RocketMQ 通用标准。真实阈值必须根据业务时效、基线流量、磁盘规格、消息大小和恢复能力确定。

对象SLO 示例告警示例
关键 Producer月度明确发送成功率 ≥ 99.95%5 分钟失败率 > 1% 触发高优先级
发送延迟P99 ≤ 200msP99 连续 10 分钟 > 200ms
关键 Consumer99.9% 的分钟窗口中最老消息延迟 < 60s延迟 > 60s 持续 5 分钟;> 300s 立即升级
重试重试率 < 0.5%连续 10 分钟 > 1%
DLQ核心业务不允许持续新增任意持续新增即告警,并按业务等级升级
Broker 磁盘日常 < 70%>80% 预警;>90% 或预计 2 小时内写满为严重
复制正常接近实时位点差持续增长 5 分钟或超过约定 RPO
GC无持续 Full GC10 分钟内出现多次 Full GC 且伴随延迟上升
Proxy成功率满足接入 SLO错误率和 P99 同时恶化时升级

告警应分成两类:

  • 症状告警:发送失败、端到端延迟、积压时间、DLQ,负责叫醒值班人员;
  • 原因告警:CPU、磁盘、GC、线程池和网络,负责辅助定位或提前预警。

避免为每个 MessageId、Key、ClientId 创建指标标签,否则会造成 Prometheus 高基数和监控系统自身故障。


15.10 生产 Runbook

15.10.1 Runbook 01:Producer 发送超时

止损:限制无上限重试,对非关键流量降级;保留业务 Key。 诊断:检查发送延迟、Broker RPC、线程池、磁盘、网络丢包,并按 Key 查询。 恢复:故障 Broker 隔离或恢复后逐步放量。 核对:把所有超时消息视为“结果未知”,通过幂等表识别重复。

15.10.2 Runbook 02:No route info

止损:暂停自动重试风暴,确认是否只有单 Topic 或单环境受影响。 诊断:核对 endpoint、namespace、Topic 拼写、NameServer 连通性和 Broker 注册。 恢复:修正地址或恢复路由;不要依赖生产环境自动创建 Topic。 核对:用 topicRoute 和金丝雀消息验证。

15.10.3 Runbook 03:Consumer 不消费

止损:冻结发布和自动扩缩容,避免持续 Rebalance。 诊断:检查在线实例、订阅一致性、权限、客户端启动日志和 Offset。 恢复:统一配置后分批启动。 核对:确认每个 Queue 位点均持续前进。

15.10.4 Runbook 04:消息积压

止损:保护数据库等下游,必要时限制非关键 Producer。 诊断:比较生产速率、消费速率、处理 P99、本地缓存和最老消息时间。 恢复:先修复慢依赖,再增加 Consumer;实例数超过 Queue 数通常不能继续提升并行度。 核对:确认积压斜率转负、预计清空时间下降。

15.10.5 Runbook 05:重试和 DLQ 激增

止损:禁止整批无条件回放,隔离相同错误的毒消息。 诊断:按错误码、消息版本、Key 和依赖服务聚类。 恢复:修复业务逻辑后小批量回放,设置速率限制。 核对:比较原消息、重试、DLQ 和业务状态表,防止重复处理。

15.10.6 Runbook 06:Broker 磁盘接近写满

止损:限制非核心写入,停止大范围消息查询和轨迹扩张。 诊断:检查写入速度、删除速度、保留期、inode 和异常大 Topic。 恢复:扩容磁盘、迁移流量或增加 Broker。 禁止动作:不要直接删除 CommitLog、ConsumeQueue 或索引文件。

15.10.7 Runbook 07:Broker CPU 高或 Full GC

止损:降低发送和查询并发,避免同时重启多个节点。 诊断:关联请求码、线程池、GC 停顿、日志量和系统 CPU。 恢复:修复热点或泄漏,单节点滚动重启;扩容后逐步恢复流量。 核对:P99、排队时间和复制延迟同时恢复才算结束。

15.10.8 Runbook 08:主从复制延迟

止损:降低写入峰值,暂停高风险切换。 诊断:比较主从位点、磁盘写延迟、网络吞吐和 Controller 状态。 恢复:修复从节点资源或链路,确认同步集合恢复。 核对:验证副本追平,并按消息 Key 抽样检查。

15.10.9 Runbook 09:Rebalance Storm 或订阅不一致

止损:停止频繁发布、伸缩和故障实例反复拉起。 诊断:检查 Group 成员变化、实例重启、健康检查和订阅表达式。 恢复:统一订阅配置并滚动重启,稳定一段时间后再扩容。 核对:队列分配稳定、Lag 不再呈锯齿。

15.10.10 Runbook 10:Proxy 请求失败

止损:把流量切换至健康 Proxy,限制连接重建风暴。 诊断:区分客户端到 Proxy 与 Proxy 到 Broker 两段,检查错误码、连接数、路由刷新和线程池。 恢复:恢复下游 Broker 或扩容 Proxy。 核对:同时执行经 Proxy 探针和 Broker 侧消息查询。


15.11 Go 健康检查示例

下面的程序使用标准库检查 NameServer、Broker、Proxy 的 TCP 可达性,以及 Broker Metrics HTTP 接口,并以 Prometheus 文本格式暴露结果。

它只能证明“网络端点和指标端点可访问”,不能证明完整消息链路可用。生产系统还应建设专用金丝雀 Topic,周期性发送带唯一 Key 的消息并验证消费结果。

package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"os"
	"sort"
	"strings"
	"sync"
	"time"
)

type probe struct {
	Name   string
	Kind   string
	Target string
}

type sample struct {
	Probe   probe
	Up      bool
	Latency time.Duration
}

var (
	mu      sync.RWMutex
	samples = map[string]sample{}
)

func parseTargets(kind, raw string) []probe {
	var result []probe
	for _, item := range strings.Split(raw, ",") {
		item = strings.TrimSpace(item)
		if item == "" {
			continue
		}

		parts := strings.SplitN(item, "=", 2)
		if len(parts) != 2 {
			log.Printf("ignore invalid target: %q", item)
			continue
		}

		result = append(result, probe{
			Name:   strings.TrimSpace(parts[0]),
			Kind:   kind,
			Target: strings.TrimSpace(parts[1]),
		})
	}
	return result
}

func execute(ctx context.Context, p probe) sample {
	start := time.Now()
	var err error

	switch p.Kind {
	case "tcp":
		var dialer net.Dialer
		conn, dialErr := dialer.DialContext(ctx, "tcp", p.Target)
		err = dialErr
		if conn != nil {
			_ = conn.Close()
		}

	case "http":
		req, reqErr := http.NewRequestWithContext(
			ctx,
			http.MethodGet,
			p.Target,
			nil,
		)
		if reqErr != nil {
			err = reqErr
			break
		}

		resp, requestErr := http.DefaultClient.Do(req)
		if requestErr != nil {
			err = requestErr
			break
		}
		defer resp.Body.Close()

		_, _ = io.CopyN(io.Discard, resp.Body, 1)
		if resp.StatusCode < 200 || resp.StatusCode >= 400 {
			err = fmt.Errorf("unexpected HTTP status: %d", resp.StatusCode)
		}

	default:
		err = fmt.Errorf("unsupported probe kind: %s", p.Kind)
	}

	if err != nil {
		log.Printf(
			"probe failed name=%s kind=%s target=%s error=%v",
			p.Name,
			p.Kind,
			p.Target,
			err,
		)
	}

	return sample{
		Probe:   p,
		Up:      err == nil,
		Latency: time.Since(start),
	}
}

func refresh(probes []probe, timeout time.Duration) {
	ch := make(chan sample, len(probes))

	for _, p := range probes {
		p := p
		go func() {
			ctx, cancel := context.WithTimeout(context.Background(), timeout)
			defer cancel()
			ch <- execute(ctx, p)
		}()
	}

	next := make(map[string]sample, len(probes))
	for range probes {
		s := <-ch
		next[s.Probe.Kind+":"+s.Probe.Name] = s
	}

	mu.Lock()
	samples = next
	mu.Unlock()
}

func escapeLabel(value string) string {
	value = strings.ReplaceAll(value, `\`, `\\`)
	value = strings.ReplaceAll(value, `"`, `\"`)
	return strings.ReplaceAll(value, "\n", `\n`)
}

func metricsHandler(w http.ResponseWriter, _ *http.Request) {
	mu.RLock()
	copyOfSamples := make(map[string]sample, len(samples))
	for key, value := range samples {
		copyOfSamples[key] = value
	}
	mu.RUnlock()

	keys := make([]string, 0, len(copyOfSamples))
	for key := range copyOfSamples {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	w.Header().Set("Content-Type", "text/plain; version=0.0.4")
	fmt.Fprintln(w, "# HELP rocketmq_probe_up Whether the endpoint is reachable.")
	fmt.Fprintln(w, "# TYPE rocketmq_probe_up gauge")
	fmt.Fprintln(w, "# HELP rocketmq_probe_latency_seconds Probe latency.")
	fmt.Fprintln(w, "# TYPE rocketmq_probe_latency_seconds gauge")

	for _, key := range keys {
		s := copyOfSamples[key]
		up := 0
		if s.Up {
			up = 1
		}

		labels := fmt.Sprintf(
			`name="%s",kind="%s"`,
			escapeLabel(s.Probe.Name),
			escapeLabel(s.Probe.Kind),
		)

		fmt.Fprintf(
			w,
			"rocketmq_probe_up{%s} %d\n",
			labels,
			up,
		)
		fmt.Fprintf(
			w,
			"rocketmq_probe_latency_seconds{%s} %.6f\n",
			labels,
			s.Latency.Seconds(),
		)
	}
}

func healthHandler(w http.ResponseWriter, _ *http.Request) {
	mu.RLock()
	defer mu.RUnlock()

	for _, s := range samples {
		if !s.Up {
			http.Error(w, "unhealthy", http.StatusServiceUnavailable)
			return
		}
	}

	w.WriteHeader(http.StatusOK)
	_, _ = w.Write([]byte("ok\n"))
}

func main() {
	probes := parseTargets("tcp", os.Getenv("RMQ_TCP_TARGETS"))
	probes = append(
		probes,
		parseTargets("http", os.Getenv("RMQ_HTTP_TARGETS"))...,
	)

	if len(probes) == 0 {
		log.Fatal("no probe targets configured")
	}

	const (
		interval = 15 * time.Second
		timeout  = 3 * time.Second
	)

	refresh(probes, timeout)
	go func() {
		ticker := time.NewTicker(interval)
		defer ticker.Stop()

		for range ticker.C {
			refresh(probes, timeout)
		}
	}()

	http.HandleFunc("/metrics", metricsHandler)
	http.HandleFunc("/healthz", healthHandler)

	log.Println("probe server listening on :9108")
	log.Fatal(http.ListenAndServe(":9108", nil))
}

示例环境变量:

RMQ_TCP_TARGETS=namesrv=10.0.0.1:9876,broker=10.0.0.2:10911,proxy=10.0.0.3:8081
RMQ_HTTP_TARGETS=broker_metrics=http://10.0.0.2:5557/metrics

15.12 线上消息积压事故复盘

以下为一场模拟生产事故,但使用真实可执行的复盘结构。

15.12.1 事故摘要

2026 年 6 月 18 日,订单状态 Consumer 发布新版本后,消费处理 P99 从 90ms 上升到 2.6s。消费速度低于生产速度,最高积压 182 万条,最老消息延迟 43 分钟。部分消息因下游数据库超时进入重试,最终有 326 条进入 DLQ。

业务没有永久丢失数据,但订单状态展示延迟,约 8.4 万名用户受到影响。

15.12.2 时间线

时间事件
09:55Consumer 新版本开始滚动发布
10:02数据库查询 P99 从 35ms 升至 1.8s
10:04Consumer 处理 P99 升至 2.6s,本地缓存快速增长
10:07最老消息延迟超过 60s,首次告警
10:10重试率升至 15%,数据库连接池接近耗尽
10:12停止发布并回滚 Consumer
10:15限制非关键订单事件生产速率
10:18数据库延迟恢复,消费速率开始高于生产速率
10:25在确认数据库容量后增加四个 Consumer 实例
11:02Ready Lag 清零
11:20完成 DLQ、小批量补偿和业务对账

15.12.3 直接原因

新版本为每条消息增加了一次按 JSON 字段查询数据库的操作,但生产数据库缺少对应索引。查询变慢后,Consumer 线程长期阻塞。

15.12.4 放大因素

  1. 消费超时设置过短,短暂变慢被快速转换为重试;
  2. 重试再次执行同一条慢查询,进一步压垮数据库;
  3. 自动扩缩容只依据 CPU,Consumer 阻塞在 IO 时 CPU 并不高;
  4. 原告警只关注积压条数,没有关注最老消息时间;
  5. 压测数据量和字段基数低于生产环境;
  6. 发布没有设置消费处理耗时的自动终止条件。

15.12.5 根因证据

  • Broker CPU、磁盘和复制指标正常;
  • Producer TPS 没有异常突增;
  • Consumer process_time 和本地缓存先于 Lag 增长;
  • 数据库慢查询与新版本发布时间完全重合;
  • 回滚后,在未调整 Broker 的情况下消费延迟立即恢复;
  • 相同消息在重试中重复触发同一慢查询。

因此,根因不是 RocketMQ Broker 容量不足,而是 Consumer 业务处理退化及重试放大。

15.12.6 止损与恢复

正确顺序是:

  1. 停止继续发布;
  2. 回滚慢 Consumer;
  3. 限制非关键生产流量;
  4. 修复或降低数据库压力;
  5. 确认单实例消费能力恢复;
  6. 再增加 Consumer 并行度;
  7. 控制积压清理速率,避免二次压垮数据库;
  8. 单独处理 DLQ 和毒消息。

若一开始直接扩容 Consumer,会产生更多数据库并发,事故反而会加重。

15.12.7 数据核对

对账以业务事件表为基准,按 business_id 比较:

  • 应发送事件数;
  • Broker 中可查询消息数;
  • Consumer 幂等表记录数;
  • 最终订单状态数;
  • 重试和 DLQ 数;
  • 重复处理但被幂等拦截的数量。

对 326 条 DLQ 消息先修复索引,再以每秒 10 条的速度回放。最终确认无消息永久缺失、无错误状态覆盖。

15.12.8 整改项

整改项负责人完成标准
增加最老消息延迟告警SRE核心 Group 全覆盖
发布过程监控消费 P99平台组超阈值自动停止发布
建立数据库索引审查DBA变更单必须附执行计划
重试增加指数退避和抖动应用组避免固定间隔重试风暴
自动扩缩容加入 Lag 和下游容量架构组不再只依据 CPU
建立 DLQ 定期巡检业务组每条 DLQ 有处置状态
增加生产规模数据压测测试组数据基数接近生产
建设金丝雀消息SRE端到端成功和延迟可观测

15.13 面试题

题目去重:本节作为本章运维自测,只保留 Metrics、Logs、Traces、排障、Runbook 和应急处理题。跨章重复题、完整追问链和模拟面试统一跳转到 第 20 章:资深面试题库、追问链与模拟面试

1. Metrics、Logs、Traces 有什么区别?

标准答案:Metrics 发现趋势,Logs 提供错误上下文,Traces 关联单条消息生命周期。 追问:为什么仍需要业务对账? 易错点:认为消息轨迹能够证明业务数据库一定成功。

2. Producer 超时是否表示消息发送失败?

标准答案:不一定。Broker 可能已存储,只是响应未到达 Producer。 追问:如何处理? 易错点:无条件快速重试,制造重复消息。

3. 如何定义 Producer 成功率?

标准答案:按逻辑消息统计明确成功数,同时单独记录未知结果。 追问:为什么不能按发送尝试统计? 易错点:重试把分母和流量同时放大。

4. No route info 的常见原因是什么?

标准答案:地址或 namespace 错误、Topic 不存在、Broker 未注册、NameServer/Proxy 不可达。 追问:第一条命令是什么? 易错点:直接重启整个集群。

5. Ready 与 Inflight 有什么区别?

标准答案:Ready 尚未被客户端拉取;Inflight 已拉取但未提交完成。 追问:两者同时高说明什么? 易错点:只看总积压条数。

6. 为什么时间 Lag 通常比数量 Lag 更重要?

标准答案:它直接反映业务消息的新鲜度和时效违约。 追问:数量相同为什么影响不同? 易错点:忽略生产、消费速率。

7. Consumer 在线但不消费如何排查?

标准答案:检查订阅一致性、过滤、Offset、拉取权限、Rebalance 和本地缓存。 追问:如何判断业务线程是否阻塞? 易错点:仅依据进程存活。

8. Consumer 实例越多,消费能力一定越高吗?

标准答案:不一定,受 Queue 数量、下游容量和单 Queue 顺序约束。 追问:实例多于 Queue 数会怎样? 易错点:把无限扩容当作积压处理方案。

9. 如何估算积压清空时间?

标准答案:积压量除以消费速率减生产速率。 追问:消费速率小于生产速率呢? 易错点:仍给出有限清空时间。

10. 重试激增首先应该扩容吗?

标准答案:不一定,应先判断是否为下游故障或毒消息。 追问:为什么扩容可能恶化事故? 易错点:增加对故障数据库的并发。

11. DLQ 消息能否直接批量回放?

标准答案:不能。必须先修复根因、确认幂等,并限速、小批量回放。 追问:如何核对结果? 易错点:把 DLQ 当普通积压。

12. Broker 磁盘满时能否手工删除 CommitLog?

标准答案:不能,可能破坏消息存储一致性。 追问:正确止损方式是什么? 易错点:直接使用文件系统命令删除数据文件。

13. Broker CPU 不高,为什么发送仍然很慢?

标准答案:可能是磁盘、Page Cache、网络、复制等待或线程池排队。 追问:重点看哪些指标? 易错点:把 CPU 当成唯一瓶颈。

14. Full GC 与 Page Cache 有什么关系?

标准答案:Heap 和操作系统页缓存争夺内存可能引起回收、swap 和 IO 抖动。 追问:为什么不能只增大 JVM Heap? 易错点:挤占 Page Cache。

15. 如何判断主从复制延迟?

标准答案:观察主从位点或字节差、时间延迟、网络和副本磁盘。 追问:什么时候可以切换? 易错点:在副本未追平时盲目切主。

16. 什么是 Rebalance Storm?

标准答案:Group 成员和队列分配频繁变化,导致消费暂停、重复和 Lag 锯齿。 追问:常见触发因素是什么? 易错点:把每次 Rebalance 都视为故障。

17. 为什么同一 ConsumerGroup 必须保持订阅一致?

标准答案:Group 是同一消费语义,实例订阅不一致会造成队列分配和过滤混乱。 追问:如何验证? 易错点:认为不同实例可以各自订阅不同 Topic。

18. 原生 Metrics 与 RocketMQ Exporter 如何选择?

标准答案:5.x Broker 高频指标优先原生 Metrics;兼容旧集群或补充管理指标可使用 Exporter。 追问:Exporter 有什么代价? 易错点:忽略采集周期和管理请求负载。

19. Dashboard 能否作为唯一监控系统?

标准答案:不能,它更适合人工查询和操作。 追问:正式告警依赖什么? 易错点:没有历史时序和自动化 SLO 告警。

20. 消息轨迹是否应全部长期保存?

标准答案:通常不应,应依据价值进行采样、控制保留期并隔离权限。 追问:有哪些安全风险? 易错点:忽略 Key、客户端地址和业务标识泄露。

21. Reset Offset 最大的风险是什么?

标准答案:产生重复消费或直接跳过消息,并可能压垮下游。 追问:执行前要保存什么? 易错点:Consumer 在线时直接强制重置。

22. 一场 RocketMQ 事故何时才算恢复?

标准答案:流量恢复、积压清零、错误停止只是技术恢复;还必须完成消息和业务数据对账。 追问:复盘应输出什么? 易错点:以告警消失作为事故结束。


15.14 本章总结

RocketMQ 生产运维的核心不是记住若干命令,而是建立一条完整证据链:

  1. 以业务 SLI 发现影响;
  2. 用 Metrics 判断故障位于发送、存储、投递还是处理阶段;
  3. 用 Logs、消息查询和轨迹缩小范围;
  4. 先止损,再定位根因;
  5. 恢复时控制流量,避免重试、扩容和回放造成二次事故;
  6. 最终以业务幂等表和对账结果证明数据正确;
  7. 将处置过程沉淀为可演练、可审计的 Runbook。

真正成熟的可观测性,不是让工程师看到更多图表,而是让值班人员在事故发生后,能够迅速回答:消息在哪里、为什么停住、怎样安全恢复、数据是否正确。

15.15 官方资料

  • Apache RocketMQ 5.5.0 Release Notes。(RocketMQ)
  • Apache RocketMQ 5.x Metrics 官方文档。(RocketMQ)
  • Apache RocketMQ Admin Tool 官方文档。(RocketMQ)
  • Apache RocketMQ Dashboard 官方文档与发布信息。(RocketMQ)
  • Apache RocketMQ Prometheus Exporter 官方文档与仓库。(RocketMQ)
  • Apache RocketMQ Message Trace 官方文档。(RocketMQ)
  • Apache RocketMQ Consistent Subscription Relationship 官方文档。(RocketMQ)