跳到主要内容

RabbitMQ 3.10 性能改进

·12 分钟阅读

RabbitMQ 3.10 于 2022 年 5 月 3 日发布,带来了许多新功能和改进。这篇博文概述了该版本中的性能改进。长话短说,您可以期待更高的吞吐量、更低的延迟和更快的节点启动速度,尤其是在启动时导入大型定义文件的情况下。

概述

首先,请查看3.10 版本概述博文,以了解该版本中新增功能的高级概述。在这里,我们将只关注性能改进和具有性能影响的功能。这里涵盖的一些改进已反向移植到 3.9.x,因此为了展示差异,我们将使用 3.9.0 作为参考点。

如果您还不能升级到 3.10,请确保您至少运行最新的 3.9.x 补丁版本,以利用这些优化。

RabbitMQ 3.9 vs 3.10

让我们在几种不同的场景中比较 RabbitMQ 3.9 和 3.10。请记住,这些是特定的基准测试,可能无法反映您的工作负载的性质和性能。

您可以使用 RabbitMQ 负载测试工具 perf-teststream-perf-test 在您自己的环境中运行这些或类似的测试。

环境

这些测试是使用以下环境执行的

apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
name: ...
spec:
replicas: 1 # or 3
image: rabbitmq:3.10.1-management # or rabbitmq:3.9.0-management
resources:
requests:
cpu: 8
memory: 16Gi
limits:
cpu: 8
memory: 16Gi
persistence:
storageClassName: premium-rwo
storage: "3000Gi"
rabbitmq:
advancedConfig: |
[
{rabbit, [
{credit_flow_default_credit,{0,0}}
]}
].

关于环境的一些说明

  1. 对于许多测试(甚至生产工作负载),这些资源设置都显得过剩。然而,这是我们团队进行 RabbitMQ 负载测试的标准配置。
  2. 您应该能够使用更好的硬件获得更高的值,包括在 Google Cloud 中
  3. 信用流被禁用,因为否则单个快速发布者将被节流(以防止过载并给其他发布者公平的机会),这在生产环境中是正确的做法,但在服务器负载测试中没有意义。

场景 1:一个队列,快速发布者和消费者

在第一个场景中,我们将仅使用 1 个队列,带有 2 个发布者和 2 个消费者。我们将测试消息大小分别为 10、100、1000 和 5000 字节的情况。

我们使用 2 个发布者,因为在某些配置中,尤其是对于非常小的消息,单个发布者无法充分利用队列。请注意,RabbitMQ 3.11(当前在 master 分支中)已经进行了一些路由效率改进,因此未来版本可能不会出现这种情况。

以下 perf-test 标志 用于此工作负载

# classic queues (with an exactly=3 mirroring policy where applicable)
perf-test --producers 2 --consumers 2 --confirm 3000 --multi-ack-every 3000 --qos 3000 \
--variable-size 10:900 --variable-size 100:900 --variable-size 1000:900 --variable-size 5000:900 \
--auto-delete false --flag persistent --queue cq

# quorum queues
perf-test --producers 2 --consumers 2 --confirm 3000 --multi-ack-every 3000 --qos 3000 \
--variable-size 10:900 --variable-size 100:900 --variable-size 1000:900 --variable-size 5000:900 \
--quorum-queue --queue qq

Scenario 1: 1 queue, 2 publishers and 2 consumers; message size of 10, 100, 1000 and 5000 bytes (15 minutes each)
场景 1:1 个队列,2 个发布者和 2 个消费者;消息大小为 10、100、1000 和 5000 字节(每个 15 分钟)

观察结果

  • 仲裁队列的吞吐量比经典镜像队列 (CMQ) 高出数倍
  • 在某些场景中,3.10 中的仲裁队列甚至可以实现高达 50% 的吞吐量提升
  • 在某些场景中,经典队列 v2 已经比 v1 更好
  • CMQ 没有获得任何新的改进,将在 RabbitMQ 4.0 中移除;请迁移到仲裁队列、或在适当情况下使用非镜像经典队列

场景 2:一个队列,10000 msg/s

在之前的场景中,RabbitMQ 中的某些代码路径始终以或接近其最大速度运行。这次,我们将设置 10000 msg/s 的固定目标吞吐量,并比较随着消息大小随时间增加,不同的环境是否可以维持此工作负载。

由于预期吞吐量是已知的,我们将专注于测量延迟及其可变性。

以下 perf-test 标志用于此场景

# classic queues (with an exactly=3 mirroring policy where applicable)
perf-test --rate 10000 --confirm 3000 --multi-ack-every 3000 --qos 3000 \
--variable-size 10:900 --variable-size 100:900 --variable-size 1000:900 --variable-size 5000:900 \
--auto-delete false --flag persistent --queue cq

# quorum queues
perf-test --rate 10000 --confirm 3000 --multi-ack-every 3000 --qos 3000 \
--variable-size 10:900 --variable-size 100:900 --variable-size 1000:900 --variable-size 5000:900 \
--quorum-queue --queue qq

仲裁队列再次自信地击败了经典镜像队列 (CMQ)

Scenario 2: 1 queue, 1 publisher and 1 consumer; message size of 10, 100, 1000 and 5000 bytes (15 minutes each)
场景 2:1 个队列,1 个发布者和 1 个消费者;消息大小为 10、100、1000 和 5000 字节(每个 15 分钟)

让我们放大非镜像经典队列,以比较 v1 和 v2 消息存储和队列索引实现。我们可以看到 CQv2 提供了更低且更一致的延迟

Scenario 2: 1 queue, 1 publisher and 1 consumer; message size of 10, 100, 1000 and 5000 bytes (15 minutes each)
场景 2:1 个队列,1 个发布者和 1 个消费者;消息大小为 10、100、1000 和 5000 字节(每个 15 分钟)

单节点仲裁队列 3.9 和 3.10 在此测试中表现非常相似(请参见第一个图表上的图例)。让我们关注 3 节点集群

Scenario 2: 1 queue, 1 publisher and 1 consumer; message size of 10, 100, 1000 and 5000 bytes (15 minutes each)
场景 2:1 个队列,1 个发布者和 1 个消费者;消息大小为 10、100、1000 和 5000 字节(每个 15 分钟)

如您所见,3.10 版本中的仲裁队列提供了显着更低且更一致的延迟。仍然存在峰值,这是由于某些仲裁队列操作的批处理或周期性性质所致。这是未来版本需要改进的领域。

场景 3:500 个队列,总共 5000 msg/s

在此场景中,我们将有 500 个队列,每个队列都有 1 个发布者,每秒发布 10 条消息,以及一个消费者来消费这些消息。因此,预期的总吞吐量为每秒 5000 条消息。我们再次运行此场景一个小时,每 15 分钟更改一次消息大小(10、100、1000 和 5000 字节)。

# classic queues (with an exactly=3 mirroring policy where applicable)
perf-test --producers 500 --consumers 500 --publishing-interval 0.1 --confirm 10 --multi-ack-every 100 --qos 100 \
--variable-size 10:900 --variable-size 100:900 --variable-size 1000:900 --variable-size 5000:900 \
--queue-pattern cq-%d --queue-pattern-from 1 --queue-pattern-to 500 \
--auto-delete false --flag persistent

# quorum queues
perf-test --producers 500 --consumers 500 --publishing-interval 0.1 --confirm 10 --multi-ack-every 100 --qos 100 \
--variable-size 10:900 --variable-size 100:900 --variable-size 1000:900 --variable-size 5000:900 \
--quorum-queue --queue-pattern qq-%d --queue-pattern-from 1 --queue-pattern-to 500

Scenario 3: 500 queues, 1 publisher, 1 consumer, 10 messages per queue; message size of 10, 100, 1000 and 5000 bytes (15 minutes each)
场景 3:500 个队列,1 个发布者,1 个消费者,每个队列 10 条消息;消息大小为 10、100、1000 和 5000 字节(每个 15 分钟)

观察结果

  1. 只有 CMQ 难以维持预期的 5000 msg/s 吞吐量
  2. 经典队列 v2 在整个测试过程中具有最低且最一致的延迟
  3. 3.9.0 CMQ 环境的发布延迟非常高;我没有调查原因,只需使用仲裁队列或流!

由于经典队列,尤其是镜像的 3.9.0 环境,在图表中占据了主导地位,因此这是相同的图表,但重点关注经典队列 v2 和 3.10 仲裁队列

Scenario 3: 3.10 environments only
场景 3:仅限 3.10 环境

如上所述,仲裁队列的延迟不如我们希望的那样一致,但大多数情况下它们保持在 25 毫秒以内。这仍然是在 500 个队列的情况下,总共 5000 msg/s,消息大小为 10/100/1000 字节,对于 5000 字节的消息,延迟并没有高很多。

在 3 节点仲裁队列的情况下,这是一个退化(边缘情况)集群,所有队列领导者和所有连接都在单个节点上。这样做是为了使测试结果在运行之间以及单节点和 3 节点集群之间更具可比性。

场景 4:长仲裁队列

在 3.10 之前,当仲裁队列很长时,性能不佳 —— 为消费者检索最旧的消息是一项代价高昂的操作。在此场景中,我们将首先使用 2 个发布者发布 1000 万条消息,然后使用两个消费者消费所有这些消息。

# publish 10 milion messages
perf-test --producers 2 --consumers 0 --confirm 3000 --pmessages 5000000 \
--queue-args x-max-in-memory-length=0 --quorum-queue --queue qq

# consume 10 milion messages
perf-test --producers 0 --consumers 2 --multi-ack-every 3000 --qos 3000 --exit-when empty \
--queue-args x-max-in-memory-length=0 --quorum-queue --queue qq

请注意,从 3.10 开始,仲裁队列会忽略 x-max-in-memory-length 属性。它仍然可以通过策略配置,但不会产生任何影响 —— 队列的行为就像将其设置为 0 一样。

Scenario 4: 10 million messages published and then consumed
场景 4:发布 1000 万条消息,然后消费

观察结果

  1. 在 3.10.1 中,发布和消费消息所花费的时间大致相同(每个约 3 分钟)
  2. 3.9.0 发布消息需要两倍的时间(约 6 分钟)
  3. 单节点 3.9.1 需要将近 15 分钟才能清空队列,而 3 节点集群则需要额外 2 分钟
  4. 3.9 的两个实例在开始消费时约为 10000 msg/s,并随着时间的推移缓慢提高。当队列变短时,3 节点 3.9.0 集群的消费率在最后显着提高

值得注意的是 3.9 发布者图表中的两个下降点(橙色线)。集群达到了内存警报,因此发布者被暂时阻止。这种情况没有发生在 3.10 环境中,尽管 3.10 当时执行了更多工作(发布和消费速度更快)。

3.10 测试中的仲裁队列平均比经典队列使用更多的内存,因为它们将消息的元数据保存在内存中,但它们使用的内存比 3.9 中使用的要少。

这是对两个完成大部分工作的节点(托管所有队列领导者和所有连接)之间的直接比较

Scenario 4: 10 million messages published and then consumed, memory usage
场景 4:发布 1000 万条消息,然后消费,内存使用情况

更快的导入和声明

对于那些在启动时导入定义的用户,升级到 3.10 后,节点应该花费更少的时间来启动。导致这种情况的因素有很多变化和功能,预期的行为取决于您的定义以及您使用和将要使用/配置的功能。这是一个摘要

  1. 如果您使用 load_definitions 配置选项,并且在 JSON 文件中有许多定义,则节点应该能够更快地启动,而无需您执行任何操作。对于拥有数千个队列的用户来说,这可以为每个节点启动节省几分钟的时间。这里的主要区别在于,在 3.10 中,重新声明已存在的实体应该会快得多。集群中的节点通常共享相同的配置文件,因此每个节点都会尝试相同的导入,但除了第一个节点之外的所有节点都将有效地重新导入现有实体。在节点重启时,假设您不删除这些实体,则所有节点都可以更快地启动。

  2. 如果您设置了一个新属性 definitions.skip_if_unchanged = true,如果定义文件的校验和与上次导入时相同,RabbitMQ 将完全跳过导入。对于具有大型定义文件的集群,这可以为每个节点节省几分钟的时间。这与前一点类似,只是您需要选择加入(设置属性),并且速度提升甚至更高,因为不尝试导入显然比检查实体是否已存在更快。

其他改进

Erlang 25

此版本支持 Erlang 25,它引入了许多编译器和运行时效率改进。这在 64 位 ARM CPU 上最为明显,因为 Erlang 25 中的 JIT 现在支持该架构。

启动时定义导入

在节点在启动时导入定义的集群中,集群中的每个节点实际上都会导入相同的定义,因为所有节点都使用相同或几乎相同的配置文件。

这通常会导致以下两个问题之一,具体取决于事件的确切时间

  • 如果节点逐个启动,则所有队列通常最终都会位于单个节点上,因为在导入时集群中只有一个节点
  • 如果节点并行启动,则当多个节点尝试声明相同的定义时,会发生大量争用

在 RabbitMQ 3.10 中,许多重新导入优化通常有助于解决第二个问题。

此外,cluster_formation.target_cluster_size_hint 是一个新的设置,现在可以设置它来告诉 RabbitMQ 预期在集群完全形成后集群中将有多少个节点。

有了这个额外的信息,只有最后一个加入集群的节点才会导入定义。主要好处是仲裁队列应该在节点之间得到很好的平衡(受领导者放置设置的约束)。在过去,如果导入在第一个节点启动后立即进行,则其他节点实际上将以空状态启动,因为所有队列在它们启动时已经运行。

结论

RabbitMQ 3.10 中发布了许多改进,其中一些改进也反向移植到了 3.9 的最新补丁版本中。

我们一直在寻找使事情变得更快的方法。但是,RabbitMQ 可以通过许多不同的方式进行配置和使用,并且许多改进都是特定于工作负载的。我们非常感谢您的帮助 —— 如果您希望 RabbitMQ 在特定场景中更快,请联系我们并告诉我们您的工作负载。理想情况下,如果您可以使用 perf-test 重现该问题,我们将很乐意看看我们可以做些什么来提高吞吐量、降低延迟或减少内存使用量。

© . All rights reserved.