从镜像经典队列迁移到仲裁队列
仲裁队列是 RabbitMQ 3.8 版本中引入的经典镜像队列的更优替代品。您需要迁移的原因有两个互补的方面。
首先,经典镜像队列在 3.9 版本中已被弃用,并于 2021 年 8 月 21 日发布了正式公告。它们将在 4.0 版本中完全移除。
而且它们更可靠、更可预测,对于大多数工作负载来说速度更快,并且需要的维护更少 - 因此您不应觉得迁移是毫无理由的强制行为。
仲裁队列在所有方面都更胜一筹,但它们在功能上与镜像队列并非 100% 兼容。因此,迁移看起来可能是一项艰巨的任务。
在简要了解未来性能改进之后,本文概述了几种可能的迁移策略,并包含了关于如何处理不兼容功能的指南。《将 RabbitMQ 镜像经典队列迁移到仲裁队列文档》也可帮助您完成迁移过程。
性能改进
在《RabbitMQ 3.10 性能改进》博客文章中,已经详细讨论了仲裁队列的性能优势。
在下面的图表中,您可以看到即将发布的 RabbitMQ 3.12 版本的性能水平。

此图表显示了在不同工作负载下使用 1kB 消息的吞吐量。越高越好,尽管在某些测试中,最大吞吐量受到限制(在这些测试中,我们关注延迟和/或吞吐量是否稳定)。
颜色如下所示
- 橙色 - 仲裁队列
- 绿色 - 镜像经典队列 v1(非延迟)
- 黄色 - 镜像经典队列 v1(延迟)
- 蓝色 - 镜像经典队列 v2
无需深入太多细节,我们可以看到仲裁队列在几乎所有情况下都提供了明显更高的吞吐量。例如,第一个测试是单队列、单发布者、单消费者的测试。仲裁队列可以维持 30000 msg/s 的吞吐量(再次使用 1kb 消息),同时提供高水平的数据安全性和将数据复制到集群中的所有 3 个节点。与此同时,经典镜像队列仅提供三分之一的吞吐量,但数据安全保证却低得多。在某些测试中,我们可以看到仲裁队列(橙色线)完全平稳,这意味着它们可以维持工作负载并且仍然有一些剩余容量(否则它们的性能将开始波动),而镜像队列提供的吞吐量更低且更不稳定。
细心的读者可能会注意到,在第二个测试中,仲裁队列最初提供了非常高的发布者吞吐量,但很快就下降了。这是我们目前正在努力解决的问题,我们希望很快能够改进。这只是一个在没有消费者的情况下出现的极端情况,队列很快变得非常长(数百万条消息)。
兼容性考虑
RabbitMQ 文档有一个专门介绍 仲裁队列的页面。具体而言,本文档中有一个 功能矩阵,其中列出了镜像经典队列和镜像队列之间的所有差异。这些差异可能需要不同程度的工作才能成功迁移。其中一些更改可能很简单,而另一些更改可能需要改变应用程序与 RabbitMQ 交互的方式。所有这些都在后续文档中进行了详尽的说明。
毋庸置疑,迁移后的应用程序应针对仲裁队列进行全面测试,因为在负载和极端情况下,行为可能会有所不同。
本文介绍了 2 种迁移策略
- 第一种策略涉及创建一个新的 vhost,并在联邦的帮助下以最小的停机时间进行迁移。如果所有不兼容的功能都已清除或移至策略,这也是一种简单的迁移方式 - 现有代码只需更改连接参数即可同时与镜像队列和仲裁队列一起工作。
- 另一种策略牺牲了正常运行时间,以重新使用相同的虚拟主机,并且需要能够停止给定队列的所有消费者和生产者。
一般要求
- 集群中应至少有 3 个节点 - 使用较少数量的副本的仲裁队列是没有意义的。
- 管理插件应在至少一个节点上运行 - 它用于导出/导入单个主机的定义,这可以大大简化定义清理。(并且
rabbitmqadmin
CLI 命令也在幕后使用该插件)。 - 应启用 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
。需要更新代码以使用发布者确认并自行执行死信处理。
可以通过上面队列列表输出中是否存在 reject-publish-dlx
来检测此功能。可以在源代码中搜索完全相同的字符串。
消费者的全局 QoS
仲裁队列不支持消费者的全局 QoS。需要决定如何使用替代方法实现必要的结果,例如,通过使用较低的每消费者 QoS,可以获得大致相同的结果(考虑到已知的应用程序负载模式)。
要检测是否使用了此功能,可以针对正在运行的系统执行以下命令,并检查是否有非空输出
rabbitmqctl list_channels pid name global_prefetch_count | sed -n '/\t0$/!p'
它将提供一个已启用全局 QoS 的通道 PID 列表,然后可以将其映射到队列名称并检查是否为镜像队列
rabbitmqctl list_consumers queue_name channel_pid
x-cancel-on-ha-failover
for consumers
当队列 leader 发生故障转移时,镜像队列消费者可以自动取消。这可能会导致丢失有关哪些消息已发送给哪些消费者的信息,并重新传递这些消息。
仲裁队列不太容易受到此类行为的影响 - 唯一仍然可能发生的情况是整个节点发生故障。对于其他 leader 更改(例如,由重新平衡引起),不会发生重新传递。
并且当消费者被取消或通道关闭时,对于正在处理中的消息也可能发生重新传递。因此,应用程序需要为重新传递做好准备,而无需专门请求此类信息。
简单的更改
当使用仲裁队列时,这些功能不起任何作用。处理它们的最佳方法是从源代码中完全删除它们,或者将它们移动到策略中。
延迟队列
经典队列可以选择以延迟模式运行,但对于仲裁队列,这是唯一的运行方式。迁移处理此问题的最佳方法是将 x-queue-mode
从源代码移动到策略。
非持久队列
非持久队列将在节点/集群启动时被删除。具有镜像提供的额外持久性保证有点毫无意义。
非持久队列的概念也将在未来的版本中消失:临时队列的唯一选择将是独占队列。这仅影响队列定义的持久性,消息仍然可以标记为瞬态。
对于此类队列,必须做出一个或另一个决定:此队列内容是否足够重要以获得仲裁队列的可用性保证,还是最好将其降级为经典(但持久)队列。
独占队列
即使策略如此规定,独占队列也不会被镜像。但是,尝试声明独占仲裁队列将导致错误。这显然是不需要迁移的情况之一,但必须注意避免使用显式的 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
。(请注意,默认 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
时,更改它
- 删除以
- 上一步之后变为空的策略应删除。
- 应将与旧 vhost 的联邦添加到任何剩余策略中,指向之前创建的联邦上游:
"federation-upstream-set":"quorum-migration-upstream"
- 如果没有捕获所有策略(应用于模式为
.*
的队列),则需要创建它并指向联邦上游。这确保了旧 vhost 中的每个队列都将被联邦。 - 应用于交换机的联邦规则的策略需要在迁移期间删除,以避免重复消息。
现在可以将修改后的 schema 从 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
将消费者指向新的 vhost
此时,只需更新连接参数即可将消费者指向新的虚拟主机。
将生产者指向新的 vhost
现在也可以将生产者指向新的虚拟主机。
停止消费者的时间也是应在旧 vhost 中禁用联合交换机,在新 vhost 中启用联合交换机的时间。
在足够的系统负载下,将不会拾取来自旧虚拟主机的消息。如果消息排序很重要,那么这应该分步骤完成:停止生产者,将剩余消息铲到新的虚拟主机,在新虚拟主机上启动消费者。
将剩余消息铲到新的 vhost
对于旧主机中的每个非空队列,都需要配置一个 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
参数可配置,而无需更改应用程序代码。
迁移步骤
- 首先,需要停止消费者和生产者。
- 消息应铲到新的临时队列中。
- 应删除旧队列。
- 应创建与原始队列同名的新仲裁队列。
- 现在应将临时队列的内容铲到新的仲裁队列。
- 现在可以重新配置消费者以使用仲裁的
x-queue-type
并启动。
结论
希望这篇博客文章已经表明,通过适当的准备,迁移可以富有成效且相对简单。
迁移有很多好处。但人们也应该记住,经典镜像队列已被弃用一年多,并且将在即将发布的版本中完全删除。因此,即使您现在不打算进行迁移,提前做好这些准备也可能是一个好主意。
我们已尝试为您提供针对此目的的全面指南。也许现在是时候做点什么了?