问题背景
订单系统与履约系统之间用 RabbitMQ 解耦,版本运行了三年,日均消息量百万级,单机 broker 带 6 个 vhost、40 多个队列,配置多年没动过。消费者的 prefetch 参数在接入时就没有设置——当时客户端采用的是默认行为,没人深究过这意味着什么;发布端用的是公司统一封装的 SDK,带 confirm 机制,业务侧对"发送成功率 100%“习以为常。
10 月的一个大促预热日,订单量较日常上涨三倍。上午 10 点开始,订单服务陆续出现"发布消息超时"告警;到 10 点半,订单创建接口大面积转圈,监控大盘上 RabbitMQ 的发布速率曲线几乎贴到零——整个发布链路卡死了。
故障现象
- 发布端症状统一:
publish调用阻塞后超时,SDK 日志报connection blocked,重连后短暂恢复又再次阻塞。 - RabbitMQ 管理页面几乎打不开,刷出来后看到所有 connection 状态标着 blocked;连接数正常、消费者在线数量正常。
- broker 系统日志出现
memory resource limit alarm set on...,随后是密集的告警反复触发记录。 - 消费者侧反而"看着正常”:消费速率没掉太多,只是比平时慢;broker 进程 CPU 不高,磁盘 IO 也不高——不是宕机,是"憋住了"。
- 重启 broker 能恢复几分钟,但流量一上来立刻复发,指标曲线像方波。
排查过程
第一步搞清 blocked 是什么机制。RabbitMQ 对发布端有一层叫 connection.blocked 的背压保护:broker 内存使用超过 vm_memory_high_watermark(默认系统内存的 40%)时,触发全局内存告警,所有发布连接被标记 blocked,客户端收到的 publish 会被挂起而不是丢弃——这是设计上的"停止进水"动作,防止 broker 被内存压垮。也就是说,故障现象里"发布卡死"不是 bug,而是告警机制在起作用;真正要查的是内存为什么涨到水位。
第二步看内存的去向。rabbitmqctl status 输出里 memory 段最显眼的是 allocated_unacked;list_queues name messages messages_unacknowledged 一对照,问题清楚了:40 多个队列里,几个核心队列的 messages 不多,但 messages_unacknowledged 高达数万。未确认消息是什么:消息已经从队列推给消费者、消费者还没 ack,在这段时间里消息不能删除,必须驻留 broker 内存(除非分页到磁盘)。几万条业务消息(单条几百 KB)在等待 ack 期间全量占着内存,把水位顶穿了。
第三步解释未确认消息为何膨胀。查消费者配置,发现消费端用的是客户端默认的 prefetch = 0(无限制)语义——对这批用多线程回调的消费者来说,broker 会一次性把队列里的消息尽可能多地推给消费者,上限由网络窗口和消费者本地缓冲决定,而不是一个受控的数字。平时消息速率低、消费跟得上,未确认窗口自然小;大促时消息速率涨三倍,消费者处理单条要打外部接口(平均 80ms),推得进、确认得慢,未确认数迅速堆到数万,内存水位随之上穿。
第四步看 broker 是否有别的内存缓解手段。RabbitMQ 在内存压力下会把 classic 队列的消息 paging 到磁盘,但分页是有限度的:只有部分消息能被换出,且换出速度远跟不上推送速度;order 系统这几个队列又是非 lazy 模式,消息默认驻留内存。加上发布端 confirm 窗口大(也是默认未收紧),大量"已发布未确认"的消息同样占内存。三个默认值叠加,把 broker 的内存安全垫吃干净了。
第五步确认磁盘水位排除:df -h 显示数据盘余量 60%,disk_free_limit 未触发;CPU、GC 均平稳——故障链完全收敛在内存水位这一个点上。
解决方案
- 止血:临时把
vm_memory_high_watermark从 0.4 调到 0.6(物理内存 16G,预留 6.4G 给系统与消费端),给消费进度争取时间;同时把非核心队列的消费者扩容一倍,加快未确认消息的消化。发布端恢复后观察两小时,blocked 未再出现。 - 治本:给全部消费者显式设置
basic.qosprefetch(核心队列 20、非核心队列 100),杜绝"一次性推满";对 order 相关的三个大流量队列开启 lazy 模式(消息直接落盘,内存只留索引),把内存占用从"与积压成正比"改为"常数级"。 - 收紧发布端:SDK 的 confirm 未确认窗口从默认值收敛到 1000,防止发布侧在途消息无限增长;发布超时告警阈值从 5s 调整到 2s,让阻塞更早被看见。
- 压测验证:按大促峰值的两倍流量做压力测试,验证 prefetch + lazy 组合下内存曲线平稳、无 blocked 触发,才允许上线。
根因分析
直接根因是消费者 prefetch 未限制,大流量下未确认消息在 broker 内存大量驻留,触发内存水位告警,RabbitMQ 按设计全局阻塞所有发布连接。深层根因有两个:其一,上线三年来 prefetch、lazy 模式、confirm 窗口三个关键的"内存安全参数"全部停留在客户端默认值,而这些默认值在低流量下看似无害、在高流量下必然出事;其二,监控只覆盖了"消费积压(messages)",没有覆盖"未确认(messages_unacknowledged)“和内存水位这两个先导指标,故障是在发布端已经卡死后才被发现,而不是在内存爬升阶段被预警。
预防措施
- 全部 MQ 消费者的 prefetch 参数纳入配置评审,禁止留空使用默认;大流量队列默认开启 lazy 模式;
- 监控面板增加三个先导指标:
messages_unacknowledged、vm_memory_used / vm_memory_limit比值、connection blocked 计数,任一项超阈值即告警; - 大促等流量高峰前执行 MQ 专项压测,验证内存水位曲线与 blocked 阈值余量;
- 把"内存安全参数"写进 MQ 接入规范:prefetch、lazy、confirm 窗口、TTL 四项为必填配置项。
总结
这次故障里 RabbitMQ 的行为全程正确甚至可以说"救了自己”:水位告警触发全局 blocked,宁可停掉发布也不让 broker OOM 崩溃——真正失守的是配置层的三个默认值和监控层的两个盲区。消息中间件的很多默认参数是为"开始用"设计的,不是为"用三年后的峰值"设计的;把默认值当作已配置项,迟早要在最不能出事的时刻补交学费。压测能提前花的钱,故障时都会十倍还回来。