前两篇已经说明一条消息如何从 Producer 经过 Exchange 和 Queue 到达 Consumer,并用 publisher confirms、mandatory、manual ack 与 prefetch 加固客户端和 Broker 之间的链路。但生产可靠性不是把这些开关全部打开就结束了:订单数据库提交、消息发布、Broker 存储、消费者处理与业务数据库提交跨越多个故障边界,任何两个动作之间都可能发生进程崩溃、节点故障或网络中断。

本篇从端到端结果出发,不把“Broker 收到了消息”误当成“业务已经完成”。我们先统一交付语义和可靠性边界,再沿这四层边界讨论后续方案各自解决什么、又留下什么。

系列导航:

  1. RabbitMQ 核心原理:一条消息如何完成路由与消费
  2. RabbitMQ + .NET 10 实战:可靠地发布与消费消息
  3. RabbitMQ 生产实践:可靠性、幂等、重试与高可用(本文)

🎯 先统一交付语义

讨论可靠性之前,要先回答系统允许丢失还是允许重复,以及“成功”究竟指 Broker 接收、消费者收到,还是业务结果提交。三种常见交付语义的差异如下:

语义允许丢失允许重复典型实现
at-most-once先确认或不重试
at-least-once否或尽量避免确认、重发、重投递
effectively-once传输层仍可重复业务结果不重复生效at-least-once + 幂等/去重

RabbitMQ 没有一个可以开启端到端 exactly-once 的开关。publisher confirms 表示 RabbitMQ 已对消息承担责任,却无法覆盖发布前后的业务数据库事务;consumer ack 表示 Broker 可以删除这次投递,也无法与消费者的数据库提交组成一个原子事务。两种确认机制彼此独立,网络故障还可能让发送方不知道确认是否已经产生,形成“实际成功、调用方却无法确认”的结果未知窗口。

因此,生产系统通常以 at-least-once 作为传输基础:发布结果不确定时允许重发,消费者失败时允许重投递,再依靠业务幂等或去重把最终效果收敛为 effectively-once。这里的“once”描述的是业务结果只生效一次,不代表消息在网络、Broker 或消费者之间只出现一次。

🧱 四层可靠性边界

端到端链路应拆成四层分别治理。每一层都有自己的确认点和失效方式,上一层成功不代表下一层已经完成。

边界主要故障主要机制仍需接受的事实
Publisher → Broker发布丢失、确认丢失Outbox、publisher confirms、重发重发可能产生重复
Broker 存储节点故障、磁盘/内存水位durable、persistent、Quorum Queue集群不是备份或跨地域灾备
Broker → Consumer消费者崩溃、毒消息manual ack、有限重试、DLX重投递会产生重复
业务数据库事务提交与 ack 之间崩溃Inbox、唯一约束、同事务写入ack 与数据库无法组成原子事务

这张表也是全文的判断框架:先定位故障发生在哪一层,再选择该层的机制。durable、persistent 与 Quorum Queue 保护 Broker 存储,不能修复应用双写;manual ack 能让未确认消息在消费者断开后重新入队,却不能阻止业务已提交后的重复执行;集群能提高节点故障下的可用性,也不能替代独立备份和跨地域灾备。

📤 生产端:先用 Outbox 封住双写窗口

假设订单服务先提交业务事务,再向 RabbitMQ 发布 order.created。两次写入分属不同系统,中间一定存在进程可能崩溃的窗口:

订单服务              业务数据库              RabbitMQ
   │ BEGIN                  │                       │
   │ 写入 Order             │                       │
   │ COMMIT                 │                       │
   │───────────────────────>│                       │
   │                                                X 发布前进程崩溃
   │                                                │
结果:订单已存在,但 order.created 没有发布

把顺序改成“先发消息,再提交数据库”并不能消除双写,只会反转窗口:RabbitMQ 已经接管消息后,数据库事务仍可能回滚,于是下游看到了一个实际上没有成功创建的订单。publisher confirms 能回答 Broker 是否接管了发布,却不能让 RabbitMQ 与业务数据库共享同一个原子提交点。

Transactional Outbox 的最小状态模型

Transactional Outbox 的关键不是增加一张表,而是把“业务变更”和“待发布事件”收进同一个本地数据库事务。下面是够用的最小结构,字段类型和 SQL 语法可按实际数据库调整:

CREATE TABLE outbox_messages (
  event_id      varchar(64) PRIMARY KEY,
  event_type    varchar(128) NOT NULL,
  payload       text NOT NULL,
  occurred_at   timestamp NOT NULL,
  published_at  timestamp NULL,
  attempt_count integer NOT NULL DEFAULT 0
);

生产端按以下固定流程工作:

  1. 在同一个数据库事务中写入订单和 Outbox 行;事务成功时两者同时存在,失败时两者同时回滚。
  2. 后台发布器批量读取 published_at IS NULL 的行,并用 attempt_count 记录发布尝试,便于退避、告警和排障。
  3. 使用 MessageId = event_id 发布,并等待 publisher confirm;event_id 同时是后续消费端去重的稳定键。
  4. 收到确认后,将对应 Outbox 行的 published_at 标记为发布时间。
  5. 超时、连接中断或结果未知时,重试同一个 event_id,不要生成新 ID。

这个流程封住了“业务已提交,但事件从未获得发布机会”的窗口,却没有创造 exactly-once。RabbitMQ 已确认消息之后、数据库写入 published_at 之前,如果进程崩溃,发布器恢复后仍会再次读取并发布同一行。因此 Outbox 主动选择 at-least-once:允许重发,并把同一个 event_id 交给消费端做幂等或去重。

💾 Broker 端:把接管、落盘和恢复连成一条链

对于需要在 Broker 重启后恢复的普通 AMQP 0-9-1 路径,可以用下面的组合检查配置是否闭环:

durable Exchange
+ durable Queue
+ persistent message
+ publisher confirms
= 可观察的 Broker 接管与重启恢复链条
  • 缺少 durable Exchange:Exchange 的定义不会作为持久拓扑在重启后恢复,发布入口及其绑定关系可能不再存在。
  • 缺少 durable Queue:Queue 的定义不会在节点重启后恢复,其中的消息也无法仅凭 persistent 属性保留下来。
  • 缺少 persistent message:对于 Classic Queue,即使 Queue 是 durable,瞬时消息也会在恢复时被丢弃;持久性是消息自己的交付属性,不由 Queue 替它推断。Quorum Queue 是例外:RabbitMQ 4.x 会忽略消息的 delivery mode,并持久化进入 Quorum Queue 的所有消息。
  • 缺少 publisher confirms:应用把字节写进连接后,无法知道 Broker 是否已经接管。节点可能在消息真正持久化前故障,生产者却没有可靠依据决定哪些消息需要重发。

对持久消息和 durable Queue,RabbitMQ 会在消息持久化后再发送 confirm;若目标是 Quorum Queue,则要等多数派副本接受并向领导者确认。confirm 因而是“Broker 已按目标队列的安全语义承担责任”的可观察信号,但它只覆盖 Publisher → Broker,不表示消费者已经收到消息,更不表示消费者的业务数据库已经提交。

🗳️ Quorum Queue:用多数派换取节点故障下的数据安全

Quorum Queue 是 durable、replicated 的队列类型,使用 Raft 风格的多数派复制。每个队列有一个领导者和若干跟随者,入队、投递状态与确认等状态变更由领导者复制给成员;只有超过半数成员可用时,队列才能继续形成一致决定。

当领导者所在节点故障时,只要仍有多数派成员在线,某个跟随者就可以被选为新领导者并恢复操作。选举期间投递会短暂停顿;如果多数派已经丢失,队列会停止工作,而不是在无法保证一致性的少数派上继续接受写入。因此,三个成员通常是获得单节点容错能力的最小有意义配置。

决策Classic QueueQuorum Queue
单节点临时/低价值负载可考虑通常成本过高
节点故障后的队列可用性取决于所在节点与部署多数派存活时可故障转移
数据复制不提供同等的多数派复制语义复制到多个成员
容量成本较低磁盘与网络放大

Quorum Queue 适合订单事件、支付指令等需要数据安全的长期队列,但复制日志、持久化和多数派确认会增加磁盘写入、网络流量、确认延迟与节点容量成本。临时队列、单节点低价值负载或可轻易重建的数据,不必无差别地承担这份成本。

还要明确集群边界:RabbitMQ 集群和 Quorum Queue 解决的是同一集群内的节点故障与副本切换,通常面向同地域、低延迟网络。它们不等于独立备份,不能恢复被应用误删或错误确认的数据,也不自动提供跨地域灾备;这些风险仍需要备份/恢复流程、独立灾备集群或跨地域复制方案另行治理。

📥 消费端:用 Inbox 把重复投递收敛为一次业务结果

第二篇已经使用 manual ack,让消费者只有在处理成功后才确认消息。但数据库提交和 ack 仍属于两个系统,无法组成同一个原子事务。最危险的不是消息被再次投递,而是消费者假定消息不会再次投递:

Consumer             业务数据库             RabbitMQ
   │ 处理 eventId         │                     │
   │ BEGIN                │                     │
   │ 更新库存 + 写 Inbox  │                     │
   │ COMMIT ─────────────>│                     │
   X ack 前进程崩溃                              │
                                                 │ 重新投递同一消息

若为了避免重复而在数据库提交前 ack,进程可能在 ack 后、提交前崩溃,消息已经被 Broker 删除,库存却没有更新。避免丢失就必须允许重投递;消费者必须让同一个 eventId 再次到达时不重复改变业务结果。 Outbox 生成并在重发时保持不变的 eventId,正是消费端识别同一业务事件的依据。

Inbox 的最小数据结构与事务边界

可以按消费者逻辑名称和事件 ID 建立联合唯一约束。同一事件允许被不同消费者各处理一次,却不能被同一个消费者重复生效:

CREATE TABLE inbox_messages (
  consumer_name varchar(128) NOT NULL,
  event_id      varchar(64) NOT NULL,
  processed_at  timestamp NOT NULL,
  PRIMARY KEY (consumer_name, event_id)
);

消费事务固定按以下顺序执行:

  1. 开启数据库事务。
  2. 尝试插入 (consumer_name, event_id)
  3. 唯一键冲突表示该消费者已经处理过此事件:回滚当前业务分支,安全 ack。
  4. 插入成功则在同一个事务内更新库存。
  5. 提交数据库事务后再 ack。

这里不能先提交 Inbox、再单独更新库存,否则两次写入之间仍有崩溃窗口;也不能在数据库事务提交前 ack。若提交返回结果未知,应按数据库实际状态查询或允许 RabbitMQ 重投递,由唯一约束裁决,而不是猜测成功与否。

Inbox 不是唯一的幂等手段,应按业务动作选择最小机制:

手段适用场景关键约束
天然幂等写入用最终值覆盖状态,例如“把订单标记为已支付”重复执行必须得到相同结果
唯一约束创建型动作,例如一个订单只能生成一条出库单业务键必须稳定且由数据库强制约束
状态机校验订单、支付等有限状态迁移只允许合法的源状态到目标状态
Inbox需要统一消费去重和审计去重记录与业务更新必须在同一事务

🚦 先分类错误,再决定 ack、重试还是停车

重投递是可靠性机制,不是默认错误处理。消费者应保留 eventId、队列、路由键和业务对象标识等上下文,再把失败分成三类:

类型示例处理
瞬时错误数据库短暂不可用、HTTP 503有上限、带退避的重试
永久业务错误订单不存在、状态不允许记录上下文并进入人工处置或停车队列
毒消息JSON 损坏、字段无法解析不原地 requeue,直接死信/停车

必须明确禁止这种兜底逻辑:捕获所有异常后无条件 requeue: true。它会让永久错误和毒消息立刻回到队头,形成高频空转,持续占用消费者、连接、CPU 与日志容量,并可能阻塞后续健康消息。只有确认仍可能恢复的瞬时错误才进入有限重试;无法通过等待恢复的问题应尽快隔离。

⏳ 30 秒、5 分钟、30 分钟的有限退避

下面是一条可作为起点的分级重试拓扑。每个重试队列设置自己的消息 TTL;TTL 到期后通过该队列的 DLX 和 routing key 回到主 Exchange,再路由到主队列:

orders.events
    │ order.created

inventory.order-events (主队列)
    │ 第 1 次临时失败

inventory.retry.30s -- TTL 到期 --> 主 Exchange --> 主队列
    │ 再次失败

inventory.retry.5m  -- TTL 到期 --> 主 Exchange --> 主队列
    │ 再次失败

inventory.retry.30m -- TTL 到期 --> 主 Exchange --> 主队列
    │ 达到最大次数 / 永久错误 / 毒消息

inventory.parking (停车队列,等待告警与人工处置)

本文把最大处理次数设为示例值 5,并把首次消费也计入次数:第一次失败等待 30 秒,第二次失败等待 5 分钟,第三、四次失败各等待 30 分钟,第五次失败不再重试,进入 inventory.parking。永久业务错误和毒消息不消耗这套退避预算,第一次识别后就直接停车。这个数字只是示例基线;生产值应由恢复时间目标、下游可恢复时长和下游能够承受的重放流量共同决定。

每次转移都要记录 eventId、当前处理次数、失败类型和经过截断与脱敏的最后错误摘要。处理次数必须随消息跨重试队列保留,并由消费者或重试发布器显式递增;不能只依赖进程内计数,也不要把 Broker 的 redelivered 标记当成完整次数。瞬时错误转入下一档时,应先确认重试消息已经被 Broker 接管,再 ack 原投递;若采用 basic.reject / basic.nackrequeue: false 交给 DLX,则必须提前验证 DLX、绑定和目标队列都存在。无论采用哪种转移方式,都不能用无限原地 requeue 代替退避队列。

DLX 用 Policy 管理,但不要把它当重试编排器

主业务队列的 DLX 建议用 Policy 配置。例如下面的正则只匹配 inventory.order-events,不会匹配 inventory.retry.*inventory.parking,因此不会把停车消息再次死信回主链路:

rabbitmqctl set_policy inventory-dlx \
  '^inventory\.order-events$' \
  '{"dead-letter-exchange":"inventory.dlx"}' \
  --apply-to queues \
  --priority 10

Policy 可以在线调整;把 x-dead-letter-exchangex-dead-letter-routing-key 等参数硬编码在队列声明中,变更时通常需要删除并重建队列,而且声明参数会覆盖 Policy。Policy 只声明“从这个队列死信到哪里”,不会根据第几次失败自动选择 30 秒、5 分钟或 30 分钟。分级路由仍要由明确的 routing key、重试发布逻辑或经过验证的 Exchange/Binding 设计完成;inventory.dlx 至少应有一条可达停车队列的兜底绑定。

死信链路也不是天然无损。目标 Exchange 在死信发生时不存在,或路由不到任何目标队列,消息可能被丢弃;配置形成死信环路时,RabbitMQ 会检测某些没有 rejection 的循环并丢弃消息。普通死信重新发布默认不使用内部 publisher confirms;如果源队列是 Quorum Queue 且这段转移也要求更强保证,需要同时评估 dead-letter-strategy=at-least-onceoverflow=reject-publish 和 DLX 配置,而不是只声明一个 DLX 名称。

RabbitMQ 4.0 及以上版本的 Quorum Queue 默认还有 delivery-limit=20,超过限制的消息会被死信;没有 DLX 时则会被丢弃。RabbitMQ 4.3 又把计数收窄为真正的失败,例如连接/Channel 崩溃或 basic.reject;AMQP 0-9-1 的 basic.nack 本身不会增加 delivery-count,所以无条件 nack(requeue: true) 仍可能形成无限循环。这个限制不能表达本文的 30 秒、5 分钟、30 分钟节奏,也不能替代错误分类、次数审计和停车处置。消息离开源队列再经 TTL 返回后,业务重试次数也不应只靠 Quorum Queue 的 x-delivery-count 推断。因此,delivery limit 只能作为 Broker 侧最后护栏,并与可达的 DLX/停车策略一起验证,而不能被当成完整的业务重试编排。

📈 积压不是一个长度,而是流入与流出的差

队列增长时,先用速率判断它是在吸收短暂波峰,还是消费能力已经长期低于发布流量。一个便于现场估算的起点是:

净积压速率 ≈ 发布速率 - 有效消费速率
有效消费速率 ≈ 消费者实例数 × 单实例并发 ÷ 平均处理耗时

例如每秒发布 500 条、有效消费 420 条,净积压约为每秒 80 条;如果高峰持续 15 分钟,就会新增约 72000 条待处理消息。反过来,若高峰结束后发布速率降到每秒 300 条,而消费仍能维持每秒 420 条,理论排空速率约为每秒 120 条。容量评估不能只问“队列最多有多长”,还要问最坏流量持续多久、多久必须排空,以及排空期间用户能接受多大的消息年龄。

第二个公式只是上限估算,不是扩容承诺。数据库连接池、行锁竞争、第三方接口限流、同一业务键的顺序约束、CPU 和内存都会截断实际并发。即使 Consumer 的线程数翻倍,只要每个回调都在等待同一个 20 连接的数据库池,有效消费速率也不会同比增长,反而可能增加超时、锁等待和重复投递。扩容消费者之前,必须先确认下游连接池、数据库写入能力、外部配额和消息顺序约束是否允许更多并发,并用压测得到的 ack rate 与消息年龄验证结果。

观察积压时要把消息状态拆开,避免把不同故障都解释成“消费者太少”:

现象优先检查
ready 持续增长发布/ack 速率差、消费者数量、下游耗时
unacknowledged 很高prefetch、处理卡死、回调并发、下游连接池
redelivered 激增消费者崩溃、超时、无界 requeue、确认失败
队列不长但最老消息很老个别分区/路由/消费者停滞,用户延迟已发生

ready 表示消息仍在队列中等待投递,unacknowledged 表示消息已经投递给 Consumer、但 Broker 尚未收到 ack。后者持续偏高通常说明消息被 prefetch 到客户端后处理不完,不能靠继续提高 prefetch 掩盖。redelivered 上升则说明同一投递正在回流,应先查崩溃、连接抖动、超时和 requeue 循环,而不是把它计作新增业务流量。最老消息年龄直接对应用户等待时间;即使总长度很小,一条被路由或顺序约束卡住的旧消息也可能已经违反业务 SLO。

🔭 监控与告警:同时看状态、速率和年龄

Broker 指标要与应用处理耗时、数据库连接池和业务 SLO 放在同一张看板上。只看某个瞬时队列长度,既会在短暂波峰时误报,也会漏掉“小队列里卡着旧消息”的真实故障。

信号它回答的问题需要联动检查
messages ready还有多少消息等待投递publish rate、ack rate、消费者数量、最老消息年龄
messages unacknowledged多少消息已投递但尚未确认prefetch、处理耗时、回调卡死、下游连接池
redelivered消息是否频繁重新投递Consumer 崩溃、连接恢复、超时、reject/nack 与 requeue 策略
publish rate当前流入速度是多少业务流量、publisher confirm 延迟、不可路由消息
ack rateConsumer 完成并确认的速度是多少处理成功率、批量 ack 方式、数据库提交耗时
consumer capacity队列有多大比例的时间可以立即投递消费者数量、处理耗时、prefetch;仅作为容量提示
消费者数量是否有实例离线或扩缩容异常实例健康、注册抖动、每实例并发
连接/Channel 数是否发生连接风暴、泄漏或异常 churn客户端复用策略、重连频率、节点文件描述符
节点磁盘告警剩余磁盘是否低于配置水位磁盘增长、持久化流量、发布连接是否被阻塞
节点内存告警内存使用是否越过配置水位队列内存、连接/Channel、发布连接是否被阻塞
最老消息年龄最慢一条消息已经等待多久业务 SLO、路由/分片停滞、顺序约束

RabbitMQ 4.3 将 consumer capacity 定义为队列能够立即向 Consumer 投递消息的时间占比。低于 100% 表示增加消费者、缩短处理时间或调整 prefetch 可能提高投递速度,但它不是下游还有容量的证明;没有 Consumer 时该值为 0%,有在线 Consumer 但没有消息流时可显示为 100%。容量决策仍要结合 ack rate、处理耗时、下游利用率和压测结果。

磁盘与内存告警不是普通的“资源快满了”提示。节点剩余磁盘低于 disk_free_limit,或内存使用越过配置水位时,RabbitMQ 会阻塞发布连接以保护节点;因此应用侧可能同时看到 publish rate 下降、连接被阻塞和 publisher confirm 延迟上升。告警恢复也不代表积压已经排空,资源恢复后仍要继续观察消息年龄和净积压速率。

告警规则应使用持续窗口、变化趋势和消息年龄,并至少包含下面三类组合条件:

  1. 容量缺口ready 连续 10 分钟增长,且 publish rate 大于 ack rate。
  2. SLO 违约:最老消息年龄超过业务 SLO,即使队列长度仍较小。
  3. 节点背压:节点触发磁盘或内存告警,并伴随发布速率下降或连接阻塞。

组合告警仍要按业务基线调参。例如批处理队列在夜间可能有计划地积压,但消息年龄必须在批处理窗口结束前回落;实时订单队列则应以分钟级甚至秒级年龄为主信号。告警内容要携带 vhost、队列、节点、当前值、趋势和触发窗口,让值班人员可以直接定位,而不是只收到一句“RabbitMQ 队列过长”。

🧪 故障演练:用证据证明边界真的成立

演练应在可控环境使用可追踪的测试事件,并提前定义恢复时间、允许重复次数和业务结果。每次演练都必须保存时间线、eventId、Broker 指标、应用日志和最终数据库结果;“服务恢复了”不能作为唯一通过标准。

演练操作预期证据
发布后断网Publisher 发送后立即中断网络,使 confirm 结果未知Outbox 保留未完成记录;恢复后重发同一 eventId;Consumer 可能收到重复投递,但 Inbox/唯一约束使业务只生效一次
提交后、ack 前终止Consumer 提交业务数据库后、发送 ack 前终止进程连接关闭后消息重新投递;库存不重复扣减;日志和数据库显示 Inbox 命中并安全 ack
下游持续失败让依赖持续返回可重试失败同一 eventId 按 30 秒、5 分钟、30 分钟退避;处理次数持续保留;第 5 次失败后进入停车队列,不再原地空转
Quorum 领导者停止确认队列成员和领导者后,停止当前领导者节点多数派仍在线时选出新领导者;客户端经过恢复后继续发布/消费;指标与日志记录选举期间的短暂停顿或延迟,而非消息丢失
消费速度不足降低 Consumer 数量/处理能力,或在受控范围提高发布速率ready 与最老消息年龄持续上升;容量组合告警触发;扩容前后的 ack rate 有对照,并记录数据库连接池等下游容量是否成为新瓶颈

发布后断网的注入点要覆盖“Broker 可能已经接管,但 Publisher 没收到 confirm”的窗口,而不是只测试发布前断网。提交后、ack 前终止必须精确落在数据库已提交之后,才能验证 effectively-once 依赖 Inbox,而非碰巧没有执行到业务更新。Quorum Queue 演练开始前应确认停止一个节点后仍保有多数派;如果本来就只剩最低多数派,继续停节点是在验证丢失 quorum,而不是正常的领导者故障转移。

每个场景的记录至少能重建下面这条证据链:故障注入时间 → 客户端观察到的异常 → Broker 的队列/连接/告警变化 → 重发或重投递使用的消息 ID → Inbox、Outbox 或停车队列状态 → 最终业务表结果。只有预期机制被实际触发、最终数据满足不丢失且不重复生效、告警在约定窗口内出现,演练才算通过。

✅ 上线检查表

上线评审可以直接使用下面的清单;任何一项不适用,都应记录原因和替代控制,而不是默认为已完成。

  • Connection 按进程长期复用,已验证断线恢复与连接风暴保护。
  • Channel 不被不安全地跨线程并发使用;并发策略和 Channel 上限已压测。
  • 关键发布启用 publisher confirms,并处理超时、nack 和结果未知后的同 ID 重发。
  • 关键发布启用 mandatory 或等价的不可路由检测,并监控 returned/dropped 消息。
  • 需要恢复的 Exchange/Queue 使用 durable,消息持久性与目标队列类型的语义已经核对。
  • 业务写入与 Outbox 事件在同一个本地数据库事务中提交。
  • 每个业务事件有稳定 eventId,Outbox 重发和重试转移不会生成新 ID。
  • Consumer 使用 Inbox、业务唯一约束、状态机或天然幂等写入;去重与业务更新处于同一事务。
  • Consumer 使用 manual ack,并且只在业务数据库事务提交后 ack。
  • 重试有明确错误分类、次数上限和 30 秒/5 分钟/30 分钟等退避,不存在无界原地 requeue。
  • 永久错误、毒消息和耗尽重试预算的消息进入可查询、可告警、可审计的停车队列。
  • 关键队列使用 Quorum Queue,并验证成员分布、领导者切换和多数派故障边界。
  • 磁盘与内存水位按节点容量设置,发布连接阻塞和恢复路径已纳入告警与运行手册。
  • 每个关键队列都有最老消息年龄告警,并以业务 SLO 设阈值。
  • 已用峰值发布速率、平均/尾部处理耗时和排空目标验证容量,同时确认下游连接池、锁、配额与顺序约束。
  • 已完成“发布后断网”演练并保存 Outbox、confirm 结果、同 ID 重发和消费去重证据。
  • 已完成“数据库提交后、ack 前终止”演练并保存重投递、Inbox 命中和最终业务结果。
  • 已完成“下游持续失败”演练并保存分级退避、处理次数与第 5 次停车证据。
  • 已完成“Quorum 领导者停止”演练并保存多数派、新领导者、客户端恢复和延迟证据。
  • 已完成“消费速度不足”演练并保存 ready、消息年龄、组合告警和下游容量证据。

🧭 结尾:用边界和证据管理可靠性

生产可靠性不是某个参数带来的承诺,而是把每一层的责任、失败窗口和恢复证据连接起来:Outbox 与 publisher confirms 处理发布边界,Quorum Queue 与集群机制提高 Broker 节点故障下的恢复能力,Inbox 或业务幂等吸收重复投递,有限重试与停车队列隔离无法立即恢复的消息,监控和故障演练则验证这些机制在真实故障窗口中确实生效。

这些机制仍有明确边界:at-least-once 允许重复,effectively-once 依赖业务事务内的去重;集群提高可用性,但不替代备份和跨地域灾备;告警恢复也不等于积压已经排空。上线评审应回到业务 SLO,检查每个边界由谁负责、失败后如何恢复,以及能否用 eventId、Broker 指标和最终业务结果重建完整证据链。

需要回看消息路由、队列、确认与竞争消费等基础模型,请阅读第一篇:RabbitMQ 核心原理;需要核对 .NET 客户端的连接、Channel、发布确认、消费确认与优雅停机实现,请阅读第二篇:RabbitMQ + .NET 10 实战。本篇到此只收束生产可靠性边界,不把客户端 API 或基础概念重新展开。

系列导航:

  1. RabbitMQ 核心原理:一条消息如何完成路由与消费
  2. RabbitMQ + .NET 10 实战:可靠地发布与消费消息
  3. RabbitMQ 生产实践:可靠性、幂等、重试与高可用(本文)

参考资料