跳至主要内容

RabbitMQ 3.13:经典队列变更

·阅读时长 11 分钟

我们已经在单独的博文中宣布了 3.13 的两个主要新功能

这篇文章重点介绍了本版本中对经典队列的更改

  • 经典队列存储格式版本 1 已弃用
  • 经典队列消息存储的新实现

经典队列存储入门

在我们深入讨论变更之前,有必要简要解释经典队列如何存储消息。对于每条消息,我们需要存储消息有效负载和有关消息的一些元数据(例如,此消息是否已传递给消费者)。将消息数据(不透明的二进制 blob,可能具有相当大的尺寸)与元数据(小的键值映射)分开是有意义的。但是,对于小型消息,执行两次独立的写入,一次用于元数据,另一次用于消息内容,是一种浪费。因此,经典队列对小型消息的处理方式与大型消息不同。

从历史上看,在我们现在称为经典队列版本 1 的内容中,此过程称为将消息嵌入到索引中,属性 queue_index_embed_msgs_below 控制着构成足够小的消息以嵌入的内容(默认值为 4kB)。大于此阈值的消息将分别存储在消息存储中 - 一个具有不同磁盘表示形式的单独键值结构。对于存储在存储中的消息,索引包含元数据和消息存储 ID,这允许在需要时检索有效负载。每个虚拟主机只有一个消息存储,而每个队列都有一个单独的索引。

经典队列存储的版本 2(在 3.10 中引入)在逻辑上非常相似:仍然存在相同的主机消息存储,以及用于元数据和小消息的单独的每队列消息存储。但是,我们存储的每个队列的结构完全不同,因此我们不再称其为索引 - 小型消息不会嵌入到索引中,而是存储在每个队列消息存储中的单独文件中。

每个主机消息存储仍然存在,用于存储较大的消息,但版本 3.13 显著改变了其行为。

为了向后兼容,queue_index_embed_msgs_below 仍然控制消息是否足够大以存储在每个主机消息存储中,默认值仍然是 4kB。

值得一提的是,每个主机消息存储极大地改善了对扇出场景的支持(当单条消息传递到多个经典队列时),因为 RabbitMQ 只需写入一次消息,而不是每次队列写入一次。

经典队列版本 2 (CQv2)

几年前,我们开始着手重新实现经典队列,以提高性能。自从最初的实现(距今已有近 20 年)以来,发生了很多变化!以下是关于这段旅程中步骤的概述

  1. 从 3.10 开始,具有 queue-version=2 的队列使用新的索引存储格式(我们以不同的方式存储每个队列数据)
  2. 从 3.12 开始,经典队列(v1 和 v2)永远不会在内存中存储超过一小部分消息
  3. 从 3.12 开始,小于 queue_index_embed_msgs_below(默认值为 4kb)的消息处理效率更高

随着 3.13 的发布,我们正接近这段旅程的终点

  1. 从 3.13 开始,大于 queue_index_embed_msgs_below 的消息处理效率更高
  2. 从 3.13 开始,经典队列 v1 已弃用

在 RabbitMQ 4.0 中,我们将移除经典队列的镜像功能。正如我们之前多次所说,如果您需要高度可用的复制队列,则应使用自 3.8 起可用的仲裁队列。移除镜像功能将使对实现进行进一步优化成为可能。

此外,在 4.0 中,我们很可能会移除队列索引的 v1 实现(这可能会根据您的反馈推迟!)。当您将来升级到 4.0 时,所有仍在使用 v1 的经典队列都需要在启动期间转换为 v2。如果消息很多,或者队列很多,这可能需要很长时间。因此,最好有计划地进行转换过程。

如果我不准备使用 CQv2 怎么办?

在索引实现的版本 1 移除之前,您仍然可以使用它。

对于消息存储实现,没有这种选择 - 3.13 包含重大改进,尤其是在与 v2 索引结合使用时。但是,与 v1 结合使用时,也可能会出现一些小问题。建议用户彻底测试其应用程序,并报告 v2 索引比 v1 性能更差的情况。

CQv1 -> CQv2 转换

由于 v1 和 v2 使用不同的文件格式,因此如果队列从 v1 更改为 v2(反之亦然 - 降级受支持),则需要进行转换。如果您有一个现有的经典队列 v1,并应用一个具有 x-queue-version=2 的策略,该队列将在转换期间不可用 - 队列需要有一段时间才能将文件重写为新的格式。这种转换不应该花费超过几秒钟的时间 - 如果您发现转换时间更长,请报告此问题。

由于队列版本可以通过策略更改,因此也可以逐步从 v1 迁移到 v2。您可以声明一个只匹配队列子集的策略,一旦这些队列完成转换,您就可以扩展正则表达式以匹配更多队列,或者声明另一个匹配队列不同子集的策略。即使策略匹配了许多队列,迁移也严格是按队列进行的操作 - 完成转换的任何队列在转换完成后,都将立即可用于为客户端应用程序提供服务,即使其他队列仍在重写其文件。

您已经在 3.12(甚至 3.10 或 3.11)上进行了此转换。如果您这样做,4.0 中移除 v1 不会真正影响您,因为您的所有队列已经是 v2 了。

性能比较

让我们看看 RabbitMQ 3.12.11 与 3.13.0-rc.4 的比较结果。请参考以前的博文,了解有关基准测试设置和我们运行这些测试的方式的详细信息,或者查看存储库,我们在这里保留了环境配置脚本,其中包含工作负载。

所有测试均使用 100B 和 5kB 的消息进行。

发布和消费

在此测试中,我们有一个发布者和一个消费者,并尝试尽快通过单个队列传递消息。

如您所见,经典队列 v2 与 CQv1 相比,性能明显更好,3.13 提高了这两个版本的性能。对于仍然使用 CQv1 的用户,在 3.13 上迁移到 CQv2 可能会使小型消息的吞吐量增加近一倍!

1 publisher, 1 queue, 1 consumer, 100B messages
1 个发布者、1 个队列、1 个消费者、100B 消息

1 publisher, 1 queue, 1 consumer, 5kB messages
1 个发布者、1 个队列、1 个消费者、5kB 消息

仅发布

在此测试中,我们使用 2 个发布者以全速向队列发布消息,而根本不消费消息。队列从空增长到 500 万条消息。

对于 100B 消息,CQv2 以 3.12 相比超过 250% 的更高吞吐量,远远超过 CQv1。

2 publishers, 1 queue, no consumers, 100B messages
2 个发布者、1 个队列、没有消费者、100B 消息

5kB 测试更为细致。3.13 与 CQv2 的组合以较大优势获胜,即使在 3.12 中,CQv2 的优越性也很明显。但是,新的消息存储与旧索引的组合表现不一致 - 它大部分时间具有良好的吞吐量,但存在显著的减速(延迟峰值)。这很不理想,但我们决定保持这种方式,因为触发此行为所需的因素很多,而且用户应该无论如何迁移到 CQv2。我们只在此测试中观察到这种行为,因此需要满足以下条件:3.13 节点运行一个大于 4kb(或 queue_index_embed_msgs_below 的值)的消息的 CQv1 队列,发布者明显快于消费者(或没有消费者),并且消息吞吐量很高。如果您有这种工作负载,迁移到 CQv2 不仅可以防止出现这种回归,而且可以为您提供 CQv1 永远无法达到的更高性能。

2 publishers, 1 queue, no consumers, 5kB messages
2 个发布者、1 个队列、没有消费者、5kB 消息

仅消费

在此测试中,我们从前一个测试中的长积压中消费消息。有 500 万条消息要消费(希望您的队列短得多!)。

对于 100B 消息,您可以看到 CQv2 在早期提供了约 30% 的更高消费速率。随着时间的推移,当队列变短时,CQv1 会变得更快,但 CQv2 环境仍然在 CQv1 之前清空队列(当消费速率降至零时,表示队列已清空)

1 publisher, 1 queue, 1 consumer, 100B messages
1 个发布者、1 个队列、1 个消费者、100B 消息

对于 5kB 消息(由每个主机消息存储处理),您可以看到 3.13 变更的主要好处。由于消息存储实现由 v1 和 v2 共享,因此在此测试中,两个 3.13 环境都明显领先于 3.12,甚至领先于使用 CQv2 的 3.12。我们可以在 3.13 中看到约 50% 的更高吞吐量

1 publisher, 1 queue, 1 consumer, 5kB messages
1 个发布者、1 个队列、1 个消费者、5kB 消息

多个队列

在此测试中,我们没有将单个队列推到其极限,而是有 5 个并发消息流:5 个发布者,每个发布者发布到不同的队列,以及 5 个消费者,每个队列一个消费者。每个发布者每秒发送 10000 条消息,因此预期总吞吐量为 50000 条消息。对于 100B 消息,所有环境都达到了预期吞吐量,而对于 5kB 消息,所有环境都在约 27000 消息/秒之间波动。这里更有趣的部分是端到端延迟 - 从消息发送的那一刻到消息被消费,需要多长时间。

对于 100B 消息,我们可以看到 CQv2 环境可以更快地传递消息。对于从 3.12 上的 CQv1 迁移到 3.13 上的 CQv2 的用户来说,这意味着平均延迟降低了 75% **并且**内存使用量降低了 50%。

5 publishers, 5 queue, 5 consumers, 100B messages, 50k msgs/s achieved
5 个发布者、5 个队列、5 个消费者、100B 消息、达到 50k 消息/秒

对于 5kB 消息,结果非常接近,实际上,3.12 在此特定测试中获胜(我们可能在未来对此进行调查)。但是,3.13 仍然可以实现类似的结果,同时使用少 100MB 的内存。

5 publishers, 5 queue, 5 consumers, 5kB messages, 50k msgs/s attempted
5 个发布者,5 个队列,5 个消费者,5kB 消息,每秒尝试发送 50k 条消息

发布者确认延迟

最后,让我们看一下一个完全不同的测试。与其用消息淹没队列,我们一次只发布一条消息,等待发布者确认,然后发布下一条消息(消费者存在,但在这里并不重要,因为它可以轻松地消费传入的消息)。

使用 100B 消息,我们可以再次看到 CQv2 的速度有多快,与 3.12 上的 CQv1 相比,速度提高了 200% 以上。

1 publishers, 1 queue, 1 consumer, 100B messages
1 个发布者,1 个队列,1 个消费者,100B 消息

使用 5kB 消息,3.13 中新的每虚拟主机消息存储实现的优势显而易见,性能提高了 350% 以上。

1 publisher, 1 queue, 1 consumer, 5kB messages
1 个发布者、1 个队列、1 个消费者、5kB 消息

你可能会注意到,使用 5kB 消息的吞吐量实际上比使用 100B 消息的吞吐量高一些。这似乎违反直觉,但实际上并不奇怪。5kB 仍然是通过网络发送的少量数据,而消息存储被设计成更好地处理此类消息。与其他一切一样,随着未来进一步的优化,以及我们可能会考虑对 `queue_index_embed_msgs_below` 的默认值进行更改,这种差异可能会改变。

注意事项

索引的版本 2 和新的消息存储实现都应该为大多数用户带来显著的益处。但是,这些实现已经关注非镜像用例,而新的消息存储实现是针对索引的版本 2 设计的。虽然它向后兼容,可以与经典队列 v1 一起使用,但在某些情况下,将 v1 的索引与新的消息存储一起使用可能会导致性能比过去更差或性能配置文件不同,如仅发布测试中所示。因此,强烈建议您

  1. 使用 3.13 测试和基准测试您的应用程序
  2. 比较 v1 和 v2 的性能
  3. 如果 v1 在**没有镜像的情况下**表现更好,请报告这种情况,以便我们进行调查。

保留 v1 可以作为在性能更好的情况下使用它的解决方法。但是,这种旧实现将在未来被移除,因此你不能长期依赖这种解决方法。请报告此类情况,以便你将来可以升级到 v2。

最后的话

重新设计和替换如此广泛使用的软件的核心组件是一项非常艰巨的任务。特别感谢Loïc Hoguin承担了这个项目,翻阅代码,有时可以追溯到 RabbitMQ 的第一个版本,那时全世界还没有听说过 iPhone。与往常一样,我们欢迎测试和反馈,我们希望此次升级将为您带来与上述类似的益处。

© 2024 RabbitMQ. All rights reserved.