RabbitMQ 消费者连着却不消费、队列一直涨

RabbitMQ 显示消费者健康、队列却越堆越多。在 RabbitMQ 4.x 上修 prefetch、unacked 消息、consumer_timeout 和死信路由。

RabbitMQ 控制台里消费者连接是绿的、消息也在投递,但队列深度一直涨、下游什么都没在做。

最快的修法: 先看管理 UI 里队列的 Unacked 列。如果它一直贴着你的 prefetch_count、从不下降,说明消费者拉了一批就没 ack,broker 于是停止继续推送。把 basic_qos(prefetch_count=N) 设成跟真实并发匹配,成功才 ack,失败用 basic_nack(requeue=False) 丢进死信队列,再确认队列设了 delivery-limit,让毒丸消息被淘汰而不是无限重投。在 RabbitMQ 4.x 的 quorum queue 上,这个上限默认是 20;经典队列得你自己加。

长任务场景里第二常见的原因是 broker 自己的 consumer_timeout(默认 30 分钟,即 1800000 毫秒):worker 还没跑完,broker 就把这条投递回收、重投给下一个 worker,于是无限循环。从 RabbitMQ 4.3 起(4.3.0 于 2026-04-23 发布),这个超时只在 quorum queue 和新的 JMS queue 上生效;经典队列和 stream 已经完全不再判断它了。

先判断你属于哪一类

看管理 UI 里的队列那行(http://<host>:15672,Queues 标签页),或者用 rabbitmqctl list_queues 看。对号入座:

症状大概率原因跳到
Unacked 贴着 prefetch、Ready 一直涨prefetch 被慢/不 ack 的消息占满原因 1
每条都 redelivered=true、每次投递 CPU 飙一下拉到 ack 之间崩溃或 PRECONDITION_FAILED原因 2、原因 6
Unacked 接近 0、没报错,但下游数据缺失auto_ack 模式悄悄吞了失败原因 3
worker CPU 接近 0、一次只处理一条单线程消费者却用高 prefetch原因 4
连接 state 显示 flow、节点有 alarm内存/磁盘水位——broker 在限流原因 5
quorum queue 上同一条消息约每 30 分钟回来一次,日志写 ... has timed out waiting for a consumer acknowledgement ...长任务撞上 consumer_timeout原因 6

常见原因

按踩坑频率排序。

原因 1. Prefetch 被未 ack 消息占满

消费者要了 prefetch_count = 100,处理得慢。100 条 unacked 全堆着等它消化一条,broker 就不再往这个通道投了。

怎么判断:管理 UI 里队列 Unacked 列基本等于消费者的 prefetch 总和;Ready 还在涨。

原因 2. 消费者拉了消息后崩掉

worker 拉了消息、panic 或者 OOM 被 kill,没发 basic_nack。RabbitMQ 把它算 unacked,等通道关了再投给下一个 worker,下一个也崩——毒丸消息死循环。

怎么判断:每条消息都 redelivered=true,(quorum queue 上)x-delivery-count header 一路涨。每次投递 CPU 都飙一下。没有进度。

原因 3. 关掉了手动 ack——auto_ack 把失败吞了

auto_ack=True(也就是 no-ack 模式)一投递就 ack 掉。处理失败消息也没了。队列深度看着没事、数据其实悄悄丢了。

怎么判断:unacked 不涨、broker 没报错,但消费方反馈数据莫名其妙不全。

原因 4. 单线程消费者却用高 prefetch

Python 单线程消费者配 prefetch=200,永远一次只处理一条。剩下 199 条占着位子、算 unacked。

怎么判断:worker CPU 接近 0,处理速率等于慢任务一条一条来。

原因 5. broker flow control 暂停了通道

内存或磁盘水位超了,broker 触发资源 alarm、阻断发布者(投递实际上也停滞)。消费者看着是连着的、消息就是不流。内存高水位默认是 vm_memory_high_watermark.relative = 0.6(检测到的 RAM 的 60%),磁盘默认 disk_free_limit.absolute50MB

怎么判断rabbitmqctl list_connections name state 显示 flow,或者管理 UI 顶上显示节点 memory/disk alarm 横幅。

原因 6. consumer_timeout 在任务跑到一半时回收投递

这个最容易被忽略。RabbitMQ 对消费者强制一个确认超时:如果一条投递在 consumer_timeout(默认 1800000 毫秒 = 30 分钟)内没被 ack,broker 就会把它回收。这个超时大约每分钟检查一次,所以实际触发会比配置值晚一分钟左右;低于 5 分钟的值官方不建议用。

不同版本行为不一样,先看清你的版本:

  • 4.3 之前:任何队列类型都可能撞上;broker 会用 PRECONDITION_FAILED 把整个通道关掉,该通道上所有消费者的所有投递都会被重新入队。
  • 4.3+:这个超时只在 quorum queue 和新的 JMS queue 上生效——经典队列和 stream 不再判断它。在 quorum queue 上,broker 现在走更温和、更有针对性的路径:对声明了 consumer_cancel_notify 的 AMQP 0.9.1 客户端,只给超时的那个消费者发 basic.cancel,通道和其他消费者保持不动(只有不支持这个能力的客户端才退回到关通道);对 AMQP 1.0 则用 DISPOSITION(state=released) 释放消息,而不是 detach 整个 link。

不管哪种,正在处理的那条消息都会被重投,于是一个本来就要跑超过超时时长的任务会无限循环。

怎么判断:broker 或客户端日志里出现类似 Consumer ... on channel N and queue 'jobs' ... has timed out waiting for a consumer acknowledgement of a delivery with delivery tag = ... 的行(4.3 之前或老客户端上还会有 PRECONDITION_FAILED)。同一条消息大约每隔 30 分钟回来一次。如果你在 4.3+ 上看到它,那这个队列就是 quorum queue。

最短修复路径

Step 1: 显式设 prefetch

prefetch 跟实际并发挂钩。常用规则:prefetch = 并发 * 2

Python(pika):

import pika

connection = pika.BlockingConnection(pika.ConnectionParameters('rabbitmq'))
channel = connection.channel()
channel.basic_qos(prefetch_count=10)   # 默认按消费者(global=False)

def on_message(ch, method, properties, body):
    try:
        process(body)
        ch.basic_ack(delivery_tag=method.delivery_tag)
    except Exception:
        ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)  # 进 DLX

channel.basic_consume(queue='jobs', on_message_callback=on_message, auto_ack=False)
channel.start_consuming()

Node(amqplib):

await channel.prefetch(10);
await channel.consume('jobs', async (msg) => {
  try {
    await process(msg.content);
    channel.ack(msg);
  } catch (e) {
    channel.nack(msg, false, false);   // 进死信、不 requeue
  }
}, { noAck: false });

Step 2: 配死信交换机

毒丸消息得有去处。让工作队列在 nack/reject 时进死信,并设投递上限,让一直失败的消息被淘汰、而不是无限循环。

# 声明 DLX 和 DLQ
rabbitmqadmin declare exchange name=jobs.dlx type=fanout
rabbitmqadmin declare queue name=jobs.dlq
rabbitmqadmin declare binding source=jobs.dlx destination=jobs.dlq

# 主队列(quorum)声明带 DLX + 投递上限
rabbitmqadmin declare queue name=jobs arguments='{"x-queue-type":"quorum","x-dead-letter-exchange":"jobs.dlx","x-delivery-limit":5}'

RabbitMQ 4.x 的 quorum queue 已经默认套了 delivery-limit = 20(4.0 起加的)。想用别的上限就显式设 x-delivery-limit。超过上限的消息会被丢弃,配了 DLX 就进死信——所以调低上限前先确认有 DLX。RabbitMQ 4.3 起,上限按 delivery-count(失败投递次数)算、而不是每次 requeue 都算,所以无限次的显式 basic_nack(requeue=True) 本身不再会撞上上限。

经典队列没有内置投递上限,得在应用代码里做:读 x-death header 的计数(DLX 循环会写),超过 N 次就 basic_nack(requeue=False)

Step 3: 告警盯 unacked、不是 ready

rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers

用内置的 rabbitmq_prometheus 插件(rabbitmq-plugins enable rabbitmq_prometheus 开启,在 15692 端口出 /metrics)。示例规则:

- alert: RabbitMQUnackedHigh
  expr: rabbitmq_queue_messages_unacked{queue="jobs"} > 50 and rate(rabbitmq_global_messages_received_total[5m]) > 0
  for: 10m
  labels:
    severity: warning
- alert: RabbitMQQueueDepthGrowing
  expr: rate(rabbitmq_queue_messages_ready[10m]) > 0 and rabbitmq_queue_consumers > 0
  for: 15m

rabbitmq_queue_messages_readyrabbitmq_queue_messages_unacked 这种按队列的指标来自 /metrics/per-object(或 /metrics/detailed,前缀是 rabbitmq_detailed_)端点;默认的 /metrics 是聚合的,所以如果你的看板要按 queue= 过滤,记得开启 per-object 指标。

Step 4: 看看 broker 是不是在 flow control

rabbitmqctl list_connections name state user
rabbitmqctl status | grep -E 'mem|disk'
rabbitmqctl list_node_alarms

stateflow 说明 broker 在限流这条连接。list_node_alarms 显示 memorydisk alarm 就是超水位了——清磁盘、减内存压力,或者(谨慎地)在 rabbitmq.conf 里提 vm_memory_high_watermark.relative / disk_free_limit.absolute 再 reload。

Step 5: 给长任务修 consumer_timeout

如果 list_queues 显示消息大约每 30 分钟重投一次、日志里写 ... has timed out waiting for a consumer acknowledgement ...,说明你的任务跑得比默认 consumer_timeout 还久。注意:RabbitMQ 4.3+ 上只有 quorum queue(和 JMS queue)才判断这个超时——你在那里看到它,说明这个队列是 quorum queue。三个正确修法,按推荐顺序:

  1. 偶发长任务最推荐:别把这条投递握着跑完整个任务。提前 ack、自己记录完成状态,或者把活拆小、让每次 ack 都远在超时之前落地。
  2. rabbitmq.conf 里提全局超时(单位毫秒),再重启节点(或用 rabbitmqctl eval 'application:set_env(rabbit, consumer_timeout, 3600000).' 在运行时生效):
# rabbitmq.conf —— 1 小时
consumer_timeout = 3600000
  1. 4.3+ 上还可以用 policy 或 x-consumer-timeout 队列参数给单个 quorum queue 设超时,这样一个慢队列就不必逼着全局值调高。完全关掉超时(consumer_timeout 设成非正值,或者不设)不建议,因为真卡住的消费者会一直握着投递不放、还可能挡住磁盘上的数据压实。

Step 6: 用 quorum queue 提稳定性

经典 mirrored(HA)队列在 RabbitMQ 4.0 已被移除——quorum queue 现在是唯一的复制/高可用队列类型,在重投上限和持久化上行为可预测。

rabbitmqadmin declare queue name=jobs durable=true arguments='{"x-queue-type":"quorum","x-dead-letter-exchange":"jobs.dlx","x-delivery-limit":5}'

quorum queue 从 RabbitMQ 3.8 起就有。截至 2026 年 6 月,当前稳定线是 4.3.x(4.3.2 于 2026-06-15 发布);4.2.x 在 2026-07-31 结束社区支持,还在用就该排升级了。如果你还在 4.0 之前、用着 mirrored 经典队列的集群上,升级到 4.x 前先按官方的经典镜像队列迁移到 quorum queue 指南走一遍。consumer_timeout 的精确语义见 RabbitMQ consumers 文档,队列类型变化见 4.3 release highlights

怎么确认修好了

  1. rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers——messages_ready 应该在下降,messages_unacknowledged 应该稳定在大约(prefetch × 消费者数)附近,而不是顶到天花板。
  2. 盯一条已知的坏(毒丸)消息:到了投递上限它该落进 jobs.dlq,而不是一直在 jobs 里冒出来。
  3. 长任务场景下,跑完一个完整任务后确认 broker 日志里不再出现新的 ... has timed out waiting for a consumer acknowledgement ...

预防

  • 永远 auto_ack=false;成功才 ack,失败 nack 到 DLQ。
  • prefetch_count 设成 worker 并发 * 2,永远别用客户端默认值。
  • 每个工作队列都配死信交换机和明确(或默认 20)的投递上限。
  • 任何任务可能跑超过 30 分钟,就刻意设好 consumer_timeout(4.3+ 上用 x-consumer-timeout 按 quorum queue 单独设,因为那里只有 quorum/JMS queue 才判断它)。
  • 告警盯 messages_unacknowledgedx-delivery-count / 重投上涨的模式。
  • 所有新工作队列都用 quorum queue;4.x 里已经没有经典 mirrored 队列了。

FAQ

消费者连着、consumers 计数也非零,队列怎么还涨? 连着不等于在消费。最常见的原因是投出去的消息卡在 Unacked 桶里、消费者一直没 ack,于是 broker 不再推超过 prefetch_count 的量。先看 Unacked 列。

prefetch_count 设多少安全? 跟真实并发挂钩,别跟着吞吐量的幻想来。单线程 worker 用小值(常是 110);N 个 worker 的池可以用大约 N × 2。在慢的或单线程的消费者上配高 prefetch,只会把消息卡成 unacked、还饿死别的消费者。

为什么同一条消息大约每 30 分钟回来一次? 那是 consumer_timeout(默认 1800000 毫秒)。任务跑得比超时还久,broker 回收了那条 unacked 投递、又重投了。提高 consumer_timeout(在 rabbitmq.conf 里全局设,或者 4.3 起用 x-consumer-timeout 参数 / consumer-timeout policy 按 quorum queue 单独设),或者提前 ack、自己记完成状态。RabbitMQ 4.3+ 上这只会发生在 quorum queue 和 JMS queue 上,经典队列和 stream 已经不再判断这个超时了。

RabbitMQ 4.x 上还需要写毒丸消息处理代码吗? quorum queue 上不用——它默认套 delivery-limit = 20(可用 x-delivery-limit 改),配了 DLX 就把超限消息路由进死信。经典队列上还是得用 x-death header 计数在应用代码里做。

经典 mirrored 队列还能用来做高可用吗? 不能了。经典队列镜像在 RabbitMQ 4.0 已被移除。quorum queue 是受支持的复制/高可用队列类型;升级到 4.x 集群前先迁移。

标签: #后端 #排查 #rabbitmq