生产事故:RabbitMQ 内存告警导致整个集群不可用——"流量控制"的连环陷阱

引言

周三下午,订单系统突然全线报错。监控大屏上 RabbitMQ 集群三个节点的内存指示灯齐刷刷变红,业务侧表现却是"冰火两重天":

  • 上游:订单服务发消息全部卡住,接口超时——生产者被 MQ 拒绝接收;
  • 下游:通知服务、积分服务的消费者日志一片寂静——消费者"拿不到"消息;
  • 队列里:明明积压了几十万条消息,消费速率却是 0。

消息既进不去、也出不来,整个异步业务链路在十分钟内彻底断开。复盘后发现,触发点小到令人发指:两天前一次消费者重构,把 ACK 模式改成了手动确认,但代码里异常分支既不 ack 也不 nack——消息被消费者"领走"后永远没有"交差",unacked 计数爬到上限后消费者不再被投递,队列只进不出,两天的积压最终撞穿内存水位线,触发了 RabbitMQ 最让人头疼的保护机制:Flow Control(流量控制)

这篇文章把事故完整复盘:RabbitMQ 的内存水位与流控机制是怎么一环扣一环地锁死集群的、手动 ACK 为什么会变成"消息黑洞"、修复方案和监控体系怎么建——重点不是骂代码,而是讲清楚这个连环陷阱的每一步,让它不会在你身上重演。


一、事故时间线:15 分钟里发生了什么

时间事件当时的现象
周一 10:00通知服务消费者重构上线:acknowledge-mode 改为 manual灰度流量正常,无人察觉异常
周一 14:00消费者日志开始出现零星异常(下游短信网关超时),异常分支吞掉异常后直接 return无 ACK,但消息量小,未告警
周二全天messages_unacknowledged 缓慢爬升,prefetch 槽位被逐步占满;新消息不再投递到这些消费者消费速率缓慢下降,值班同学误以为是流量低谷
周三 14:02队列积压监控告警:order.notify.queue 消息数 > 5 万群里有人开始看
周三 14:05MQ 节点内存使用率突破 0.4 水位线,内存告警(memory alarm)触发管理 UI 出现红色 Memory 告警条
周三 14:06Flow Control 启动:所有发布连接进入 blocked 状态订单服务发送线程全部阻塞,接口超时
周三 14:08业务雪崩:订单创建接口大面积超时,上游重试加剧积压P0 事故成立
周三 14:12排查发现 unacked 消息 48 万条,消费者代码异常分支无 ACK锁定根因
周三 14:15重启异常消费者实例:连接断开,unacked 消息重新入队(ready),内存压力骤降,告警解除链路恢复
周三 14:25消费追平积压,业务恢复正常——

最反直觉的一点:内存告警后,消费端并没有被 RabbitMQ 主动掐断——它在内存告警期间依然允许消费。但我们的消费者早已"自己把自己锁死"了:prefetch 槽位被永不确认的消息占满,RabbitMQ 认为它们"手里还有 250 条没处理完",不会再投递新消息。于是生产者被流控堵住、消费者被自己堵死,队列变成只进不出的堰塞湖。


二、RabbitMQ 内存与流控机制:保护机制如何变成连环陷阱

2.1 内存水位线:0.4 阈值是什么

RabbitMQ 有个核心配置 vm_memory_high_watermark默认值是 0.4:当节点内存使用量达到系统可用内存的 40% 时,触发内存告警(memory alarm)。

# 查看当前水位与内存使用
rabbitmqctl status | grep -A 8 memory
#   {total,{34359738368}},          ← 总内存 32GB
#   {watermark,0.4},                ← 水位线 40%
#   ...
# memory: 14.2GB / 32GB            ← 使用接近 0.44,告警已触发

设计初衷完全是善意的:Erlang 虚拟机之外,操作系统还需要内存(page cache、文件句柄缓冲区等)。如果 MQ 把内存吃到 90% 以上,机器会触发 swap(交换分区),Erlang 进程遇到磁盘级 swap 会慢 100 倍,节点直接卡死——所以 RabbitMQ 选择在 40% 就主动"踩刹车",给系统留足缓冲。

2.2 Flow Control:刹车是怎么踩的

内存告警触发后的连锁反应(这是事故的核心机制):

内存使用 > 0.4 水位线
        │
        ▼
① 节点触发 memory alarm(管理 UI 顶部出现红色告警条)
        │
        ▼
② 所有"发布消息"的连接被强制进入 blocked 状态
   ├─ RabbitMQ 停止从这些 TCP 连接读取数据
   ├─ 客户端发送缓冲区写满 → 发送调用阻塞
   └─ rabbitmqctl list_connections 可见 state = blocked
        │
        ▼
③ 生产者侧表现:channel.basicPublish() 卡住不返回
   ├─ 用 Confirm 模式:confirm 永远等不到(后续超时)
   ├─ 用事务模式:txCommit 阻塞
   └─ 业务线程池被发送线程占满 → 接口超时雪崩
        │
        ▼
④ 消费侧:投递不受内存告警直接阻止(可以继续消费帮 MQ 减压)
   └─ 但如果消费侧本身有问题(本案:prefetch 槽位被占满),就彻底死锁

关键点:Flow Control 阻塞的是"写",不直接阻塞"读"。RabbitMQ 的设计逻辑是"内存满了别再往里灌,但欢迎往外掏"。这也解释了为什么正常情况下内存告警能自愈——消费者把队列吃下去,内存回落,告警自动解除。但当消费侧也失效时,这个自救通道就断了——善意的保护机制变成了死锁的闭环。

管理命令速查(事故时我们一条条敲过):

# 哪些连接被 blocked(生产者卡住的直接证据)
rabbitmqctl list_connections peer_host peer_port state blocked_by

# 队列消息分布:ready(待投递) vs unacked(已投递未确认)——本案关键
rabbitmqctl list_queues name messages messages_ready messages_unacknowledged memory

# name                    messages  messages_ready  messages_unacknowledged  memory
# order.notify.queue     482331         2133              480198              6.1GB   ← 异常:unacked 占了 48 万
# order.dlx.queue         3201          3201                  0               8.2MB

# 内存被什么吃掉了
rabbitmqctl status | grep -A 20 breakdown

messages_unacknowledged 这个数字在本案中就是破案线索:正常情况下它应该等于"消费者正在处理中的消息数"(量级 ≈ 消费者数 × prefetch),稳定在一个小范围波动。它高达 48 万且只增不减,说明消息被领走后再也没回来确认。

2.3 内存到底被什么占了:unacked 消息的成本

很多人以为"消息投递给消费者就从队列里消失了",错。消息投递后只是状态从 ready 变为 unacked——RabbitMQ 依然持有这条消息的完整副本(在内存中,或可分页到磁盘),因为它随时准备在你不确认时重新投递给别人。

经典队列(classic queue)在内存压力下会把部分消息分页到磁盘,但这个机制在本案中没救得了场:分页速度跟不上持续涌入,且 unacked 消息的分页策略远没有 ready 消息激进。48 万条消息、每条附带业务 JSON(短信内容、用户信息,平均 2~5KB),6GB+ 内存就这么没了。


三、根因:手动 ACK 的"消息黑洞"

3.1 出事的代码长什么样

重构后的消费者(简化还原):

/**
 * ❌ 事故代码:手动 ACK 模式,但异常分支不 ack 也不 nack
 */
@RabbitListener(queues = "order.notify.queue")
public void onMessage(OrderNotifyMessage msg, Channel channel,
                      @Header(AmqpHeaders.DELIVERY_TAG) long tag) throws IOException {
    try {
        SmsResult result = smsClient.send(msg.getPhone(), msg.getContent());
        if (result.isSuccess()) {
            channel.basicAck(tag, false);              // ✅ 只有成功分支确认
        } else {
            log.warn("短信发送失败: {}", result);
            // ❌ 失败分支:既不 nack,也不 ack——消息永远停在 unacked
        }
    } catch (Exception e) {
        log.error("处理异常", e);
        // ❌ catch 里直接吞掉,同样没有任何确认动作
        // (更隐蔽的是:这里 return 之后,这条消息的 tag 就再也没人管了)
    }
}

配套配置:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: manual      # ← 重构时从 auto 改成了 manual
        prefetch: 250                  # ← 每个消费者最多"领走"250 条未确认消息
        concurrent-consumers: 8        # ← 8 个消费者线程
        # retry 未配置

3.2 为什么两天才爆:prefetch 的"温水煮青蛙"

事故代码不是一上线就炸的,机制是这样的:

单个消费者线程的视角:
  ① RabbitMQ 给它投递消息(最多 prefetch=250 条未确认)
  ② 正常处理 → ack,槽位释放,可以领新消息
  ③ 异常分支 → 无 ack/nack,这条消息永久占用 1 个槽位
  ④ 槽位从 250 → 249 → 248 → ... 逐步被"死消息"占满
  ⑤ 250 个槽位全满后,RabbitMQ 认为该消费者"手头工作堆积"
     → 不再投递新消息 → 这个线程事实上停止消费

单实例 8 线程 × 250 = 2000 个槽位;6 个实例 = 12000 个槽位被死消息占满
→ 消费者全部"假死",消费速率归零
→ 队列只进不出,ready 消息开始堆积
→ 48 万条消息(含 unacked 副本)逐步吃掉内存
→ 两天后撞穿 0.4 水位线 → Flow Control → 生产者也被堵死

prefetch 在这里扮演了放大器角色:它本意是"让消费者批量领消息减少网络往返",但在 ACK 逻辑有缺陷时,它变成了"每个消费者能安全吞掉多少条消息不被发现"的上限。prefetch 越大,黑洞容量越大,问题暴露越晚。

3.3 为什么是"集群不可用"而不是"一个队列慢"

复盘时被问最多的问题:一个通知队列出事,为什么整个集群挂了?

层级原因
内存告警是节点级内存超水位的节点上,所有 vhost、所有队列的生产者连接都被 block,不分业务
队列分布集群 3 节点承载了全部业务队列,通知队列恰好在节点 A,节点 A 先告警,该节点上所有业务的生产者被堵
连接漂移客户端重连会漂移到其他节点,但其他节点内存也在高位(消息共享/镜像副本),14:08 三个节点全部告警
业务链路依赖订单创建 → 发 MQ 是同步动作(Confirm 模式等待),发送阻塞直接拖垮订单接口

一个队列的消费缺陷,通过"内存"这个共享资源,升级成了整个节点乃至集群的事件——这是本事故最值得记住的教训:MQ 的资源隔离是软隔离,内存和连接是全局共享的。


四、修复方案:四层加固

4.1 第一层:ACK 模式纠偏(根因修复)

先立选型原则,再改代码:

场景推荐模式理由
通知类、日志类、允许偶发丢失或可重放AUTO(Spring 容器确认)监听器正常返回自动 ack,抛异常自动 nack/reject,无"忘记确认"可能
业务处理必须成功、失败要重试AUTO + 容器重试 + 死信队列见下方配置,可靠性不输给手写 manual
需要在 ack 前做复杂事务控制(如批量 ack、跨消息聚合)MANUAL,但必须保证每条路径都有确认try/finally 兜底

我们的通知业务属于第二类,最终配置:

spring:
  rabbitmq:
    listener:
      simple:
        acknowledge-mode: auto          # ✅ 容器托管 ACK
        prefetch: 30                     # ✅ 从 250 降到 30(见 4.3)
        concurrent-consumers: 4
        max-concurrent-consumers: 16     # 允许弹性扩容消费者
        retry:
          enabled: true                  # ✅ 容器内重试:异常时自动重试 3 次
          max-attempts: 3
          initial-interval: 1000ms
          multiplier: 2.0
        default-requeue-rejected: false  # ✅ 重试耗尽不重新入队(避免毒消息无限循环),进 DLX
/**
 * ✅ 修复后:AUTO 模式下业务代码只关心业务,ACK 由容器保证
 * 抛出异常 → 容器 nack;重试耗尽 → 拒绝并路由到死信队列
 */
@RabbitListener(queues = "order.notify.queue")
public void onMessage(OrderNotifyMessage msg) {
    SmsResult result = smsClient.send(msg.getPhone(), msg.getContent());
    if (!result.isSuccess()) {
        // 业务失败抛异常即可:容器负责重试 → DLX,消息永远不会"消失"
        throw new BizException("SMS_SEND_FAIL", result.getErrorMsg());
    }
}

如果确实需要保留 MANUAL(个别批量确认场景),强制规范写法:

/**
 * ✅ MANUAL 模式的安全模板:try/catch/finally 三路径全覆盖
 */
@RabbitListener(queues = "xxx")
public void onMessage(Message message, Channel channel) throws IOException {
    long tag = message.getMessageProperties().getDeliveryTag();
    boolean success = false;
    try {
        success = handle(message);
    } catch (Exception e) {
        log.error("处理异常, tag={}", tag, e);
    } finally {
        // 无论成功失败,必须给出结论:requeue=false 让失败消息走 DLX,不允许"悬着"
        if (success) {
            channel.basicAck(tag, false);
        } else {
            channel.basicNack(tag, false, false);
        }
    }
}

铁律:手动 ACK 模式下,消息的终态只有 ack 和 nack 两种,不允许存在"什么都不做"的代码路径。Code Review 时把"catch 块里有没有确认动作"列为 MQ 消费代码的必查项。

4.2 第二层:队列 TTL + 死信队列(兜底过期清理)

即使消费再出问题,也要让积压消息有"过期释放"的出路,避免无限吃内存:

/**
 * 队列声明:消息 TTL 30 分钟 + 死信路由
 * 超时未消费的消息自动进入 DLX,主队列内存/磁盘压力有上限
 */
@Bean
public Queue notifyQueue() {
    return QueueBuilder.durable("order.notify.queue")
            .withArgument("x-message-ttl", 1800000)              // 消息最长存活 30 分钟
            .withArgument("x-dead-letter-exchange", "order.dlx")  // 过期/拒绝 → 死信交换机
            .withArgument("x-dead-letter-routing-key", "notify.dead")
            // 可选硬上限:队列长度封顶,防止任何情况下无限堆积
            .withArgument("x-max-length", 200000)
            .withArgument("x-overflow", "reject-publish")        // 超限时拒绝新消息(生产者可感知)
            .build();
}

设计要点:

参数取值依据
x-message-ttl按业务时效定:短信通知 30 分钟后再发已无意义,比业务最大容忍延迟略长
x-max-length按消费能力 × 故障容忍时间估算(如峰值 5000 条/分钟 × 30 分钟 ≈ 15 万,留冗余设 20 万)
x-overflowreject-publish 让生产者在队列满时收到失败(配合 Confirm 可降级),优于静默丢头的 drop-head
DLX 队列死信消息进 DLX 后有独立消费者做人工/自动补偿,TTL 丢弃的是时效,不是数据

4.3 第三层:prefetch 与消费者并发调优

prefetch 的本质是"每个消费者手上允许同时有几条在途消息",调优原则:

prefetch 太小(如 1):每处理一条都要等网络往返拿新消息,消费者大量时间在等
prefetch 太大(如 250/1000):
  ① ACK 缺陷时黑洞容量巨大(本案)
  ② 单消费者囤积消息,集群负载倾斜(其他消费者闲着)
  ③ 消息在单节点内存里集中

经验值:
  消息处理 10~50ms 的快任务:prefetch = 20~50
  消息处理 100ms~1s 的慢任务(调外部接口):prefetch = 10~20
  并发靠 concurrent-consumers / max-concurrent-consumers 弹性扩,不靠调大 prefetch

我们把 prefetch 从 250 降到 30、消费者并发弹性 4~16,吞吐不降反升(负载更均匀),且任何消费缺陷影响面的"静默上限"从 12000 条降到 480 条,监控几分钟内就能发现异常。

4.4 第四层:内存水位与堆积监控告警

最后补上"早发现"的能力。RabbitMQ 自带 Prometheus 插件(3.8+):

# 启用
rabbitmq-plugins enable rabbitmq_prometheus
# 默认在 15692 端口暴露 /metrics

Prometheus 关键告警规则(按我们的真实配置整理):

groups:
- name: rabbitmq
  rules:
  # ① 内存水位:超过 0.7 × 水位线就预警(0.4 水位线 → 0.28 内存使用率),不要等到 blocked
  - alert: RabbitMQMemoryHigh
    expr: rabbitmq_process_resident_memory_bytes
          / rabbitmq_resident_memory_limit_bytes > 0.7
    for: 2m
    labels: { severity: P1 }
    annotations:
      summary: "RabbitMQ {{instance}} 内存使用超水位线 70%"

  # ② 内存/磁盘告警已触发(流控可能正在发生)
  - alert: RabbitMQAlarmActive
    expr: rabbitmq_alarms_memory_alarm == 1 or rabbitmq_alarms_disk_space_alarm == 1
    for: 0m
    labels: { severity: P0 }
    annotations:
      summary: "RabbitMQ {{instance}} 触发内存/磁盘告警,生产者可能已被阻塞"

  # ③ unacked 异常:unacked 数远超"消费者×prefetch"理论上限,或只增不减
  - alert: RabbitMqUnackedGrowth
    expr: sum(rabbitmq_queue_messages_unacked) by (queue) > 1000
    for: 10m
    labels: { severity: P1 }
    annotations:
      summary: "队列 {{queue}} unacked 消息持续 >1000,疑似 ACK 逻辑异常"

  # ④ 队列积压:ready 消息增长速率 > 消费速率
  - alert: RabbitMqQueueBacklog
    expr: rate(rabbitmq_queue_messages_ready[5m]) > 0
          and rabbitmq_queue_consumers > 0
    for: 5m
    labels: { severity: P1 }
    annotations:
      summary: "队列 {{queue}} 有消费者但消息持续积压"

  # ⑤ 生产者连接被 blocked(事故进行中)
  - alert: RabbitMqConnectionsBlocked
    expr: rabbitmq_connections_state{state="blocked"} > 0
    for: 1m
    labels: { severity: P0 }

告警设计的核心思路:告警要发在"水位线之前"和"死锁形成之前"。内存到 0.4 才告警等于事故已经发生,所以我们设了 0.7 × 水位线的预警;unacked 的异常增长(规则③)更是能在事故前两天就暴露问题——48 万条消息的黑洞,在 1000 条时就该被看见。


五、排查方法论:遇到 MQ 卡死按这个顺序查

把事故后的排查路径固化成 SOP:

第一步:看状态
  rabbitmqctl list_connections state        → blocked 连接有多少?
  管理 UI 顶部有没有红色 memory/disk alarm?
  → 有 blocked + memory alarm = 流控进行中,优先释放内存

第二步:看内存被谁占
  rabbitmqctl list_queues name messages messages_ready \
        messages_unacknowledged memory | sort -k5 -n
  → 重点看 memory 列最大、unacked 异常高的队列

第三步:判断堆积类型
  ├─ ready 高 + unacked 正常 + consumers=0 → 消费者挂了/没部署
  ├─ ready 高 + unacked 正常 + consumers>0 + 无消费速率 → 消费者处理慢/卡住
  └─ ready 高 + unacked 异常高(本案)→ ACK 逻辑黑洞,查消费代码异常路径

第四步:止血
  ├─ unacked 黑洞:重启问题消费者(连接断开 → unacked 自动 requeue 回 ready)
  ├─ 单纯积压:临时扩消费者实例 / 调大 max-concurrent-consumers
  └─ 内存告警:止血后消费追平,内存自然回落,alarm 自动解除

第五步:防复发
  ACK 模式审计 + TTL/DLX 补齐 + 4.4 的五条告警上线

六、常见问题

6.1 AUTO 确认模式会丢消息吗?

不会因为"忘记确认"丢消息。Spring 的 AUTO 模式语义是:监听器方法正常返回 → ack;抛出异常 → nack(配合 retry 重试,耗尽后 reject)。它丢失消息的唯一场景是"业务成功了但 ack 发送时连接断开"——这个窗口在 MANUAL 模式下同样存在(你手动 ack 的那一刻网络也可能断),不是 AUTO 特有的风险。真正需要 at-least-once 强保证的业务,靠的是消费幂等 + 死信补偿,而不是把 ack 写在业务代码里。

6.2 手动 ACK 什么时候才真正需要?

三类场景:① 批量处理后统一 ack(basicAck(tag, true) 批量确认多条);② 消息处理结果需要跨事务/跨资源协调(如先写库再 ack,且要求顺序严格);③ 需要自定义 requeue 目标(requeue=true 重新入队重试)。这些场景下务必套用 4.1 的 try/finally 模板,并配 Code Review checklist。

6.3 prefetch 设成多少合适?有公式吗?

没有精确公式,但有可靠的推导:prefetch ≈ 单条处理耗时(秒) × 期望单消费者吞吐(条/秒) 向上取整后 × 2 的冗余。例:单条处理 50ms、目标单消费者 200 条/秒 → 0.05 × 200 = 10,取 20~30。慢任务(调外部接口 500ms)反而要调小(10 左右),避免大量消息在一个消费者上超时堆积。总吞吐不够时加消费者实例,不要加 prefetch。

6.4 为什么不把内存水位线调高(比如 0.8)来"避免"流控?

这是事故后被提过的馊主意。水位线是根据 Erlang VM + 操作系统缓冲的安全比例定的,调高到 0.8 意味着内存吃满时没有缓冲,一旦分页机制跟不上就是节点 swap 卡死——那时不是"生产者 blocked 几分钟",而是节点整个无响应、集群脑裂风险,恢复只能重启。流控是保险丝,不是故障本身。内存紧张的正确解法是加内存、减少积压、迁移队列,而不是拆保险丝。

6.5 消息持久化了,unacked 也占内存吗?

占。持久化(deliveryMode=2)保证的是节点重启后消息不丢(磁盘副本),不代表消息平时只在磁盘上。经典队列在内存宽裕时消息常驻内存以保证性能,内存压力下才分页。且消息的元数据、索引无论如何都在内存中。不要以为"消息落盘了就不占内存",这是我们复盘时纠正的一个普遍误解。队列内存大时优先排查消息体积和积压量。

6.6 Lazy Queue(惰性队列)能解决这个问题吗?

能缓解"ready 消息吃内存"(惰性队列让消息尽量驻留磁盘),但解决不了本案的 unacked 黑洞——已投递未确认的消息管理逻辑不同,且惰性队列吞吐更低、延迟更高,不适合在线业务队列。它的定位是"海量消息、允许较慢消费"的场景(如日志、审计)。根因(ACK 缺陷)不解决,换队列类型只是让黑洞填得更慢。


七、总结

事故链条速查卡

代码层:手动ACK异常分支无确认
   ↓ 每条异常消息永久占用 1 个 prefetch 槽位
消费者层:槽位占满 → RabbitMQ 停止投递 → 消费者"假死"
   ↓ 消费速率归零
队列层:只进不出,ready + unacked 双份消息持续积压
   ↓ 内存爬升(48万条 ≈ 6GB)
节点层:内存超 0.4 水位线 → memory alarm → Flow Control
   ↓ 所有发布连接 blocked
集群层:生产者发送阻塞 → 订单接口超时 → 业务雪崩
   ↓
重启消费者 → unacked requeue → 内存回落 → alarm 解除(止血)
根因修复:AUTO确认 + TTL/DLX + prefetch=30 + 水位/堆积告警

四层修复对照表

层级措施防住的风险
代码acknowledge-mode=auto;manual 必须 try/finally 双路径确认ACK 黑洞(根因)
队列x-message-ttl + DLX + x-max-length + reject-publish无限积压、内存无上限
消费端prefetch 30 + 消费者并发弹性 4~16黑洞放大、负载倾斜
监控内存 0.7 水位预警、alarm P0、unacked 增长、积压速率、blocked 连接故障提前两天可见

关键数据

  • 事故影响:订单创建接口超时 17 分钟,P0
  • 根因消息量:unacked 48 万条、占用内存 6.1GB,两天缓慢形成
  • 止血耗时:重启消费者后 3 分钟内存告警解除
  • 修复后:同故障场景下 unacked 上限从 12000 条降至 480 条,监控 10 分钟内必告警;队列 TTL 保证积压 30 分钟自动释放

一句话

RabbitMQ 的 Flow Control 是保险丝不是故障——内存撞 0.4 水位线时,它宁可阻塞生产者也要保住节点不 swap 卡死;真正让集群死锁的是"写被堵、读也自己堵死自己"的闭环,而闭环的源头永远在消费侧。手动 ACK 的每一条消息都必须有终态(ack 或 nack),prefetch 不是越大越好,队列必须有 TTL 和死信兜底,监控要建在水位线之前——这四条做到,内存告警永远只是一条预警短信,而不是一次 P0。

给团队的建议

阶段建议
所有 MQ 消费代码审计 ACK 模式:能用 AUTO 不用 MANUAL;MANUAL 强制 try/finally 模板
所有队列补 x-message-ttl + DLX;无 TTL 的队列逐个给出业务理由
本周可做执行 list_queues messages_unacknowledged,unacked 长期 > consumers×prefetch 的队列立刻排查
监控上线 4.4 的五条告警,重点是内存水位预警和 unacked 增长告警
Code ReviewMQ 消费者代码 checklist:异常路径有无确认、prefetch 多大、失败消息去哪

互动话题:你们踩过 RabbitMQ Flow Control 的坑吗?unacked 消息数你们平时会监控吗?手动 ACK 的代码在 Code Review 时会专门检查异常分支吗?评论区聊聊。



标题:生产事故:RabbitMQ 内存告警导致整个集群不可用——"流量控制"的连环陷阱
作者:jiangyi
地址:http://jiangyi.space/articles/2026/09/04/1788585536408.html
公众号:服务端技术精选
    评论
    0 评论
avatar

取消