从镜像经典队列迁移到仲裁队列
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
仲裁队列是经典镜像队列的优秀替代品,它在 RabbitMQ 3.8 版本中引入。您需要迁移的原因有两个互补。
首先,经典镜像队列已在 3.9 版本中弃用,于 2021 年 8 月 21 日正式发布了公告。它们将在 4.0 版本中完全移除。
而且它们更可靠、更可预测,对大多数工作负载更快,维护也更少——所以您不应该觉得自己被无缘无故地逼迫。
仲裁队列在所有方面都更好,但它们在功能上与镜像队列不是 100% 兼容的。因此,迁移看起来可能是一项艰巨的任务。
在对未来的性能改进进行一窥之后,本文概述了几种可能的迁移策略,并提供了如何处理不兼容功能的指南。此外,还有迁移您的 RabbitMQ 镜像经典队列到仲裁队列文档可帮助您完成迁移过程。
性能提升
在 RabbitMQ 3.10 性能提升博文中,我们已经详细讨论了仲裁队列的性能优势。
在下图中,您可以看到即将发布的 RabbitMQ 3.12 能够达到何种新的性能水平:

该图显示了使用 1kB 消息在不同工作负载下的吞吐量。数值越高越好,尽管在某些测试中最大吞吐量存在上限(在此类测试中,我们关注的是延迟和/或吞吐量是否稳定)。
颜色含义如下:
- 橙色 - 仲裁队列
- 绿色 - 镜像经典队列 v1(非懒加载)
- 黄色 - 镜像经典队列 v1(懒加载)
- 蓝色 - 镜像经典队列 v2
无需深入细节,我们可以看到在几乎所有情况下,仲裁队列都提供了显著更高的吞吐量。例如,第一个测试是单队列、单发布者、单消费者的测试。仲裁队列可以维持 30000 条消息/秒的吞吐量(同样使用 1kb 消息),同时提供高水平的数据安全性和向集群中所有 3 个节点复制数据的能力。相比之下,经典镜像队列的吞吐量仅为前者的三分之一,且提供的数据安全保证要低得多。在某些测试中,我们可以看到仲裁队列(橙色线)完全平稳,这意味着它们可以维持该工作负载并仍有余力(否则性能会开始波动),而镜像队列的吞吐量则较低且不太稳定。
敏锐的读者可能会注意到,在第二个测试中,仲裁队列最初提供非常高的发布者吞吐量,但很快就会下降。这是我们目前正在努力解决的问题,我们希望很快能对其进行改进。这仅仅是在没有消费者且队列迅速变得非常长(数百万条消息)时的极端情况。
兼容性考量
RabbitMQ 文档有一个关于 仲裁队列 的专用页面。特别是在本文档中,有一个 功能矩阵,列出了镜像经典队列与仲裁队列之间的所有区别。这些差异可能需要不同程度的工作量才能完成成功迁移。其中一些 变更 可能是微不足道的,而 另一些 则可能需要更改应用程序与 RabbitMQ 交互的方式。所有这些都在后文中进行了详细记录。
毋庸置疑,迁移后的应用程序应针对仲裁队列进行彻底测试,因为在负载和极端情况下,其行为可能会有所不同。
本文档介绍了两种迁移策略:
- 第一种策略涉及创建一个新的虚拟主机(vhost),并借助联合(federation)插件以最小的停机损失进行迁移。如果所有不兼容的功能都已清理或移至策略中,这也被视为顺利迁移的捷径——只需更改连接参数,现有代码即可同时适用于镜像队列和仲裁队列。
- 另一种策略以牺牲停机时间为代价重复使用同一个虚拟主机,并且需要能够停止给定队列的所有消费者和生产者。
通用要求
- 集群中至少应有 3 个节点——使用少于此数量的副本使用仲裁队列毫无意义。
- 管理插件(Management plugin)应至少在一个节点上运行——它用于导出/导入单个主机的定义,这可以极大地简化定义的清理工作。(
rabbitmqadmin命令行工具也在后台使用该插件)。 - 应启用 Shovel 插件。
查找正在使用的队列和功能
所有镜像经典队列在其有效策略定义中都有 ha-mode。应用该策略的策略可以通过以下脚本找到:
#!/bin/sh
printf "%s\t%s\t%s\t%s\t%s\t%s\n" vhost policy_name pattern apply_to definition priority
for vhost in $(rabbitmqctl -q list_vhosts | tail -n +2) ; do
rabbitmqctl -q list_policies -p "$vhost" |
grep 'ha-mode'
done
但直接列出运行系统中实际镜像的队列可能更容易。这样就不需要猜测 HA 策略是否真正应用了。
#!/bin/sh
printf "%s\t%s\t%s\n" vhost queue_name mirrors
for vhost in $(rabbitmqctl -q list_vhosts | tail -n +2) ; do
rabbitmqctl -q list_queues -p "$vhost" name durable policy effective_policy_definition arguments mirror_pids type |
sed -n '/\t\[[^\t]\+\tclassic$/{s/\t\[[^\t]\+\tclassic$//; p}' |
xargs -x -r -L1 -d '\n' printf "%s\t%s\n" "$vhost"
done
请注意,上述命令使用了 effective_policy_definition 参数,该参数仅在 3.10.13/3.11.5 及更高版本中可用。如果不可用,可以使用来自新版本 RabbitMQ 的 rabbitmqctl,或者手动将策略名称与其定义进行匹配。
重大变更
当使用以下一种或多种功能时,无法直接迁移到仲裁队列。需要更改应用程序与 Broker 的交互方式。
本节概述了如何查找运行系统中是否正在使用这些功能,以及为了简化迁移需要做出哪些更改。
优先级队列
经典镜像队列实际上在后台为每个优先级创建一个单独的队列。对于迁移,应用程序必须明确处理这些队列的创建,以及向其发布消息和从中消费消息。
可以通过上述队列列表输出中是否存在 x-max-priority 来检测此功能。可以在源代码中搜索完全相同的字符串。优先级队列不能通过策略创建,因此不涉及策略更改。
溢出死信处理
仲裁队列不支持溢出模式 reject-publish-dlx。代码需要更新为使用发布者确认(publisher confirms)并自行处理死信。
可以通过上述队列列表输出中是否存在 reject-publish-dlx 来检测此功能。可以在源代码中搜索完全相同的字符串。
消费者全局 QoS
仲裁队列不支持 消费者全局 QoS。需要做出决定,通过替代手段(例如使用较低的每消费者 QoS,在已知的应用程序负载模式下可以获得大致相同的结果)来实现必要的结果。
要检测是否使用了此功能,可以对运行中的系统执行以下命令并检查是否有非空输出:
rabbitmqctl list_channels pid name global_prefetch_count | sed -n '/\t0$/!p'
它将给出一组启用了全局 QoS 的通道 PID,然后可以将这些 PID 映射到队列名称,并检查它们是否为镜像队列。
rabbitmqctl list_consumers queue_name channel_pid
消费者的 x-cancel-on-ha-failover
当队列领导者发生故障转移时,镜像队列的消费者可以 自动取消。这可能会导致丢失哪些消息已发送给哪个消费者的信息,从而导致此类消息的重新传递。
仲裁队列受此类行为的影响较小——唯一可能发生这种情况的情况是整个节点宕机。对于其他领导者变更(例如由重新平衡引起的),不会发生重新传递。
而且,当消费者被取消或通道关闭时,正在传输的消息也可能发生重新传递。因此,无论是否明确要求此类信息,应用程序都需要为重新传递做好准备。
微小变更
这些功能在使用仲裁队列时不起作用。处理它们的最佳方法是完全从源代码中删除它们,或者将它们移至策略中。
懒加载队列
经典队列可以选择性地在 懒加载模式 下运行,但对于仲裁队列,这是唯一的运行方式。迁移时处理此问题的最佳方法是将 x-queue-mode 从源代码移至策略中。
非持久化队列
非持久化队列将在节点/集群启动时被删除。拥有镜像提供的额外持久性保证意义不大。
非持久化队列的概念也将在未来版本中消失:临时队列的唯一选择将是排他队列(exclusive queues)。这仅影响队列定义的持久性,消息仍然可以标记为瞬态。
对于此类队列,必须做出决定:此队列内容是否重要到需要仲裁队列的可用性保证,或者将其降级为经典(但持久化)队列是否更好。
排他队列
即使策略中包含镜像设置,排他队列也不会被镜像。尝试声明排他仲裁队列将导致错误。这显然属于不需要迁移的情况之一,但必须小心,避免使用明确的 x-queue-type: quorum 参数来声明排他队列。
按虚拟主机迁移队列
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
从经典镜像队列迁移到仲裁队列的过程类似于 蓝绿集群升级,不同之处在于迁移可以在同一 RabbitMQ 集群上的新虚拟主机上进行。然后使用 联合插件(Federation Plugin) 无缝地从旧主机迁移到新主机。
此迁移路径的一个重要方面是,可以为新的虚拟主机指定默认队列类型。将其设置为 quorum,使得所有未明确指定类型的队列在创建时都默认为仲裁队列(排他、非持久化或自动删除队列除外)。
如果源代码中清理了所有不兼容的功能(并且源代码中没有明确的 x-queue-type 参数),那么使用完全相同的代码既可以连接到带有经典镜像队列的旧虚拟主机,也可以连接到带有仲裁队列的新虚拟主机——只需要更改连接参数中的虚拟主机即可。
创建目标虚拟主机
需要特别注意确保使用正确的默认队列类型创建新的虚拟主机。通过管理 UI 添加新虚拟主机时,应从队列类型下拉菜单中进行选择。也可以使用 CLI 界面创建,指定默认队列类型并添加一些权限。
rabbitmqctl add_vhost NEW_VHOST --default-queue-type quorum
rabbitmqctl set_permissions -p NEW_VHOST USERNAME '.*' '.*' '.*'
创建联合上游
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
应为 NEW_VHOST 创建一个新的联合上游,其 URI 指向 OLD_VHOST:amqp:///OLD_VHOST。(注意默认虚拟主机的 URI 是 amqp:///%2f)。
上游可以通过管理 UI 或 CLI 创建。
rabbitmqctl set_parameter federation-upstream quorum-migration-upstream \
--vhost NEW_VHOST \
'{"uri":"amqp:///OLD_VHOST", "trust-user-id":true}'
当使用这种主机名为空的 URI 格式时,无需指定凭据,但连接仅限于单个集群内。
如果消息中的 user-id 用于任何目的,也可以如上面的 CLI 示例所示进行保留。
迁移定义
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
将源虚拟主机的定义导出到文件。这可以在管理 UI 的“概览”页面上完成(别忘了选择单个虚拟主机)。或者使用以下 CLI 命令:
rabbitmqadmin export -V OLD_VHOST OLD_VHOST.json
在将文件加载回 NEW_VHOST 之前,需要对其进行以下更改:
- 删除您希望在旧虚拟主机中作为经典队列而在新虚拟主机中作为仲裁队列的队列的
x-queue-type声明。 - 需要应用于队列定义的其他更改:
- 删除
x-max-priority参数。 - 当
x-overflow设置为reject-publish-dlx时,更改该参数。 - 删除
x-queue-mode参数。 - 将
durable属性更改为true。
- 删除
- 更改策略中的以下键:
- 删除所有以
ha-开头的键:ha-mode,ha-params,ha-sync-mode,ha-sync-batch-size,ha-promote-on-shutdown,ha-promote-on-failure。 - 删除
queue-mode。 - 当
overflow设置为reject-publish-dlx时,更改该值。
- 删除所有以
- 上一步后变为空的策略应被丢弃。
- 应将与旧虚拟主机的联合关系添加到所有剩余的策略中,指向之前创建的联合上游:
"federation-upstream-set":"quorum-migration-upstream"。 - 如果没有全匹配策略(应用于模式为
.*的队列),则需要创建一个并使其也指向联合上游。这确保了旧虚拟主机中的每个队列都将被联合。 - 在迁移期间,需要删除将联合规则应用于交换机的策略,以避免重复消息。
现在,可以通过 UI 或 CLI 工具将修改后的方案加载到新的虚拟主机中。
# Import definitions for a single virtual host using rabbitmqadmin.
# See https://rabbitmq.cn/docs/definitions to learn more.
rabbitmqadmin import -V NEW_VHOST NEW_VHOST.json
将消费者指向新虚拟主机
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
此时,应该只需更新连接参数即可将消费者指向新的虚拟主机。
将生产者指向新虚拟主机
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
生产者现在也可以指向新的虚拟主机。
停止消费者的时间点也是在旧虚拟主机中禁用联合交换机并在此新交换机中启用它的时间。
在足够的系统负载下,旧虚拟主机的消息将不会被获取。如果消息顺序很重要,那么这应该分步完成:停止生产者,将剩余消息 Shovel 到新虚拟主机,在新虚拟主机上启动消费者。
将剩余消息 Shovel 到新虚拟主机
对于旧主机中每个非空队列,都需要配置一个 shovel。
rabbitmqctl set_parameter shovel migrate-QUEUE_TO_MIGRATE \
'{"src-protocol": "amqp091", "src-uri": "amqp:///OLD_VHOST", "src-queue": "QUEUE_TO_MIGRATE",
"dest-protocol": "amqp091", "dest-uri": "amqp:///NEW_VHOST", "dest-queue": "QUEUE_TO_MIGRATE"}'
队列排空后,可以删除 shovel。
rabbitmqctl clear_parameter shovel migrate-QUEUE_TO_MIGRATE
确保未来的队列声明成功
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
许多应用程序在使用队列之前会在多个地方声明队列。在迁移离开经典镜像队列时,这提出了一个挑战:如果客户端在声明队列时没有显式提供队列类型,在 迁移定义 步骤之后,当重新声明现有队列时,所有未来的声明尝试都会遇到 PRECONDITION_FAILURE 通道错误。
为了避免这种情况,有三个选项:
- 将
x-queue-type声明添加回所有使用仲裁队列的客户端。 - 使用
default_queue_type在节点范围内设置默认队列类型,这是一个在 RabbitMQ3.13.3及更高版本中提供的rabbitmq.conf设置。 - 设置
quorum_queue.property_equivalence.relaxed_checks_on_redeclaration = true,这是一个自 RabbitMQ3.11.16起提供的rabbitmq.conf设置。
第三个选项(将 quorum_queue.property_equivalence.relaxed_checks_on_redeclaration 设置为 true)可以在迁移过程中的任何时间采用。
就地迁移
在此版本的过程中,我们以停机时间为代价换取在现有虚拟主机和集群中执行迁移的能力。
对于正在迁移的每个队列(或某组队列),在迁移期间应能够停止所有的消费者和生产者。
准备生产者和消费者
应清理所有不兼容的功能。此外,在声明队列的每个地方,最好使 x-queue-type 参数可配置,而无需更改应用程序代码。
迁移步骤
关于此主题有一篇较新的博文,它使用了更现代的工具并自动化了大部分过程。
- 首先,需要停止消费者和生产者。
- 应将消息 Shovel 到一个新的临时队列中。
- 应删除旧队列。
- 应创建与原始队列名称相同的新仲裁队列。
- 临时队列的内容现在应 Shovel 到新的仲裁队列中。
- 现在可以重新配置消费者以使用
quorum的x-queue-type并启动它们。
结论
希望这篇博文表明,通过适当的准备,迁移可以卓有成效,且是一个相对简单的尝试。
迁移有很多好处。但也应记住,经典镜像队列已经弃用了一年多,并将在未来的版本中完全移除。因此,即使您现在不打算进行迁移,提前做好这些准备也是一个好主意。
我们已经努力为您提供了此目的的综合指南。也许现在是采取行动的时候了?
