ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

RabbitMQ性能压测与调优实战:吞吐量、延迟全解析

RabbitMQ性能压测与调优实战:吞吐量、延迟全解析

1. 测试目标与整体方案设计

先聊聊我为什么要写这个主题。之前负责一个数据中台项目,每天要支撑千万级埋点数据的接入,RabbitMQ 承担着数据缓冲和削峰填谷的职责。开发阶段一切正常,但到了联调阶段,随着模拟流量从千级涨到万级,消息堆积、消费延迟、连接被断开等问题一个接一个冒出来。当时就觉得必须做一次系统性的性能测试与评估,搞清楚这套 MQ 集群的真实吞吐上限在哪、延迟表现如何、哪些参数配置在拖后腿。基于当时的实战经历,我整理了一份个人的压测记录,包活测试方案设计、工具选型、参数调优、问题排查的全过程。

要设计一个靠谱的 RabbitMQ 性能测试方案,第一件事不是急着搭环境,而是想清楚“测什么、为什么测”。因为性能测试没有统一答案,同样的集群配置,业务场景不同,结论可能完全不一样。

1.1 大数据场景对消息队列的真实需求

大数据场景下,RabbitMQ 扮演的角色和普通业务系统里不太一样。普通业务系统可能只是把 MQ 当作异步解耦的工具,消息量不大,但对可靠性要求高。而在大数据场景中,消息队列通常是整个数据链路的最前端,所有采集服务、日志代理、业务系统的数据都要先汇入 MQ,再由下游的消费服务拉取处理后写入数据仓库或湖泊。

这个位置的特性决定了几件事:

  • 流量具有明显的突发性。比如运营活动上线瞬间,埋点流量可能是平时的几十倍,消息队列必须能在短时间内扛住突发流量,处理好削峰。
  • 数据量级大。千万级甚至亿级消息的日吞吐是常态,消息体通常也不小,JSON 序列化后的数据可能达到几 KB 甚至几十 KB。
  • 消费速度可能跟不上生产速度。下游分析任务、ETL 任务的处理耗时远高于消息投递耗时,所以 MQ 要有足够的能力缓冲堆积,不能因为堆积导致性能崩盘。
  • 可靠性要求分场景。有些数据丢一条问题不大,有些核心业务数据要求消息必须持久化、不能丢失。

所以性能测试的目标不能只盯着单个数字,而是要看多种场景下的综合表现。我在项目里总结了一套测试维度,后面展开细说。

1.2 性能测试指标选取:不只是吞吐量

很多人一提 RabbitMQ 性能就只问“能扛多少 QPS”。但做过真实压测的人都知道,吞吐量只是最表面的指标。真正要关注的是这么几个维度:

指标含义大数据场景的关注点
吞吐量单位时间内成功投递/消费的消息条数能否支撑业务高峰时段的峰值流量
端到端延迟消息从发送到被消费的耗时实时分析场景非常敏感,分钟级延迟都会导致数据失真
堆积量队列中未被消费的消息条数消费速度跟不上时,堆积是正常现象,但堆积不能无限增长
消费积压恢复时间堆积发生后,恢复正常水位所需的时间削峰填谷场景下的关键指标
连接数/信道数客户端与 MQ 建立的 TCP 连接及信道数量大量采集服务接入时的稳定性基础

表格说起来简单,实际操作中要合理取舍。比如端到端延迟,如果测试时生产和消费同时进行,延迟数据会因为网络波动、垃圾回收抖动而出现大量的离群值,不能只看平均值,要看 P99 甚至 P999。如果测试时只生产不消费,那么测的就是纯生产端的吞吐能力,这个数字又会虚高。所以每一项指标都要明确测试场景和控制条件,不然跑出来的数据没法用。

1.3 为什么要采用压测工具加集群监控的联动方案

我见过不少人做压测,只跑压测工具,那边 TPS 数据出来了就收工。这样的结果只有一个数字,根本无法定位瓶颈在哪。

真正有效的做法是压测工具和集群监控联动。压测期间同步采集 MQ 节点的系统指标——CPU、内存、磁盘 IO、网络带宽、文件句柄数、Erlang 虚拟机的内存分配与垃圾回收频次,以及 RabbitMQ 自身的队列状态、channel 数量、connection 数量。这样当吞吐量上不去的时候,可以立刻判断是交换机处理能力到瓶颈了、还是磁盘写不下持久化消息、或者是内存触发 flow 控制导致生产被阻塞。

这次压测我就用了一套组合:PerfTest 作为主压测工具,配合 Prometheus + Grafana 监控 RabbitMQ 节点指标。步骤就是一边逐步加压,一边盯监控看哪个环节先出现拐点。这样测出来的结果才具备调优指导价值。

2. 环境配置与关键参数解析

测试环境搭建属于那种“看似简单、实则处处是坑”的环节。很多人在小机器上随便跑一下就开始下结论,这是最容易被挑战的测试结果——因为环境本身的瓶颈掩盖了 MQ 的真实性能。

2.1 单机还是集群:环境搭建的取舍

做性能测试首先要决策用单机还是集群。这个问题没有绝对答案,取决于压测目的。

如果目标是评估 RabbitMQ 本身的软件性能上限,在物理资源充足的情况下单机就能说明问题,集群的数据反而会因为网络开销等因素干扰判断。我当时的目标是为生产集群做容量校验,所以选择了三节点集群的部署方式,和线上环境保持一致。

集群环境要注意几个点:

  • 三台物理机配置需要一致,包括 CPU 核数、内存大小、磁盘类型。混配会引入“短板效应”,压测结果非常难看。
  • 磁盘优先选择 NVMe 固态硬盘。RabbitMQ 的持久化消息要写磁盘,HDD 在持久化场景下直接就是瓶颈。如果机器上还是机械盘,建议直接换机型再测。
  • 网络建议万兆内网。虽然 RabbitMQ 本体对带宽要求不算极端,但大数据场景下消息体通常较大,带宽会先被打满。
  • 内存不是越大越好,但肯定不能小。我当时用的机器是 16 核 64G 内存,RabbitMQ 单节点默认内存阈值是总内存的 40%,即约 25G 可用作消息缓冲区,对压测来说足够富裕了。

2.2 关键性能参数的设定依据

RabbitMQ 有大量参数会影响性能表现,有些是配置文件里的,有些是策略设置里的。这里列几个我在实测中验证过的关键点:

内存水位阈值(vm_memory_high_watermark)

默认值是 0.4,即内存使用达到物理内存 40% 时触发 flow control,生产端会被被降速甚至阻塞。看起来像是保护机制,但对性能测试影响很大。我在测试时把它调整为 0.5,加了一点缓冲空间。不过即使调高阈值,单条消息积压在内存中占用空间仍然受 queue 模式的限制,这个后面细说。

持久化还是非持久化

RabbitMQ 的消息有两层持久化概念:交换机/队列声明时是否 durable,以及消息投递时是否 persistent。两者都开启才会真正落盘。做性能测试必须分开测,因为持久化和非持久化的吞吐量差异可能有三到四倍。很多业务场景其实不需要每条消息都落盘,如果数据允许小概率丢失,性能收益非常可观。

确认模式(Publisher Confirm)

生产中大部分人会用 confirm 模式确保消息不丢。但 confirm 是同步等待 broker 确认的,开启后会明显拉低吞吐量。测试时建议分别跑“普通模式”和“confirm 模式”两组数据,这样能明确业务在可靠性上的取舍成本。

Persistent Queue 与 Lazy Queue

RabbitMQ 3.6 之前,持久化队列默认将消息驻留内存,达到阈值后换出到磁盘,以一个 chunk 为单位进行批量写入,旧版本消息写盘路径在大量小消息场景下会频繁触发 GC 和锁竞争。Lazy Queue 则是一边入队一边写磁盘,不大量驻留内存。之后 3.12 版本里的新队列类型全面采用了类似 lazy 的落盘策略。如果消息体较大且堆积是常态,用新队列类型对性能稳定性的提升是很明显的。

Prefetch 消费预取值

消费端一次批量拉取的消息条数。默认用 0 是无限拉取,可能造成某个消费者节点消息大量堆积、其他节点空闲的不均衡。我一般设为 100 到 300 之间。这个参数对消费吞吐和延迟的影响非常显著,值得专门压几组数据对比。

2.3 监控体系搭建:压测不能闭着眼跑

压测过程中,如果不开监控,出现问题你根本不知道瓶颈在哪。比如吞吐量上不去,可能是磁盘写满,也可能是内存 GC 频繁,还可能是 Erlang 虚拟机调度瓶颈,看着同一个现象,原因完全不同。

我搭监控时重点采集了以下几类指标:

  • RabbitMQ 自带指标:队列消息速率(publish/s, deliver/s)、ready/unacknowledged 消息数、connection 和 channel 数量、alarm 状态(memory_alarm、disk_free_alarm)。
  • 节点系统指标:CPU 使用率、内存使用率、磁盘写入带宽和 IO 延迟、网络流量、TCP 重传率。
  • Erlang VM 指标:内存各分配器占用,尤其是 binary heap;GC 次数与耗时;调度器利用率。

这里我想专门提醒一个很多人没有注意到的问题:RabbitMQ 的 Erlang 虚拟机默认只使用一半的 CPU 核数。如果机器是 16 核,RabbitMQ 默认只调度 8 个核。压测时 CPU 使用率显示 50% 就上不去了,不是性能不行,而是调度器数量没改。这个参数在 rabbitmq-env.conf 里通过 RABBITMQ_SCHEDULER_BIND_TYPE 和 ERL_ASYNC_THREADS 相关配置调整。我当时换了 32 核机器跑了压测,CPU 却一直卡在 50% 利用率,排查了很久才发现是这个原因。

3. 压测执行与分析口径

环境准备好了,接下来就是开始跑压测。这里我建议不要一上来就跑极限压力,而是采用“阶梯加压、多场景覆盖”的方式,逐步摸清系统的真实水位。

3.1 用官方 PerfTest 快速搭建压测

RabbitMQ 官方提供了 PerfTest 工具,用 Java 写成,专门用于性能测试。它基于 RabbitMQ Java Client,支持自定义消息体大小、发送速率、消费者数量、确认模式等参数,非常适合快速搭建压测场景。

基础用法非常简单。比如向名为 perf-test-queue 的队列连续发送小消息:

java -jar rabbitmq-perf-test.jar \ --uri amqp://testuser:password@node1:5672 \ --queue perf-test-queue \ --producers 4 \ --consumers 4 \ --size 1024 \ --time 300

参数含义:4 个生产者、4 个消费者、每条消息 1KB、持续 300 秒。默认会输出每 5 秒的吞吐和延迟统计。

实际压测时我不会只用这种单一参数压到底。我一般会先跑小消息(256 字节)看 broker 的处理极限,再跑大数据包(10KB 以上)看带宽和磁盘影响,再测发布确认开启时的损耗。不同测试组合的数据才有对比意义。

有几个参数需要特别注意:

  • -rate可以限制发送速率,做恒定压力场景时非常有用。
  • -multi可以指定多个队列,模拟真实业务中多个队列并存的场景。
  • -id是每次测试的任务标识,方便从监控系统里关联数据。
  • -format可以定制输出格式,如果你想做长期的定时压测、自动记录结果,输出 JSON 格式最方便。

还有一个比较实用的方式,是直接用 PerfTest 自带的参数生成阶段性的报告。比如--metrics-format=json配合--metrics-interval=10,每 10 秒输出一份统计。这个能力在做长时间稳定性测试时非常好用,把数据灌到日志系统里可以观察到延迟随时间的变化趋势。

3.2 多轮测试与数据评估口径

单跑一轮数据是没有参考价值的,原因是性能测试受 GC 抖动、虚拟机调度影响会上下波动。正确的做法是执行多轮测试,每轮之间留出冷却时间,再做数据汇总。

我在项目里设计了一套测试矩阵,简单复制这个思路也可以:

轮次测试场景生产者数消费者数消息体大小开启确认
1小消息极限吞吐88256B否
2小消息可靠性模式88256B是
3大消息吞吐8810KB否
4无消费者堆积场景1601KB否
5消费积压恢复场景1641KB是

每组跑 3 次,取中位数而不是平均值。平均值容易被极端值带偏,中位数更能反映真实能力。每组测试之间间隔至少一分钟,让系统把残留消息消费完、内存回收掉,避免上一轮数据影响下一轮。

分析时按照下面的口径处理:

  • 吞吐量取稳定阶段的中位数,去掉前 30 秒的爬坡数据。
  • 延迟分布看 P50、P99、P999 三个分位,尤其关注 P999,它往往揭示了系统在 GC 暂停或磁盘抖动时的表现。
  • 如果延迟曲线出现周期性尖峰,大概率是 GC 周期导致,要结合监控确认。
  • 如果吞吐量随客户端数量增加呈线性增长,说明服务端还没到瓶颈,可以继续加压。

3.3 结果分析:如何判断瓶颈在客户端还是服务端

很多人在压测结束后盯着 RabbitMQ 监控面板看数据,却不知道先看哪个指标。这里我提供一个简单有效的判断思路:

服务端节点 CPU 长时间超过 70%、消息速率曲线进入平台期、内存持续在高位,基本可以判定瓶颈在 broker 本身。但如果 CPU 使用率不高、内存也不紧张,吞吐就是上不去,就要怀疑是不是客户端压力没给够,可以把生产者数量翻倍再测一次。

另一个可能会被忽略的瓶颈是单条连接上的 channel 数量。RabbitMQ 的并发能力不以连接数为单位,而是以 channel 为单位。一个 TCP 连接上可以复用大量 channel。但如果确实是为每个线程建立独立连接,连接本身也会成为瓶颈。客户端连接数不是越多越好,高并发场景下用连接池复用最合适。

大数据场景还有一种特殊瓶颈,就是消息体过大导致网络成为瓶颈。我测过 100KB 消息体,发现 CPU、内存、磁盘都远未到瓶颈,但网络带宽已经被打满了。这个时候再用高性能机器也没用,只能从业务侧做压缩或拆分消息。

4. 实际问题排查与实测心得

压测过程中遇到的问题几乎都是“数据挂在纸面上根本发现不了、一跑就露出马脚”的类型。这类问题做性能测试的价值恰恰在于提前发现。下面列出几个我在实测中踩到的典型坑。

4.1 消息持久化对吞吐量的影响实测

非持久化场景下,单节点吞吐量轻松到 20,000 条/秒以上,但开启消息持久化之后,指标瞬间下滑。这个现象在预期之内,实际数据却超出了我的预期——下滑幅度接近 70%。

原因是持久化模式下每条消息不仅要写入内存,还要通过磁盘 IO 写入持久化日志,并等待刷盘确认。在这个过程里,磁盘写入延迟决定了确认响应时间。NVMe 固态硬盘实测还能到 4,000 条/秒,如果换成 SATA 固态,数字会再掉 30% 左右。

给业务侧的建议很简单:根据数据容忍度决定是否开启持久化。日志数据允许分钟级恢复性丢失,没必要开启。交易记录类数据则必须开启,没有妥协空间。

顺带提一个实践技巧:RabbitMQ 的持久化是批量刷盘的,不是逐条写。如果测试时发现吞吐量随着并发升高反而下降,大概率是客户端确认超时导致消息重发,进而产生了大量重复写盘操作。检查一下确认超时时间设置,不要设置得过短。

4.2 镜像队列在高并发下的坑

这个坑让我印象非常深刻。测试过程中发现集群整体吞吐量比单机还低,不管怎么调参数都上不去。最终定位到结构性问题:为了高可用,所有队列都配置了镜像模式,消息被同步复制到所有节点,主节点和镜像节点之间的网络同步开销彻底拖垮了并发性能。

RabbitMQ 镜像队列的模式在普通场景下问题不明显,但在高吞吐场景下,性能损耗相当大——三个节点的镜像集群测出来的最大吞吐甚至不如单节点模式。因为每条消息要等镜像确认后才算成功,等于把网络往返时间变成了关键路径上的固定开销。

在新版本和 quorum queue 的模式下,由于基于 Raft 的日志复制机制,每条消息都需多数派节点落盘确认,性能开销同样不可低估,但也换来了更一致的强同步保证。大数据量且不需要高可靠的场景,建议使用普通队列参考经典的镜像方案淘汰掉。如果你的业务必须保证高可用,那就要接受性能折损,在大数据量接入的场景独立拆分集群,把高可用队列和普通队列分开部署。

4.3 消费端拖尾延迟的排查过程

有个测试场景让我花了好几天排查:端到端延迟 90% 都稳定在 10 毫秒左右,但 P999 却高达 500 毫秒。这不是正常波动,因为正常波动不会差出两个数量级。

当时的第一反应是看 GC 日志,果然发现 Erlang 虚拟机里的 binary 内存分配频繁触发 major GC。进一步追踪发现队列的消息体全是大对象,它们直接占据了二进制堆空间,导致 GC 频繁。即便将队列切换为在新版本下采用磁盘优先的持久策略,大消息在生产和消费两侧的序列化/反序列化开销仍会带来瞬时 CPU 毛刺,反映在 P999 上就是数百毫秒的尖峰。

后面针对这个问题的优化思路是:

  • 在客户端层面将大 JSON 消息体压缩后再投递,实测压缩后吞吐量提升了 2 倍。
  • 调整消费者线程模型,避免同步阻塞读取,改为异步批量拉取。
  • 增加 Prefetch,减少消费端的确认交互频率。

压测过程中发现的大对象问题带出了一个整体结论:大数据场景下把体积做小带来的性能收益往往比调优 RabbitMQ 本身更显著。如果消息体已经是压缩过的,才值得花精力优化 broker 参数。

4.4 连接风暴与 channel 泄漏问题

测试时顺手压了一个特殊场景——模拟大量客户端同时接入,单机 5000 个连接同时建立。结果服务端直接进入异常状态,大量连接被拒绝,CPU 满负荷运转,Erlang 虚拟机几乎像卡死了一样。

排查后确认瓶颈有两个:一是 Erlang 默认的连接数上限,二是 RabbitMQ 对大量并发连接建立时的握手处理能力。这个问题在生产环境也有真实案例,应用重启时所有服务实例同时重连 MQ,造成连接风暴,Mq 集群瞬间被打挂。

这个教训很有价值。生产环境一定要为客户端增加随机退避重连策略,重连间隔至少加上随机抖动,不要让所有客户端在同一时间点发起连接。同时 RabbitMQ 侧也建议设置合理的连接上限,超出后直接拒绝新连接,而不是硬扛到崩溃。

5. 结合业务场景的性能调优建议

压测做完之后,最终要回答的问题是:生产集群到底该怎么调,才能扛住业务高峰?基于测试数据和踩坑经验,我梳理了几条可落地的调优建议。

优先评估消息体积优化

这个建议是压测带给我最深的感悟:调 MQ 参数带来的收益,往往远不如把单个消息体缩小见效快。一条 10KB 消息和 1KB 消息,在相同 TPS 下对网络、内存、磁盘的压力差距是 10 倍。做数据接入时尽量把消息精简成最小必要字段,大字段另存对象存储后只传递引用。

合理设计队列参数

队列声明时的参数会决定运行期行为,运行期不好改。x-max-length 限制队列最大长度,超限后的处理策略可以配置为拒绝新消息或者丢弃头部消息;x-message-ttl 可以设置消息过期时间,防止堆积的消息永不过期占满内存。不过大数据场景下使用 TTL 一定要慎重,消息过期被丢弃等于数据丢失,业务能不能接受要提前评估清楚。

结合场景选择交换机和路由方式

大数据场景强烈建议使用 direct 或 topic 交换机做精确路由,尽量减少使用 fanout 广播。广播会把每条消息复制到所有绑定队列,消费者如果不需要全量广播,这个操作就是纯浪费。

consumer 端配置合理 Prefetch

我在生产环境的一般建议是 100-300。如果消费逻辑本身耗时较长,Prefetch 太大可能导致单节点任务严重积压,消息确认超时后被重新投递,造成大量重复处理。如果消费逻辑很快且是纯 CPU 操作,Prefetch 可以适当调大以提升吞吐。

集群规模与业务流量匹配

结合压测数据评估集群容量时,不要按平均流量配,要按峰值流量多预留 30% 的缓冲。如果峰值流量远超过单集群承载能力,优先考虑拆分集群按业务域隔离,而不是无限堆节点。RabbitMQ 集群的节点数不是越多越好,Erlang 的集群通信会随着节点增加变得复杂,网络抖动时副本同步很容易把集群拖入不稳定状态。

监控告警策略

压测结束后,告警阈值可以参照实测数据来定。比如实测内存水位在高峰时会达到物理内存的 50%,那么线上告警阈值设置在 60% 比较合理,既不会频繁误报,又能在异常时及时预警。磁盘告警可以设置为磁盘剩余空间低于 1GB 时立刻报警。连接数告警按节点承载上限的 70% 设置。这样线上出问题时能第一时间响应,不用等用户报障。

最后再分享一个实际操作层面的建议:性能测试不是一次性的动作,而应该形成一套可重复执行的回归流程。每当 RabbitMQ 版本升级、服务器配置变更、业务消息模型调整时,都应该跑一遍基础压测,对比历史数据看是否有明显变化。我在项目里就是把压测脚本和监控模板做成了自动化任务,每次变更后自动触发,对比结果直接推送到团队群。这个习惯帮我们避免了好几次因为配置变更导致的线上性能劣化,省了很多半夜救火的麻烦。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进