RabbitMQ 3.13:经典队列变更
我们已经在单独的博文中宣布了 3.13 的两个主要新功能
本文将重点介绍此版本中对经典队列的更改
- 经典队列存储格式 1.0 版本已弃用
- 经典队列消息存储的新实现
经典队列存储入门
在深入了解具体变更之前,有必要简要说明一下经典队列是如何存储消息的。对于每条消息,我们需要存储其有效载荷以及有关该消息的一些元数据(例如,该消息是否已投递给消费者)。将消息数据(一个不透明的二进制大对象,可能相当大)与元数据(小的键值映射)分开存储是合理的。然而,对于小消息而言,执行两次单独的写入操作(一次存元数据,一次存内容)是浪费的。因此,经典队列处理小消息的方式与大消息不同。
在历史上,即我们现在所说的经典队列版本 1 中,此过程被称为将消息嵌入到索引中,属性 queue_index_embed_msgs_below 控制什么大小的消息可以被视为“足够小”而进行嵌入(默认值为 4kB)。超过此阈值的消息会分开存储在消息存储(message store)中——这是一种具有不同磁盘表示形式的独立键值结构。对于存储在消息存储中的消息,索引仅包含元数据和消息存储 ID,以便在需要时检索有效载荷。每个虚拟主机(vhost)有一个消息存储,而每个队列都有一个独立的索引。
3.10 版本引入的经典队列存储版本 2,在逻辑上非常相似:仍然存在相同的每个虚拟主机共享的消息存储,以及用于元数据和小消息的每个队列独立的存储。然而,我们每个队列所存储的数据结构已完全不同,因此我们不再仅仅将其称为索引——小消息不再嵌入到索引中,而是存储在每个队列的消息存储内的独立文件中。
每个虚拟主机的消息存储仍然用于大消息,但 3.13 版本显著改变了其行为。
出于向后兼容性考虑,queue_index_embed_msgs_below 仍然控制消息是否足够大以至于需要存储在每个虚拟主机的消息存储中,默认值仍为 4kB。
经典队列版本 2 (CQv2)
几年前,我们开始了一项旨在提升性能的经典队列重新实现之旅。自最初的实现(距今已有近 20 年历史)以来,很多事情都发生了变化!以下是这段旅程中各阶段的简要回顾:
- 自 3.10 版本起,使用
queue-version=2的队列采用了新的索引存储格式(我们以不同方式存储每个队列的数据)。 - 自 3.12 版本起,经典队列(v1 和 v2)永远不会在内存中存储超过一小部分的消息。
- 自 3.12 版本起,低于
queue_index_embed_msgs_below(默认 4kB)的消息得到了更高效的处理。
随着 3.13 的发布,我们已接近这段旅程的终点。
- 从 3.13 版本开始,超过
queue_index_embed_msgs_below的消息得到了更高效的处理。 - 从 3.13 版本开始,经典队列 v1 已被弃用。
在 RabbitMQ 4.0 中,我们将移除经典队列的镜像功能。正如我们之前多次提到的,如果您需要高可用性复制队列,应该使用自 3.8 版本起就已提供的仲裁队列(Quorum Queues)。移除镜像功能将使实现方案能够进行进一步的优化。
此外,在 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 的消息进行。
发布与消费
在此测试中,我们有一个单独的发布者和一个单独的消费者,并尝试通过单个队列尽可能快地投递消息。
正如您所见,与 CQv1 相比,经典队列 v2 提供了明显更好的性能,且 3.13 版本提升了两个版本的性能。对于仍在使用 CQv1 的用户,在 3.13 上迁移到 CQv2 可能会使小消息的吞吐量翻倍!


仅发布
在此测试中,我们以全速向队列发布消息(2 个发布者),同时完全不消费这些消息。队列从空增加到 500 万条消息。
对于 100B 消息,CQv2 的吞吐量比 3.12 高出 250% 以上,远超 CQv1。

5kB 的测试则更为微妙。3.13 配合 CQv2 以较大优势胜出,且 CQv2 的优越性在 3.12 中也显而易见。然而,新消息存储与旧索引的组合表现并不稳定——它在大部分时间里拥有良好的吞吐量,但会出现明显的减速(延迟尖峰)。这很不幸,但考虑到触发此行为的因素较多,且用户终究应该转向 CQv2,我们决定维持现状。我们仅在此测试中观察到了此行为,因此需要满足以下条件:运行 CQv1 队列且消息大于 4kB(或 queue_index_embed_msgs_below 的值)的 3.13 节点、发布者速度明显快于消费者(或根本没有消费者)、高吞吐量。如果您有此类工作负载,迁移到 CQv2 不仅应能防止此回归,还能获得比 CQv1 所能达到的高得多的性能。

仅消费
在此测试中,我们消费上一测试留下的海量积压消息。共有 500 万条消息需要消费(希望您的队列不会积压这么多!)。
对于 100B 消息,您可以看到 CQv2 在初期提供了约 30% 更高的消费率。随着时间的推移,队列变短,CQv1 的速度会加快,但 CQv2 环境清空队列的速度仍远早于 CQv1(当消费率为零时,表示队列已空)。

对于由每个虚拟主机消息存储处理的 5kB 消息,您可以看到 3.13 变更的主要益处。由于消息存储实现由 v1 和 v2 共享,在此测试中,两个 3.13 环境都明显领先于 3.12,即便是运行 CQv2 的 3.12 也不例外。在 3.13 中,我们看到了约 50% 的吞吐量提升。

多个队列
在此测试中,我们没有将单个队列推向极限,而是设置了 5 个并发消息流:5 个发布者,每个发布者向不同的队列发布消息,以及 5 个消费者,每个队列对应一个消费者。每个发布者每秒发送 10000 条消息,因此预期的总吞吐量为 50000 条消息/秒。对于 100B 消息,所有环境都达到了预期吞吐量;而对于 5kB 消息,所有环境都在 27000 条消息/秒左右波动。这里更有趣的部分是端到端延迟——从消息发送到消息被消费需要多长时间。
对于 100B 消息,我们可以看到 CQv2 环境投递消息的速度要快得多。对于从 3.12 上的 CQv1 迁移到 3.13 上的 CQv2 的用户,这代表着平均延迟降低了 75%,同时内存使用量降低了 50%。

对于 5kB 消息,结果非常接近,实际上 3.12 在此项特定测试中胜出(这是我们未来可能会研究的内容)。然而,3.13 仍然能够实现类似的结果,同时少使用 100MB 内存。

发布者确认延迟
最后,让我们看看一个非常不同的测试。我们不向队列发送海量消息,而是一次只发布一条消息,等待发布者确认,然后再发布下一条(虽然有消费者存在,但在这里并不重要,因为它可以轻松消费进入的消息)。
对于 100B 消息,我们可以再次看到 CQv2 的速度有多快,与 3.12 上的 CQv1 相比,速度提升了 200% 以上。

对于 5kB 消息,3.13 中新的每个虚拟主机消息存储实现的优势显而易见,改进幅度超过 350%。

您可能会注意到,5kB 消息的吞吐量实际上比 100B 消息略高。这虽然违反直觉,但其实并不奇怪。5kB 仍然是网络上传输的极少量数据,而消息存储旨在更好地处理此类消息。与其他事物一样,这种差异未来可能会随着进一步优化而改变,或许我们还会考虑更改 queue_index_embed_msgs_below 的默认值。
注意事项
索引版本 2 和新的消息存储实现都应该为大多数用户提供显著的好处。然而,这些实现目前侧重于非镜像的使用场景,且新的消息存储实现是针对 v2 索引设计的。虽然它向后兼容,可以与经典队列 v1 一起使用,但在某些情况下,将 v1 索引与新的消息存储一起使用可能会提供比过去更差的性能或不同的性能表现(正如在“仅发布”测试中所见)。因此,强烈建议:
- 使用 3.13 测试并对您的应用程序进行基准测试。
- 比较 v1 和 v2 的性能。
- 如果 v1 在没有镜像的情况下表现更好,请报告该情况,以便我们查看。
保留在 v1 上可能是性能表现更好的情况下的变通方案。然而,旧的实现将来会被移除,所以您不能长期依赖该方案。请报告此类情况,以便您将来能够升级到 v2。
结语
重新设计和替换广泛使用的软件中的核心组件是一项非常艰巨的任务。特别感谢 Loïc Hoguin 承担了这一项目,他深入挖掘了代码库,有时甚至追溯到 RabbitMQ 的第一个发布版本——那还是在世界听到 iPhone 这个名字之前。一如既往,我们欢迎测试和反馈,希望此次升级能为您带来上述相似的收益。
