告警触发,你的发布者戛然而止。flow control介入,消息入队速度远超消费速度,值班人员被叫醒。在你开始重启节点、祈祷好运之前,这里有一套结构化的方法,帮你找出内存告警的真实原因,并在不把事情搞得更糟的前提下修复它。
内存告警的真正含义
RabbitMQ设置了一个高水位阈值——vm_memory_high_watermark——作为系统总内存的比例。默认值为0.4,即一旦Erlang内存分配器报告使用量超过可用内存的40%,RabbitMQ就会阻塞所有发布连接。
这不是崩溃,而是一种有意为之的背压机制。broker依然存活,consumer仍可继续消费消息,但发布者会收到resource-alarm的credit-flow阻塞,直到内存降至阈值以下。问题在于,“降至阈值以下”往往不会自然发生,因为根本原因仍在持续运行。
两秒内即可确认告警是否活跃:
rabbitmqctl status | grep -A5 "alarms"
或通过HTTP API查询:
curl -s -u guest:guest http://localhost:15672/api/nodes | \
jq '.[].mem_alarm'
如果返回true,告警正在触发。现在来找出原因。
如何检查当前内存使用情况
通过rabbitmqctl
rabbitmqctl status | grep -A10 "memory"
输出按类别分解内存:connection_readers、connection_writers、queue_procs、plugins、binary、code、atom等。binary和queue_procs这两个数字通常是我首先关注的——这里的峰值直接指向大消息或臃肿的queue。
通过Management API
curl -s -u guest:guest http://localhost:15672/api/nodes/<node-name> | \
jq '{mem_used: .mem_used, mem_limit: .mem_limit, mem_alarm: .mem_alarm}'
mem_used与mem_limit的比值告诉你当前距离临界点有多近。如果mem_used已达到mem_limit的95%,你正在走钢丝——一旦负载恢复,告警随时会再次触发。
省去反复SSH的麻烦。 Qarote的节点内存面板可跨cluster中每个节点实时显示这一比值,包括告警状态,并支持在
mem_alarm翻转为true之前设置阈值告警。查看内存仪表盘 →
水位线本身
rabbitmqctl environment | grep vm_memory_high_watermark
记录当前值。如果曾在某次故障中手动调高过且从未回滚,这是你需要了解的背景信息。
六大最常见根因
1. 大消息在内存中积压
RabbitMQ将消息体存储在二进制堆中。当单条消息较大——比如payload超过几百KB——且未被消费而持续堆积时,binary内存段会迅速膨胀。
检查每个queue的平均消息大小:
curl -s -u guest:guest http://localhost:15672/api/queues | \
jq '.[] | {name: .name, messages: .messages, message_bytes_ram: .message_bytes_ram}'
如果某个存在consumer滞后问题的queue上message_bytes_ram非常高,那你已经找到了罪魁祸首。修复方法是加速消费——扩容consumer、修复导致它们变慢的问题——从长远看,对接收大payload的queue强制执行max-length-bytes策略。
rabbitmqctl set_policy max-size "^your-queue-name$" \
'{"max-length-bytes": 52428800}' --apply-to queues
2. 未配置Lazy Queue
Classic queue默认将消息保存在内存中,只有在内存压力下才会分页到磁盘。如果未启用lazy模式,即使是中等大小的消息积压也会迅速耗尽RAM。
检查哪些queue不是lazy模式:
curl -s -u guest:guest http://localhost:15672/api/queues | \
jq '.[] | select(.arguments["x-queue-mode"] != "lazy") | .name'
在RabbitMQ 3.12+中,等效方案是x-queue-type: quorum——quorum queue默认将消息存储在磁盘上,如果你已完成迁移,这基本上不再是问题。对于仍在生产中运行的classic queue,启用lazy模式:
rabbitmqctl set_policy lazy-all ".*" '{"queue-mode":"lazy"}' \
--apply-to queues --priority 0
或在声明时通过x-queue-mode: lazy参数设置。
3. 未确认消息持续堆积
consumer拉取消息但从不ack,这些消息会被无限期地保留在RAM中。这是最难察觉的根因之一,因为queue深度看起来正常,但messages_unacknowledged计数在后台悄悄增长。
curl -s -u guest:guest http://localhost:15672/api/queues | \
jq '.[] | {name: .name, unacked: .messages_unacknowledged}' | \
jq 'select(.unacked > 0)'
unacked数量高且投递速率不变,通常意味着consumer陷入循环、在ack前抛出异常,或者带着已取出的消息直接崩溃了。修复consumer代码路径,然后设置prefetch count来限制单个consumer同时持有的消息数量:
# In your consumer configuration (AMQP 0-9-1)
channel.basic_qos(prefetch_count=50)
将低prefetch与consumer_timeout(RabbitMQ 3.8.15+)结合使用,可自动nack持有时间过长的消息:
# rabbitmq.conf
consumer_timeout = 1800000
4. 连接和Channel过多
每个AMQP连接和channel都会为其自身的进程、缓冲区以及reader/writer消耗内存。应用程序泄漏连接——只开不关——会在数小时内悄无声息地吃掉大量RAM。
rabbitmqctl list_connections name client_properties state memory | \
sort -k4 -n -r | head -20
关注单个连接内存异常高的情况,或连接总数远超应用拓扑预期的情况。Channel泄漏甚至更为常见:
rabbitmqctl list_channels connection channel_max messages_unacknowledged | \
sort -k3 -n -r | head -20
修复在应用层进行——确保连接和channel在finally块中或通过上下文管理器显式关闭。在broker侧,设置连接数限制作为临时保护:
# rabbitmq.conf
connection_max = 1024
5. Plugin内存开销
Management plugin将历史统计数据缓存在ETS表中。如果你在繁忙的cluster上以高粒度存储统计数据,这个缓存可能增长到数GB。
检查Management plugin消耗了多少内存:
rabbitmqctl status | grep -A3 "mgmt_db"
在rabbitmq.conf中减小统计数据保留窗口:
management.rates_mode = basic
management.sample_retention_policies.global.minute = 5
management.sample_retention_policies.global.hour = 60
management.sample_retention_policies.global.day = 1200
修改后重启management plugin:
rabbitmq-plugins disable rabbitmq_management
rabbitmq-plugins enable rabbitmq_management
6. 水位线对当前硬件设置过低
有时真正的问题是:水位线在多年前被保守地设置,而现在broker运行在内存远更充裕的机器上。0.4的水位线在4 GB虚拟机上给你1.6 GB的余量;在128 GB裸金属节点上,同样的比例意味着告警在51 GB时触发,而系统还有77 GB空闲。
检查当前实际限制:
rabbitmqctl status | grep mem_limit
如果限制相对于可用RAM显得过低,调高水位线:
# Live change — takes effect immediately, no restart needed
rabbitmqctl set_vm_memory_high_watermark 0.6
在rabbitmq.conf中持久化以在重启后保留:
vm_memory_high_watermark.relative = 0.6
除非经过负载测试,否则不要将此值推过0.7——为操作系统和Erlang自身留出太少余量,是把内存告警变成OOM kill的最快途径。
如何防止告警再次触发
内存告警是一个滞后指标。当它触发时,你已经处于flow control状态,SLA面临风险,能采取的措施只剩被动应对。应该关注的前置指标包括:
- 每个queue的
messages_unacknowledged——在达到预期吞吐量的10–20%时告警 - 每个queue的
message_bytes_ram——超过你定义的单queue预算时告警 - 节点
mem_used / mem_limit比值——在70%时告警,而不是等到100%时告警触发 - 连接数漂移——如果总连接数较基线增长超过20%则告警
所有这些指标都可以从Management API获取,或通过rabbitmq_prometheus plugin接入Prometheus:
rabbitmq-plugins enable rabbitmq_prometheus
# Scrape endpoint: http://localhost:15692/metrics
除工具层面外,结构性预防措施包括:
- 将classic queue迁移至quorum queue——后者默认分页到磁盘
- 对所有可能接收突发流量的queue设置
x-max-length或x-max-length-bytes策略 - 在每个consumer中强制执行prefetch限制
- 每月审计连接和channel——连接泄漏是缓慢而隐蔽的
- 对拥有数百个以上queue的cluster,检查Management plugin的数据保留设置
如果你希望在告警触发前而非触发后收到通知,设置能真正提供提前预警的RabbitMQ告警。
无需Prometheus技术栈。 Qarote内置内存水位线告警规则,无需配置采集器。了解告警功能 →
tl;dr: 当Erlang内存超过vm_memory_high_watermark(默认0.4×RAM)时,内存告警触发。运行rabbitmqctl status | grep -A10 memory,并检查每个queue的messages_unacknowledged和message_bytes_ram来定位罪魁祸首。最常见的原因包括:未确认消息堆积(用prefetch限制修复)、classic queue未启用lazy模式、连接/channel泄漏、Management plugin统计缓存,以及水位线对实际硬件设置过低。修复根本原因——在不解决泄漏的情况下调高水位线,只是推迟下一次告警的到来。