RabbitMQ 3.12 性能改进
RabbitMQ 3.12 即将发布,其中包含许多新功能和改进。这篇博文重点介绍与性能相关的差异。最重要的变化是经典队列的 lazy
模式现在是标准行为(下面会详细介绍)。新的实现应该更节省内存,同时比早期版本的 lazy
或 non-lazy
实现提供更高的吞吐量和更低的延迟。
为了获得更好的性能,我们强烈建议切换到经典队列版本 2 (CQv2)。
概述
让我们快速浏览一下 RabbitMQ 3.12 中最重要的与性能相关的改进。
经典队列:惰性模式的更改
从 3.12 开始,x-queue-mode=lazy
参数将被忽略。所有经典队列现在的行为都类似于之前的惰性队列。也就是说,消息倾向于写入磁盘,只有一小部分保留在内存中。内存中消息的数量取决于消费速率。此更改会影响所有经典队列用户。根据我们的测试,对于绝大多数用户来说,新的实现应该会带来显着的性能提升,并降低内存使用率。请继续阅读一些基准测试结果,但也要确保在升级前使用 PerfTest 测试您的系统。
经典队列:大幅改进的经典队列 v2 (CQv2)
本段已更新以反映路线图的更改。经典队列版本 2 将成为 RabbitMQ 4.0 中的默认且唯一的选项(之前我们计划在 3.13 中将其设为默认选项)。
自 RabbitMQ 3.10 以来,我们有了经典队列的两种实现:原始版本 (CQv1) 和新版本 (CQv2)。它们之间的区别主要在于磁盘存储。
大多数用户仍然使用 CQv1,但从 3.12 开始,我们强烈建议切换到 CQv2,或至少评估一下。版本 2 将成为 RabbitMQ 4.0 中唯一可用的实现。
迁移过程很简单:在声明队列时,添加新的策略键或可选的队列参数 x-queue-version=2
。要全局切换到 CQv2,请在配置文件中将 classic_queue.default_version
设置为 2
classic_queue.default_version = 2
可以通过将版本设置回 1 来返回。每次值更改时,RabbitMQ 都会转换队列的磁盘表示形式。对于大多数空队列,该过程是瞬间完成的。对于合理的积压,可能需要几秒钟。
在许多用例中,切换到 CQv2 将带来 20-40% 的吞吐量提升,同时降低内存使用率。那些仍然使用经典队列镜像的用户(你不应该这样做,它很快将被删除!),需要更彻底地测试你的系统,因为在某些情况下,版本 1 在镜像经典队列中效果更好,但也存在许多版本 2 更好,尽管没有任何镜像特定代码优化的场景。
新的 MQTT 实现:每个连接显着节省内存,支持每个节点数百万个连接
MQTT 插件已完全重新设计,可提供更低的内存使用率、更低的延迟,并且可以处理比以前更多的连接。
我们已经发布了一篇关于 MQTT 相关改进的单独博文。
仲裁队列的重大改进
使仲裁队列更高效的工作仍在继续。RabbitMQ 3.12 带来了一些改进,因此所有仲裁队列用户都应该看到更好的性能。最大的变化将体现在具有长仲裁队列的环境中。以前,随着队列变长,其吞吐量会降低。这应该不再是一个问题。
节点应该更快地停止和启动
具有许多经典队列(数万个或更多)的 RabbitMQ 节点应该更快地停止和启动。这意味着在升级和其他维护操作期间,节点不可用时间更短。
基准测试设置
以下所有数据都比较了 3.11.7 和 3.12-rc.2。一些(主要是较小的)优化已反向移植到更新的 3.11 补丁版本中,这就是为什么此比较未使用最新的 3.11 补丁版本。
基准测试
在撰写本文时,RabbitMQ 团队的标准性能测试套件包含 14 个测试。每个测试运行 5 分钟。单独的环境同时运行相同的测试,但消息大小不同,以便可以轻松查看消息大小的影响,和/或比较相同工作负载下不同队列类型或版本。
以下是测试列表,按它们在 Grafana 仪表板中出现的顺序排列
- 一个发布者尽可能快地发布,而一个消费者尽可能快地消费
- 两个发布者,没有消费者(队列变长时的性能)
- 一个消费者消费来自上一个测试的长队列(消费者的性能不受发布者的影响)
- 五个队列,每个队列有 1 个发布者以 10k 消息/秒的速度发布,以及 1 个消费者(总预期吞吐量为 50k/秒,我们关注延迟)
- 扇出到 10 个队列 - 1 个发布者和 10 个消费者,一个扇出交换机
- 一个发布者,一个消费者,但只有 1 个未确认的消息(发布者在发送下一条消息之前等待上一条消息的确认)
- 扇入:7000 个发布者每秒发布 1 条消息,到一个队列
- 1000 个发布者发布 10 条消息/秒,每个发布到不同的队列;每个队列也有一个消费者(总预期吞吐量:10k/秒)
- 一个没有消费者的发布者创建消息积压,然后 10 个消费者加入以耗尽队列
- 与上一个类似,但 50 个消费者加入,但设置了 single-active-consumer(因此只有一个开始耗尽队列)
- 与第一个测试类似,但在消费者端使用了 1000 的多重确认
- 具有 TTL 的消息被发布并快速过期(它们永远不会被消费)
- 消息被发布以便稍后被否定确认(这只是下一个测试的设置)
- 来自上一个测试的消息被否定确认
最后几个测试不太有趣。引入它们是为了查找某些特定领域的问题。
环境
这些测试是使用以下环境进行的
- 一个 GKE 集群,带有 e2-standard-16 节点
- 使用我们的 Kubernetes Operator 部署的 RabbitMQ 集群,具有以下资源和配置
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
name: ...
spec:
replicas: 1 # or 3 for mirrored and quorum queues
image: rabbitmq:3.11.7-management # or rabbitmq:3.12.0-rc.2-management
resources:
requests:
cpu: 14
memory: 12Gi
limits:
cpu: 14
memory: 12Gi
persistence:
storageClassName: premium-rwo
storage: "150Gi"
rabbitmq:
advancedConfig: |
[
{rabbit, [
{credit_flow_default_credit,{0,0}}
]}
].
关于环境的一些注意事项
- 对于许多测试(甚至生产工作负载),这些资源设置都过高。这只是我们碰巧用于性能测试的配置,而不是建议
- 您应该能够使用更好的硬件达到更高的值。
e2-standard-16
远非可以购买/租用的最佳硬件 - 信用流被禁用,因为否则单个快速发布者将被节流(以防止过载并为其他发布者提供公平的机会);流量控制在具有许多用户/连接的系统中很重要,但在基准测试中通常不是我们想要的
测试结果
经典队列和经典队列 v2 的新的类似惰性的默认行为
RabbitMQ 的初始版本于 2007 年发布。那时,与任何其他操作相比,磁盘访问速度非常慢。但是,随着存储技术的多年发展,越来越不需要避免将数据写入磁盘。在 3.6.0 版本中,早在 2015 年,就添加了惰性队列作为一种选择。惰性队列将所有消息存储在磁盘上以节省内存,这对于可能变长的队列尤其重要。如今,将消息写入磁盘是一项非常廉价的操作(除非您执行 fsync
以保证高数据安全性,就像仲裁队列所做的那样)。通过将消息存储在磁盘上,我们可以使用更少的内存。这意味着更低的成本、更少的内存警报以及更少由集群中突然的内存峰值引起的麻烦。因此,我们已将此作为经典队列的唯一选择。
3.11 非惰性 vs 3.12
让我们首先看看当前使用非惰性经典队列版本 1 (CQv1) 的用户在升级后期望发生什么。屏幕截图取自 12 字节消息大小测试。

如您所见,3.12 在每个测试中都表现更好:更高的吞吐量、更低的延迟、更小的可变性(更少的速率峰值)。与此同时,3.12 具有更低的内存使用率(类似于惰性队列)。在最后一个面板中,您可以看到 3.11 内存在使用队列变长时会出现峰值,而 3.12 仅在涉及许多连接的测试中超过 1GB 内存使用率(是连接使用了内存,而不是队列)。
3.12 CQv1 vs CQv2
上面,我们看到大多数用户在升级到 RabbitMQ 3.12 后应该获得的一些好处,但这仅仅是个开始!让我们将经典队列版本 2 添加到比较中

使用 CQv2,我们观察到更高的吞吐量和更低的延迟。尤其是在队列变长的第二个测试中,版本 2 的延迟不超过 50 毫秒,而 3.11 可能会飙升到几秒钟。
请不要犹豫,尝试一下经典队列版本 2。您只需设置 x-queue-version=2
策略键即可。要全局切换到 CQv2,请在配置文件中将 classic_queue.default_version
设置为 2
classic_queue.default_version = 2
从 RabbitMQ 3.13 开始,版本 2 将成为默认版本。
经典镜像队列
如上所述,经典队列版本 1 包含一些专门实现的优化,以改善队列被镜像时的行为。当我们准备删除镜像功能时,版本 2 不再执行任何特殊的镜像技巧。因此,结果是好坏参半的,但版本 2 非常高效,即使没有特殊考虑,它在大多数情况下也能胜过版本 1,即使使用镜像也是如此。

您可以看到版本 2 是否更适合您,但更重要的是,请开始迁移到仲裁队列和流,越快越好。
仲裁队列
仲裁队列多年来提供了比队列镜像更好的性能和数据安全性,并且它们只会变得更好。
3.12 中最大的改进是仲裁队列如何处理长积压。

发布到长仲裁队列
在 RabbitMQ 3.12 之前,如果仲裁队列有很长的积压,发布延迟可能会显着增加,从而降低吞吐量。从 3.12 开始,队列的长度应该不再对延迟和吞吐量产生太大影响。
您可以在第二个测试中看到这一点:虽然两个版本都以大约 25k 消息/秒的速度开始,但 3.11 很快下降到 10k/秒左右。同时,3.12 保持在 20k/秒以上。
3.12 的性能不像我们希望的那样平稳,但已经好得多,并且可以进一步改进。同样重要的是要记住,在这些测试中,我们实际上是在过载队列:消息尽可能快地持续流入,因此任何垃圾回收或定期操作(例如 Raft 预写日志滚动)都将表现为延迟峰值。
消费吞吐量
从仲裁队列消费消息也比以前快得多。特别是,在我们的测试中,从非常长的队列消费消息的速度最多可以提高 10 倍。队列仍然应该是相对较短的(如果您需要存储大量消息,可以使用流),但通过这些改进,仲裁队列应该能够很好地应对各种消息积压。
看一下最后一个面板(每秒消费消息数)上的第三个测试。3.12 以超过 15k 消息/秒的速度开始,并且随着队列变短而变得更快。同时,3.11 几乎无法超过每秒 1000 条消息。在此测试中,我们正在消费一个积压了 500 万条消息的队列,因此您可能从未见过仲裁队列如此挣扎,但好消息是:即使在这些情况下,仲裁队列现在也应该表现得更好且更可预测。
更可能的情况是消费者在一段时间内不可用并且需要赶上的情况。让我们关注这些测试

在每个测试的第一个阶段,消费者关闭,并创建消息积压。然后消费者启动。在第一个测试中,有 10 个消费者,在第二个测试中,有 50 个消费者,但只有一个是活动的(作为单活动消费者)。在这两种情况下,3.12 都提供了显着更高的消费速率,并且队列耗尽得更快。在 3.11 的情况下,我们可以看到随着队列积压变短,它逐渐变得更快。
更快的节点重启
这不应该影响大多数用户,但对于具有许多经典队列的用户(例如,具有许多订阅的 MQTT 用户)来说,应该是非常好的消息。在我们的测试中,我们启动了一个节点,导入了 100,000 个经典队列版本 2,然后重启了该节点。3.12 在 3 分钟内启动并运行,而 3.11 需要 15 分钟才能再次为客户端提供服务。3.11 在启动时遇到了内存警报,这使得启动过程特别缓慢。有一个客户端正在运行,只是为了查看它何时失去连接并可以再次建立连接。

不再有许多空闲队列导致的周期性资源使用峰值
您可能已经在上面的屏幕截图中注意到,3.11 不仅重启时间更长,而且发布速率也呈峰值状,即使它是一个非常轻的工作负载(每秒仅 100 条消息)。这些峰值是由一个内部进程引起的,该进程检查队列的运行状况以防止发出陈旧的队列指标。在 3.12 之前,它会查询每个队列的状态以确定队列是否健康。但是,空闲的经典队列会休眠(它们的 Erlang 进程被停止,其内存被压缩),并且需要唤醒才能回复它们是健康的。从 3.12 开始,休眠的队列被认为是健康的,而无需唤醒它们,因此即使在具有许多队列的节点上,CPU 和内存使用率也应该更低且更稳定。
结论
RabbitMQ 3.12 应该可以提高几乎所有用户的性能,通常会显着提高。与往常一样,我们衷心建议您在升级之前测试候选版本和新版本。我们也一直有兴趣了解人们如何在 GitHub Discussions 和我们的 社区 Discord 服务器 中使用 RabbitMQ。
如果您可以分享有关您的工作负载的信息,最好是以 perf-test 命令的形式,这将有助于我们为您改进 RabbitMQ。