关于我们

质量为本、客户为根、勇于拼搏、务实创新

< 返回新闻公共列表

队列积压几百万条、消费越来越慢:RabbitMQ 的 prefetch、确认机制与队列类型到底该怎么配

发布时间:2026-10-10

一台跑了一年多的 RabbitMQ,监控上 ready 消息数从几百一路涨到几百万,消费速率不升反降,接着生产者开始报超时。运维先把消费者翻了一倍,速率回升了半小时,然后掉回原点,甚至比之前更慢。这个现象在消息队列运维里出现频率很高,处理方向却经常搞反。

  • 加消费者只在前半小时有效:说明瓶颈不在消费者数量,而在服务端的水位与队列状态
  • unacked 数居高不下:消息已经发给消费者但没确认,一直占着服务端资源
  • prefetch 用默认值:等于不限制,单个消费者可以把队列里的大半消息一次性搬回客户端
  • 内存高水位触发后开始换页:队列消息被写到磁盘,性能是断崖式下跌而不是线性变慢
  • 毒丸消息靠 nack + requeue 处理:一条处理不了的消息能把整个队列拖死

现象切入:积压曲线长什么样,先看哪几个数

真实的积压过程很少是"突然满了",它有一段可辨认的形状。先是 ready 缓慢抬升,消费速率持平;接着 ready 越过某个量级后消费速率开始往下走,这时候 unacked 通常是高的;再往后生产者侧出现 publish 超时,或者连接上出现 blocked 状态。这三个阶段对应的根因完全不同,第一个动作也不同。

盯住这几个指标就够了

队列维度看三个数:ready(待投递)、unacked(已投递未确认)、消费者数。节点维度看四个数:内存占用与高水位线的距离、磁盘剩余空间与磁盘告警线的距离、处于 blocked 状态的生产者连接数、磁盘写入等待时间(await 或 fsync 延迟)。消费端侧还要单独打一个点:单条消息处理耗时的 p50 与 p99。这十来个数能覆盖绝大多数积压定位,再多就看不过来了。

这里有个很实际的判断:如果 ready 涨但 unacked 稳定在低位数,问题多半在生产端入口;如果 ready 涨且 unacked 同步涨,问题在消费端或者 prefetch;如果 ready 涨、生产者 blocked 数大于零,那是服务端已经进入流控阶段,此时再动消费者数量意义不大。

四种积压成因与第一个动作

生产端突发:入口流量先涨

特征是 publish 速率先抬起来,ready 跟着涨,消费速率基本不变或者略有上升,unacked 平稳,节点内存缓慢增长。这种情况本质是消费者绝对吞吐不够,或者说消费能力一直贴着生产均值跑,没有冗余。第一个动作不是加机器,而是先看消费速率有没有被单条处理耗时卡住——很多时候是某类消息变大了、或者消费逻辑里多了一次同步 HTTP 调用,把单条耗时从 5 毫秒推到 80 毫秒,吞吐直接掉一个数量级。

确认是纯入口流量问题之后,加消费者是有效的,但要按目标吞吐算数量,不是按"翻一倍"来加。同时给入口做限流或者削峰,让生产速率回到消费能力之内,否则加多少都会被下一波流量吃掉。

消费端变慢:处理耗时先涨

特征是 publish 速率没变,单条处理耗时先涨,消费速率下降,ready 缓慢累积,unacked 会随着 prefetch 的大小一起涨。定位点在消费者进程本身:下游数据库慢查询、外部接口超时重试、线程池被打满、GC 停顿变长、或者消费者数量没变但每个消费者的并发线程被调小了。

第一个动作是拉一条处理耗时的分位曲线出来,对比积压开始的时间点。如果 p99 和 p50 一起涨,是整体性变慢,查下游;如果只有 p99 涨,是部分消息触发了慢路径,查消息内容分布。这一步没做就去调 prefetch,很容易把症状压下去、根因留着。

服务端流控:生产者被阻塞

特征是生产者连接出现 blocked/unblocked 状态切换,publish 端报超时或者写阻塞,节点内存或磁盘有一个触到了告警线。RabbitMQ 在资源吃紧时会通过流控机制反压:先阻塞发布连接,网络层不再读数据,生产端表现为写入卡住。这是保护机制,不是故障本身。

第一个动作是确认是内存还是磁盘触发的。内存触发的流控通常伴随换页动作,磁盘触发的流控意味着磁盘剩余已经低于告警线。此时加消费者不但无效,还会因为新增的内存占用让水位更高。正确动作是先清出资源:临时降低生产速率、清掉无人消费的陈旧队列、把大积压队列里可以丢弃的历史消息先 purge 一部分,等水位回落再谈扩容。

磁盘水位触发:消息被刷盘

特征是磁盘写入等待时间飙升,队列消息被写入磁盘,消费速率出现断崖而不是缓坡。持久化消息、惰性队列、仲裁队列这三种情况都会把落盘压力放大。如果磁盘是机械盘或者云盘 IOPS 有上限,几百 MB 的消息往盘上刷,消费一条就要一次读,速率掉到每秒几十条是很常见的。

第一个动作是看磁盘 await/fsync 延迟,而不是看容量。容量够但 IOPS 打满的盘一样会让队列停摆。确认之后,短期动作是降低持久化比例或者把非关键队列改成内存优先,中期动作是换 fsync 更稳定的存储介质。

prefetch 的账与算式

prefetch 是消费者通过 basic.qos 告诉服务端"我一次最多拿多少条未确认消息"。它是 RabbitMQ 调优里性价比最高的一个参数,也是被忽视得最彻底的一个——很多线上集群一直在用默认值,也就是不限制。

值太大:消息在客户端囤积

prefetch 不限时,服务端会把队列里的消息尽可能推给已注册的消费者。结果是先启动的那个消费者可能一次拿走几十万条,后启动的消费者几乎分不到消息。这些消息在服务端显示为 unacked,一直占着内存与索引,不能被换页也不能被丢弃,也无法重新分配给空闲消费者。客户端这边同样难受:消息堆在消费者进程的内存里,Java 客户端表现为堆内存上涨,最终 OOM 或者 GC 停顿拉长,处理耗时进一步上升,形成正反馈。

另一个副作用是"假积压"。ready 看起来不高,但 unacked 巨大,队列深度实际是两者之和。只看 ready 判断积压严重程度的监控会在这时候给出错误结论。未确认消息在消费者断开后会被全部重新入队,一个消费者重启可以把几十万条消息瞬间打回队列头部,下一次启动又是一轮同样的循环。

值太小:网络往返成为瓶颈

prefetch=1 时,消费者处理完一条、发回 ack、服务端再投下一条。每条消息都要一个完整的网络往返,端到端耗时至少是"处理耗时 + RTT"。跨机房或者跨城部署时 RTT 可能到几毫秒甚至几十毫秒,处理耗时如果是 1 毫秒,那 90% 以上的时间都花在等网络上,吞吐上不去。这种情况的典型特征是消费者 CPU 很闲、网络包量很大、吞吐远低于消费者数除以处理耗时的理论值。

需要说明的是,prefetch=1 并不是"安全"的代名词。它只是把未确认消息数压到最低,代价是吞吐。真正在意不丢消息,靠的是确认机制与持久化,不是靠把 prefetch 设成 1。

按自家吞吐反推 prefetch 的算式

先定三个量:目标吞吐 T(条/秒)、消费者数 N、单条处理耗时 t(秒,取 p50 而不是平均值,取 p99 会得到偏保守的值)。再补一个量:消费者到服务端的网络往返 RTT(秒,机房内通常可以忽略,跨城必须计入)。

单消费者目标速率 r = T ÷ N。为了保证消费者处理完手上消息时下一条已经在路上,在途消息数的下限是 r × RTT;为了让消费者不会因为等消息而空转,合理在途量还要覆盖一段处理时间窗口。工程上常用的取值是:

prefetch ≈ (T ÷ N) × (t + RTT) × 余量系数,向上取整到 10 的倍数,余量系数取 1.5 到 2,消费者耗时抖动大的场景取 3。

举个例子(通用工程估算,非实测数据):目标 3000 条/秒,30 个消费者,单条处理耗时 30 毫秒,RTT 1 毫秒。r = 100 条/秒,t + RTT = 0.031 秒,乘出来是 3.1,余量系数取 2 得 6.2,向上取整到 10 的倍数取 10。这个数看起来很小,但它是对的——每个消费者每秒处理 100 条、每条 30 毫秒,手上同时有 3 条就够周转了,给它 300 条纯属让它囤货。

同一个算式在耗时不同的场景会给出完全不同的结论:如果单条处理耗时是 1 毫秒,消费者数只有 3 个,同样是 3000 条/秒,r = 1000,结果约 2,余量后取 5;反过来如果单条耗时 500 毫秒、消费者 100 个,r = 30,算式给到约 30,余量后取 60。这说明 prefetch 没有通用推荐值,只有按自家耗时算出来的值。

还要卡一个上限:单消费者在途消息占用的内存 = prefetch × 单条消息大小。如果单条消息 200 KB、prefetch 200,单消费者就是 40 MB,100 个消费者是 4 GB。这个上限常常比吞吐算式更早撞墙。

批量处理与批量确认怎么改变这个数

如果消费者改成一次取一批(比如攒够 50 条或者等满 100 毫秒触发一次批量处理),然后整批处理完用 multiple ack 一次性确认,网络往返次数降为原来的 1/50,prefetch 的约束就变了:应该把 prefetch 设成批大小的整数倍,常见做法是 2 到 3 倍,让消费者在忙一批的时候下一批已经在路上。同时批量 ack 会把重复投递的范围从"一条"放大到"一批"——消费者在批量处理中途崩溃,整批都会重新投递,业务侧的幂等设计必须能承受这个粒度。

还有一个容易被忽略的联动:消费者线程池大小与 prefetch 的关系。如果每个消费者进程内部开了 M 个业务线程并行处理,那实际在途量是 prefetch,但并行度是 M,此时的 prefetch 至少要大于 M,否则线程会空转;反过来 prefetch 远大于 M 时,多余的消息只是堆在内存里排队。经验做法是让 prefetch 略大于 M,比如 M 的 1.5 倍,并且用有界队列限制内部排队长度,避免内存无上限增长。

确认机制与毒丸消息

auto ack、manual ack、批量 ack 各是什么账

auto ack 模式下,消息一发出去服务端就认为已确认,消费者还没处理完,消息在服务端就已经不存在了。消费者崩溃、进程被 OOM 杀掉、网络断开,这批消息就彻底没了。它的吞吐最好,代价是丢消息,只适合允许丢失的场景,比如可以被重新采集的指标数据。

manual ack 模式下,消息在消费者确认之前一直处于 unacked 状态,占服务端资源,消费者断开后会被重新入队并投递给别的消费者。这是"至少一次"语义的基础,也是绝大多数业务该用的模式。它的成本就是前面说的 unacked 占用,而控制这个成本的手段正是 prefetch。

批量 ack(multiple ack)把确认号之前的所有消息一次性确认,减少网络往返,代价是确认粒度变粗。它在高吞吐场景是必要的,但要确认业务能接受"一批重新投递"。

毒丸消息是怎么把整个队列拖死的

消费者遇到一条处理失败的消息,用 basic.nack 并且 requeue=true 把它塞回队列。因为重新入队的消息会被放回队列头部附近,它几乎立刻又被投递给同一个消费者,再次失败,再次 requeue。这个循环每秒可以跑几千次,单条消息就能把一个消费者的全部吞吐吃光;如果这类消息有一批,整个队列的消费速率会掉到接近零,而 ready 数还在涨——从监控上看就是"明明有消费者在跑,积压就是清不掉"。

判断方法很直接:看队列的 redelivered 计数或者消费端的重复投递日志,如果 redeliver 速率和投递速率几乎相等,就是毒丸循环。另一个特征是消费者 CPU 很高但业务量没增长。

该用什么路径替代

正确的做法是失败不回主队列,而是走一条独立的重试与死信路径:消费者处理失败时,用 basic.reject 且 requeue=false,让消息进入死信交换机,投递到重试队列;重试队列带 TTL,到期后通过死信交换机回到主队列,实现延迟重试;重试次数写在消息头里,超过阈值就进死信队列等待人工处理或者落库。这样主队列永远只跑"能处理的消息",毒丸被隔离在旁路队列里,不会阻塞正常流量。

重试队列的 TTL 应该按等级拆分,而不是所有重试都等同一个时间。常见做法是三档:秒级(5 秒到 30 秒,应对瞬时抖动)、分钟级(1 分钟到 5 分钟,应对下游短暂不可用)、然后是死信。三档以下通常够用,档位太多运维复杂度上升但收益递减。

幂等:重复投递在业务侧怎么兜

只要用了 manual ack,重复投递就是必然事件而不是意外:消费者处理成功但 ack 前断开、网络分区导致服务端没收到 ack、批量 ack 中途失败,都会造成同一条消息被处理两次。所以幂等不是可选项。常见的兜底方式有三种:业务侧用消息 ID 做去重表或者唯一索引,重复插入直接冲突;用状态机保证重复操作是幂等的(比如"订单置为已支付"重复执行结果一致);或者用版本号/乐观锁,让过期消息写不进去。去重表的清理策略要提前定好,否则它会自己长成一张大表。

队列类型的取舍

classic 队列:默认、内存优先、吞吐最好

classic 是默认队列类型,消息尽量留在内存里,只有在内存压力下才换页到磁盘。它的单条延迟最低、吞吐上限最高,是绝大多数业务的首选。代价是节点故障时队列的可用性依赖持久化与复制策略:如果队列没做复制,节点挂了队列就不可用;如果消息不是持久化的,重启就丢。

惰性队列:为大规模积压设计,单条延迟上升

惰性(lazy)模式的队列把消息尽早写入磁盘,内存里只保留少量索引与在途消息。它的设计目标就是"队列里堆着几百万条也不把内存吃光",适合生产者快、消费者慢、允许较高单条延迟的场景,比如日志汇聚、离线任务分发。代价是每条消息几乎都要一次磁盘读,单条延迟明显高于 classic,吞吐受磁盘 IOPS 约束。

惰性模式的代价常被低估:一个平时每秒几千条的队列,改成惰性之后可能掉到几百条,如果业务对端到端延迟有要求(比如秒级),这个改动会直接违反 SLA。所以惰性队列更适合一开始就规划成"允许慢"的队列,而不是在积压发生之后临时切换。

仲裁队列:强一致复制,写入开销更高

仲裁(quorum)队列基于 Raft 类共识协议做复制,消息写入需要多数节点确认,节点故障时数据安全性明显高于 classic。它同时要求在声明时就是持久化的,不支持瞬时消息。代价是每次发布都要一次复制往返加一次 fsync,写入延迟和磁盘压力都高于 classic,吞吐上限也更低。它适合的是"消息比性能重要"的场景:订单、支付、库存变更这类丢不起的数据。

仲裁队列对磁盘 fsync 延迟极度敏感。同样一份消息量,classic 队列在普通 SSD 上跑得好好的,换成仲裁队列放在 fsync 抖动大的盘上,发布延迟可以从个位数毫秒涨到几十毫秒。规划仲裁队列时,磁盘的写入延迟是比容量更重要的指标。

持久化与队列类型是两件事

这是一个高频混淆点。"消息持久化"指的是投递时把 delivery mode 设为持久化,服务端会把消息写入磁盘再确认;"队列类型"指的是队列的存储与复制模型。classic 队列可以是持久化的,惰性队列也是持久化的,仲裁队列则强制持久化。反过来,队列声明为 durable 只代表队列元数据在重启后还在,不代表队列里的消息会持久化——消息是否落盘由投递时的 delivery mode 决定。

另一个容易搞混的是复制。经典镜像队列在较新版本中已被移除或不再推荐,新部署应该用仲裁队列来做复制。复制带来的额外开销有两块:网络上是每条消息多一次跨节点传输,磁盘上是每个副本各写一份。三副本的仲裁队列,写入放大是三倍,规划磁盘 IOPS 时要按副本数乘。

网络分区之后怎么恢复

集群发生网络分区时,不同分区里的节点可能同时接受写入,恢复时必然要有取舍。常见的恢复策略有几种取向:少数派自动暂停自己(牺牲少数派的可用性换取一致性)、自动治愈(选一个分区为权威、丢弃其他分区的数据)、人工决策。选择哪种取决于业务能接受"丢一部分消息"还是"一段时间不可用"。用仲裁队列的集群在分区场景下行为更可预测,因为它本身就是按多数派写的,少数派分区根本写不进去。

运维上真正重要的是提前定好策略并写进配置,而不是等分区发生再决定。分区期间还要盯住一件事:被暂停的节点恢复后,队列需要重新同步数据,这段时间的磁盘与网络开销会明显高于常态,如果同步窗口赶上了业务高峰,会看到一次"莫名其妙"的性能下降。

内存与磁盘水位:两个隐形开关

内存高水位:换页到磁盘的断崖

节点内存占用达到高水位线后,服务端开始把队列中的消息换页到磁盘,腾出内存。这个动作的特征是性能断崖而不是缓坡:内存里的消息可以随手投递,换页之后每投递一条都要一次磁盘读。默认值因版本不同而异(常见文档中的默认比例是物理内存的 40%),以所用版本的官方文档为准。

换成惰性队列之后换页行为会变化,因为消息本来就在盘上;仲裁队列的处理方式也与 classic 不同,不会用同样的方式整体换页。这就是为什么"同样的内存压力,不同队列类型的表现差别很大"。

磁盘告警线:直接阻塞生产者

磁盘剩余空间低于告警线时,服务端会阻塞所有发布连接,防止把磁盘写满。默认值因版本不同而异(常见文档中默认为 50 MB 左右),以所用版本官方文档为准。这个阈值明显偏低,实际部署几乎一定要调大:50 MB 在现代磁盘上几乎是"已经写满了才报警",等告警触发时往往已经来不及优雅处理。

为什么"加了消费者反而更慢"

这是水位阶段最容易出现的反直觉现象。加消费者带来三个额外开销:每个新连接和信道都要占一部分固定内存,内存占用上升意味着离高水位更近、换页更激进;新消费者开始消费后,被换页到磁盘的消息要被读回来,磁盘 IOPS 被进一步打满;如果消费者端 prefetch 较大,unacked 数上升,又反过来增加了无法换页的内存占用。三个因素叠加,就出现了"消费者越多、速率越低"。

所以出现加消费者无效甚至变慢时,第一步应该是看内存与磁盘水位,而不是继续加机器。

这两个阈值该怎么设、留多少余量

内存高水位的设置原则是"留出足够时间让运维反应"。如果节点 32 GB 内存,高水位设在 40%,那么 12.8 GB 之后开始换页;真正的问题是从水位触发到内存耗尽的窗口有多长。积压增长快的集群应该把水位调低一点(更早开始换页,换取更长的反应时间),而不是调高。同时要给水位配置配套的告警,在水位之下再设一条预警线,比如水位的 80%。

磁盘告警线应该按"增长速率 × 响应时间"来算:如果磁盘每分钟能被写入 2 GB,运维从收到告警到处理完成需要 10 分钟,那告警线至少要 20 GB 以上,并且留一倍余量。只按容量比例设、不考虑写入速率,是最常见的踩坑方式。这个告警必须真的接进告警通道,很多团队的磁盘告警配了但没人收。

死信与延迟重试:TTL + DLX 的队头阻塞

同一队列里混着不同 TTL 会互相阻塞

用 TTL + 死信交换机做延迟重试时,一个自然的想法是"重试队列设一个 TTL,消息过期后死信回主队列"。但如果需要多档延迟,很容易把所有重试消息塞进同一个队列、用每条消息各自的 TTL 来控制时间。问题在于 per-message TTL 的过期判定只在消息到达队头时发生:队头是一条 5 分钟后过期的消息,后面排着一条 10 秒后过期的消息,后者必须等前者过期出队之后才会被检查。结果就是短 TTL 被长 TTL 堵住,实际延迟远大于设定值。

按延迟等级拆队列

最直接的解法是每个延迟档位一个队列,队列级 TTL 而不是消息级 TTL。队列级 TTL 对整个队列统一生效,不存在队头阻塞问题。代价是队列数量翻倍,声明、绑定、监控都要跟着扩展,档位一多运维会变得啰嗦。实践中三到四档通常够用,档位设计成指数级(10 秒、1 分钟、5 分钟)比线性(10 秒、20 秒、30 秒)更能覆盖不同故障时长。

延迟消息插件

延迟消息交换机插件提供了按消息指定延迟时间的能力,用起来比拆队列优雅,不需要为每个档位建队列和绑定。它的代价在运维侧:待投递消息保存在插件的内部存储里,节点重启或插件异常时的行为需要按所用版本确认,规模很大时也要额外评估内存与恢复时间;同时它不像仲裁队列那样有多数派复制,数据安全性不如队列方案。要不要上这个插件,取决于团队愿不愿意为它单独建立一套监控与演练流程。

三种队列配置路线对比

对比维度 路线一:classic 队列 + 内存优先 + 适中 prefetch 路线二:惰性队列 + 大积压场景 路线三:仲裁队列 + 强一致复制
单条延迟 最低,消息常驻内存,投递路径最短 明显偏高,每条消息基本都要一次磁盘读 中偏高,写入需多数节点确认加一次 fsync
吞吐上限 最高,适合每秒数千到数万条的常规业务 受磁盘 IOPS 约束,通常为 classic 的几分之一 低于 classic,随副本数增加进一步下降
磁盘 IOPS 敏感度 常态下不敏感,换页阶段敏感 全程高度敏感,盘慢则队列慢 对 fsync 延迟极度敏感,写入放大按副本数计
故障时消息安全性 取决于是否持久化与是否有复制,单节点最弱 消息已落盘,重启不丢,但无跨节点复制 最强,多数派确认,节点故障不丢已确认消息
适用积压规模 十万条以内,积压应被快速消费掉 百万条级常态积压,允许分钟级延迟 中小规模积压,优先保证不丢而非堆量
内存占用 与常驻消息量成正比,积压时会快速上涨 最低,消息主要在磁盘,内存压力小 中等,多副本各存一份,内存与磁盘都按副本计

规格反推:内存、磁盘、网络、连接各按什么算

消息中间件服务器的规格反推和 Web 服务器不一样,它不怎么看 CPU,主要看内存、磁盘写入延迟和网络双向吞吐。下面这套估算是通用工程口径,实际值要按自家消息大小与吞吐实测修正。

内存:三块加在一起

第一块是常驻消息量:常驻条数 × 单条大小,再乘一个索引与元数据开销系数(工程上常取 1.3 到 1.5,具体系数与版本实现有关)。如果队列里常驻 50 万条、单条 2 KB,就是 1 GB,加上开销约 1.3 到 1.5 GB。第二块是 unacked 占用:消费者数 × prefetch × 单条大小,这一块是硬占,不参与换页。第三块是连接与信道的固定开销:每个连接和每个信道都有固定内存成本,几千个信道会吃掉可观的量。三块相加之后再除以规划的内存高水位比例,才是需要的物理内存——如果高水位是 40%,那常驻 4 GB 就需要至少 10 GB 的物理内存。

磁盘:看 fsync 延迟而不是只看容量

持久化消息、惰性队列、仲裁队列这三种情况都把磁盘写入延迟直接映射到发布延迟上。评估磁盘时应该看的是 fsync 延迟的 p99 和抖动范围,而不是容量够不够。一块 p99 fsync 延迟 20 毫秒的盘,跑仲裁队列时发布延迟不可能低于这个量级。容量侧按"峰值积压量 × 单条大小 × 副本数 × 安全余量(1.5 倍以上)"估算,同时留出换页与队列索引的空间。从介质选择上看,内存优先、磁盘 fsync 稳定的服务器是消息中间件的主要比选方向,一万网络在这一点上给客户的建议通常是先把磁盘写入延迟指标定死,再回头选机型与盘型,而不是先定容量再碰运气。

网络:双向吞吐一起算

消息中间件的网络压力是发布加消费双向叠加的:入口吞吐是生产速率 × 单条大小,出口吞吐是消费速率 × 单条大小,副本同步还要再乘副本数。稳态下两者大致相等,但清积压阶段消费速率会高于生产速率,出口带宽成为短时瓶颈。规划时按"峰值吞吐 × 单条大小 × 2 × 副本数"估算,再留 30% 余量。大消息这块也要注意的问题:单条消息超过几百 KB 时,网络的包量与序列化成本都会显著上升,吞吐会跌得比线性更快。工程上的通行做法是大消息只放引用(比如对象存储的地址),消息体里只传 ID 和元数据,正文走独立的存储通道。

连接与信道:少量长连接 + 多信道

连接和信道的内存占用基本是线性的,一个进程开一个连接、每个线程开一个信道的模式在上规模之后会明显吃内存。常见做法是少量长连接 + 多信道:每个消费者进程维持一到两个长连接,在连接上按需开多个信道,线程与信道可以池化复用。这样既控制了连接数,又不牺牲并发度。

连接抖动与心跳也要提前配。心跳间隔决定了服务端多久能发现一个僵死连接,间隔太长会导致消费者已经死了但消息还挂在 unacked 里;间隔太短在网络抖动时会误判断开,造成大量消息重新入队。默认值因版本不同而异,以所用版本官方文档为准;跨城或者跨公网部署时通常需要显式调整。消费者重连必须带退避,否则一批消费者同时重连会形成连接风暴。

避坑指南

坑一:prefetch 保持默认的 unlimited。为什么发生:客户端库默认不限制,开发者不知道要设。怎么判断:unacked 数长期处于高位、消费者之间消息分配极不均匀、客户端内存随积压一起涨。怎么规避:按前面给的算式算出具体数值并写进客户端配置与代码评审清单,不要留默认值;同时把 unacked 作为一级监控指标。

坑二:用 TTL + DLX 做统一延迟重试。为什么发生:一条队列搞定所有重试看起来最省事,但 per-message TTL 只在队头判定。怎么判断:消息的实际重试间隔明显大于设定值,且间隔随队列长度变化。怎么规避:按延迟等级拆成多个带队列级 TTL 的队列,或者评估延迟消息插件并配套监控与演练。

坑三:没设磁盘水位告警。为什么发生:默认告警线很低,很多团队没意识到要改,或者告警配了但没接通知通道。怎么判断:等到生产者被阻塞才发现磁盘满了。怎么规避:按"写入速率 × 响应时间 × 2"重设告警线,接入告警通道并定期演练一次。

坑四:把持久化消息和惰性队列混为一谈。为什么发生:两者都"写到磁盘",容易被当成同一件事。怎么判断:以为开了持久化就等于能扛大积压,结果内存照样被打爆。怎么规避:明确区分——持久化管的是重启后消息还在不在,惰性队列管的是常态下消息放内存还是放磁盘;想要抗大积压要改队列模式,不是改 delivery mode。

坑五:消费者里做重活。为什么发生:消费逻辑里塞了同步 HTTP 调用、复杂计算或者同步写库,单条处理耗时被放大几十倍。怎么判断:处理耗时 p99 远高于 p50,且耗时分布与外部接口延迟曲线重合。怎么规避:消费者只做编排,重活丢给独立的任务服务;外部调用必须带超时与熔断,不能无限等待。

常见问题

Q1:积压多少条算严重?

A1:看条数不如看增长趋势和消费耗时。一个每秒消费 5000 条的队列,积压 10 万条 20 秒就清完了,不算问题;同样 10 万条放在每秒消费 50 条的队列上,那是 30 多分钟。真正该盯的是积压的增长速率:如果 ready 在以每秒几百条的速度净增长且没有收敛迹象,不管当前多少条都要处理。另一个判断口径是"清完需要多久",超过业务能容忍的延迟上限就是严重。

Q2:prefetch 到底设多少合适?

A2:没有通用推荐值,按"目标吞吐 ÷ 消费者数 × (单条处理耗时 + RTT) × 1.5 到 2"算,再向上取整到 10 的倍数。算出来如果很小(比如 10)不要怀疑,那正是它该有的值——prefetch 的作用是保证消费者手上有活干,不是让它囤货。同时要校验上限:prefetch × 单条大小 × 消费者数不能超过客户端和服务端能承受的内存。批量处理场景则把 prefetch 设成批大小的 2 到 3 倍。

Q3:消费者该加多少个?

A3:按目标吞吐和单消费者实际吞吐反推:需要的消费者数 = 目标吞吐 ÷ (1 ÷ 单条处理耗时 × 内部并发度)。比如目标 3000 条/秒,单条耗时 30 毫秒、单消费者内部 4 并发,单消费者约 133 条/秒,需要约 23 个。加之前先确认两件事:内存和磁盘水位是否还有余量,以及下游能不能承受更高的并发压力。如果下游是瓶颈,加消费者只会把压力传导过去。

Q4:消息要不要持久化?

A4:看丢一条的代价。订单、支付、库存这类数据必须持久化,并且配持久化队列和复制;可以被重新采集的指标、可重算的缓存失效通知,不持久化能换来明显更高的吞吐和更低的磁盘压力。要注意的是持久化不等于"一定不丢":消息写入磁盘到 fsync 完成之间仍有窗口,需要更强保证就上仲裁队列或者发布端确认机制。持久化还会放大磁盘 IOPS 压力,规划时按峰值吞吐算。

Q5:惰性队列会不会很慢?

A5:会比 classic 慢,单条延迟明显上升,吞吐受磁盘 IOPS 约束,掉到 classic 的几分之一是常见情况。它的设计目标本来就不是快,而是"堆几百万条也不吃内存"。所以它适合的场景是原本就允许分钟级延迟的离线类业务,不适合在线链路。需要提醒的是,别在积压已经发生之后临时把队列改成惰性模式来救火,那样只是把内存问题换成磁盘 IOPS 问题。

Q6:内存水位调到多少合适?

A6:默认值因版本不同而异,常见文档中默认是物理内存的一个比例(约 40%),以所用版本官方文档为准。调整的方向应该是"留出反应时间"而不是"尽量多装":积压增长快的集群把水位调低一些,让换页更早开始,换取更长的处置窗口。同时在水位之下设一条预警线(比如水位的 80%)并接入告警。调之前先确认换页目标磁盘的性能,换页到慢盘上等于把内存问题换成更严重的磁盘问题。

Q7:队列类型能不能中途改?

A7:不能直接改,队列类型在声明时就确定了。变通做法是新建一个目标类型的队列,让生产者切到新队列,等旧队列消费干净后下线——这需要生产者支持双写或者灰度切换,业务侧要能接受短暂的两套队列并存。仲裁队列与 classic 之间尤其不能原地切换。所以队列类型的决策应该在新业务上线前做,而不是等出问题了再改;用策略(policy)声明默认队列类型可以减少这类遗漏。

Q8:积压一直清不掉,有没有兜底方案?

A8:有,但要分级。第一级是止损:对明确可丢的历史消息按时间范围 purge,或者把队列里的消息转储到对象存储后离线慢慢处理,先把在线链路恢复。第二级是加速:临时加消费者并把 prefetch 调大,前提是内存和磁盘有余量。第三级是绕行:让生产者把新消息发到一个全新的空队列,老队列单独慢慢清。选择哪一级取决于业务能不能接受丢消息,所以"哪些消息可以丢"这个问题应该在平时就定义好,而不是在故障现场临时决定。

RabbitMQ 积压治理一文的数据来源、参考范围与估算口径

本文关于 prefetch(QoS)机制、消费者确认模式、队列类型(classic / 惰性 / 仲裁)、内存高水位与磁盘告警线的行为描述,参考 RabbitMQ 官方公开文档的技术说明;具体阈值默认值因版本不同而异,以所用版本官方文档为准。文中出现的吞吐、延迟、容量估算均为通用工程估算口径,用于说明计算方法,非实测压测数据,实际数值需按业务消息大小、处理耗时与硬件条件自行实测校准。涉及服务器选型与资源价格的说明,具体以签约时最新报价与合同为准。相关产品与服务信息可参见 https://www.idc10000.net/ 。

RabbitMQ 这篇手册里,一万网络给出的落地结论

按架构取向给结论:以在线业务为主、要求低延迟和高吞吐的队列,走 classic 队列 + 内存优先 + 按算式算出的适中 prefetch,把 unacked 当作一级指标盯着,这条路线的性价比最高;以离线汇聚、允许分钟级延迟、常态积压在百万条级的队列,从建队列那天起就用惰性模式,别等内存打爆了再切;涉及资金、库存、订单这类丢不起的消息,直接上仲裁队列并接受它更低的吞吐上限,同时把磁盘 fsync 延迟作为硬指标写进选型要求。三种路线可以共存于同一集群,按队列逐条定,不要全集群一刀切。

第二层结论在水位上:内存高水位与磁盘告警线是消息中间件最容易被忽略的两个开关,绝大多数"加消费者反而更慢"的事故都发生在这两个开关被触发之后。上线前必须重设磁盘告警线(默认偏低,按写入速率 × 响应时间 × 2 计算),并在水位之下配置预警告警。第三层结论在确认机制上:manual ack 是默认选择,失败消息走"reject + 死信 + 分级延迟重试",绝不用 nack + requeue 处理;业务侧幂等是必须配套的工程项,不是可选项。

消息中间件服务器与备机租用咨询:一万网络能提供的支持

一万网络深耕 19 年(成立于 2007 年),提供中国香港及华南、华东、华北等多节点的服务器租用与备机方案,BGP 多线与 CN2 GIA 回国线路可选,自营机柜最快 1 分钟上架,配备 7×24 中文工单、平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘快照(每日 3 份、30 秒回滚)、5–20G 免费 DDoS 防护与免费备案协助。

消息中间件类负载的选型重点是内存容量、磁盘 fsync 稳定性与双向网络吞吐,工程师可协助按常驻消息量、unacked 占用与副本数反推内存与磁盘规格,并给出高水位与磁盘告警线的配置建议。具体机型、带宽与价格需实时询价,以官网实时报价与合同为准。


上一篇:K8s 集群才二十几个节点,apiserver 却开始间歇性超时:etcd 的 fsync、压缩与 db 配额这三件事查过没有

下一篇:同一张卡并发从 4 提到 32,吞吐没翻倍延迟先翻了几倍:连续批处理与 KV cache 这笔显存账怎么算