跳到主要内容

从镜像经典队列迁移到仲裁队列

·阅读时长:14分钟

仲裁队列是经典镜像队列的优良替代品,在 RabbitMQ 版本 3.8 中引入。您需要迁移有两个互补的原因。

首先,经典镜像队列在 3.9 中已弃用,于 2021 年 8 月 21 日发布了正式公告。它们将在 4.0 中完全移除。

但它们也更可靠、更可预测,对于大多数工作负载来说更快,并且需要更少的维护 - 所以你不应该觉得你的手被强迫而没有明显的理由。

仲裁队列在所有方面都更好,但它们在功能上与镜像队列不完全兼容。因此迁移看起来可能是一项艰巨的任务。

在对未来性能改进进行初步了解后,本文概述了一些可能的迁移策略,并包括有关如何处理不兼容功能的指导。您还可以参考将 RabbitMQ 镜像经典队列迁移到仲裁队列文档,以帮助您完成迁移过程。

性能改进

RabbitMQ 3.10 性能改进 博客文章中,已经详细讨论了仲裁队列的性能优势。

在以下图表中,您可以看到即将发布的 RabbitMQ 3.12 可以预期达到的新性能水平。

Quorum Queues vs Mirrored Queues Performance Showcase
仲裁队列与镜像队列性能展示

该图表显示了使用 1kB 消息在不同工作负载下的吞吐量。越高越好,虽然在一些测试中最大吞吐量受到限制(在这些测试中,我们关注延迟和/或吞吐量是否稳定)。

颜色如下

  • 橙色 - 仲裁队列
  • 绿色 - 镜像经典队列 v1(非延迟)
  • 黄色 - 镜像经典队列 v1(延迟)
  • 蓝色 - 镜像经典队列 v2

无需深入细节,我们可以看到仲裁队列在几乎所有情况下都提供了显著更高的吞吐量。例如,第一个测试是一个单队列、单个发布者、单个消费者的测试。仲裁队列可以维持 30000 条消息/秒的吞吐量(再次使用 1kb 消息),同时提供高水平的数据安全性并将数据复制到集群中的所有 3 个节点。与此同时,经典镜像队列只提供了三分之一的吞吐量,但提供了更低的数据安全保障。在一些测试中,我们可以看到仲裁队列(橙色线)完全平坦,这意味着它们可以维持工作负载并且仍然有一些剩余容量(否则它们的性能将开始波动),而镜像队列提供更低且不太稳定的吞吐量。

细心的读者会注意到,在第二个测试中,仲裁队列最初提供了非常高的发布者吞吐量,但很快下降。这是我们目前正在努力解决的问题,我们希望很快就能改进。这仅仅是当没有消费者并且队列迅速变长(数百万条消息)时的一种特殊情况。

兼容性注意事项

RabbitMQ 文档有一页专门介绍仲裁队列。具体来说,在这篇文档中有一个功能矩阵,它提供了镜像经典队列和镜像队列之间所有差异的列表。这些差异可能需要不同的工作量才能成功迁移。一些它们可能很容易改变,而其他可能需要更改应用程序与 RabbitMQ 交互的方式。所有这些都在后面进行了详细的说明。

而且不言而喻,迁移后的应用程序应该针对仲裁队列进行彻底测试,因为在负载和边缘情况下,行为可能会有所不同。

本文介绍了两种迁移策略。

  • 第一个涉及创建新的虚拟主机,并在联合的帮助下以最小的停机时间迁移。如果所有不兼容的功能都被清理掉或移动到策略中,这也是最佳迁移路径 - 现有的代码只需要更改连接参数,就可以同时与镜像队列和仲裁队列一起工作。
  • 另一个是以牺牲停机时间为代价,重复使用相同的虚拟主机,并且需要能够停止给定队列的所有消费者和生产者。

一般要求

  1. 集群中至少应该有 3 个节点 - 使用少于此数量的副本是没有意义的。
  2. 管理插件应该至少在一个节点上运行 - 它被用于导出/导入单个主机的定义,这可以极大地简化定义清理。(而且 rabbitmqadmin CLI 命令也在幕后使用该插件)。
  3. 铲斗插件应该启用。

查找正在使用的队列和功能

所有镜像经典队列在其有效的策略定义中都有 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,或者手动将策略名称与其定义进行匹配。

重大更改

当使用以下一个或多个功能时,无法直接迁移到仲裁队列。应用程序与代理交互的方式需要更改。

本节概述了如何查找运行系统中是否使用了其中一些功能,以及为更轻松地迁移需要进行哪些更改。

优先级队列

经典镜像队列实际上是在幕后为每个优先级创建了单独的队列。为了迁移,应用程序需要显式处理这些队列的创建,以及发布/消费到它们和从它们中消费。

可以通过在队列列表输出中存在 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 用于消费者

当队列领导者发生故障转移时,镜像队列消费者可以自动取消。这会导致丢失有关哪些消息发送给哪个消费者的信息,以及重新传递这些消息。

仲裁队列不太容易受到这种行为的影响 - 只有在整个节点宕机时才会发生这种情况。对于其他领导者更改(例如,由重新平衡引起),将不会发生重新传递。

当消费者被取消或通道被关闭时,正在传输的消息也会发生重新传递。因此,应用程序需要为重新传递做好准备,而无需专门请求此信息。

琐碎的更改

当使用仲裁队列时,这些功能不会做任何事情。处理它们最好的方法是完全从源代码中删除它们,或者将它们移动到策略中。

延迟队列

经典队列可以选择在延迟模式下运行,但对于仲裁队列来说,这是唯一的运行方式。处理迁移的最佳方法是将 x-queue-mode 从源代码移动到策略中。

非持久队列

非持久队列将在节点/集群启动时被删除。拥有镜像提供的额外持久性保证有点毫无意义。

在将来的版本中,非持久队列的概念也将消失:短暂队列的唯一选择将是独占队列。这只会影响队列定义的持久性,消息仍然可以标记为短暂。

对于这样的队列,必须做出一个决定:队列内容是否足够重要以获得仲裁队列的可用性保证,或者最好将其降级为经典(但持久)队列。

独占队列

即使策略中指定了镜像,独占队列也不会被镜像。但尝试声明一个独占的仲裁队列会导致错误。这显然是迁移不必要的案例之一,但必须注意避免使用显式的x-queue-type: quorum参数声明独占队列。

一次迁移一个虚拟主机上的队列

从经典镜像队列迁移到仲裁队列的过程类似于蓝绿集群升级,不同之处在于迁移可以发生在同一个 RabbitMQ 集群中的新虚拟主机上。联邦插件 随后被用来无缝地从旧的迁移到新的。

此迁移路径的一个重要方面是,可以为新的虚拟主机指定默认队列类型。将其设置为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 之前,需要对它进行以下更改。

  1. 删除您希望在旧虚拟主机中作为经典队列,在新虚拟主机中作为仲裁队列的队列的x-queue-type 声明。
  2. 需要应用于队列定义的其他更改
    • 删除x-max-priority 参数
    • x-overlow 参数设置为reject-publish-dlx 时进行更改
    • 删除x-queue-mode 参数
    • durable 属性更改为true
  3. 更改策略中的以下键
    • 删除所有以ha- 开头的内容:ha-modeha-paramsha-sync-modeha-sync-batch-sizeha-promote-on-shutdownha-promote-on-failure
    • 删除queue-mode
    • 当它被设置为reject-publish-dlx 时更改overflow
  4. 在上一步骤后变为空的策略应该被删除。
  5. 应将与旧虚拟主机之间的联邦添加到任何剩余的策略中,并指向之前创建的联邦上游:"federation-upstream-set":"quorum-migration-upstream"
  6. 如果没有通配策略(适用于具有模式.* 的队列),则需要创建它并也指向联邦上游。这确保了旧虚拟主机中的每个队列都会被联邦。
  7. 在迁移期间,需要删除将联邦规则应用于交换的策略,以避免重复消息。

现在,可以使用 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

将消费者指向新的虚拟主机

此时,只需更新连接参数即可将消费者指向新的虚拟主机。

将生产者指向新的虚拟主机

现在也可以将生产者指向新的虚拟主机。

消费者停止的时间也是在旧虚拟主机中禁用联邦交换,在新虚拟主机中启用它的时间。

在系统负载足够的情况下,旧虚拟主机中的消息不会被接收。如果消息排序很重要,那么应该分步骤进行:停止生产者,将剩余消息铲入新的虚拟主机,在新的虚拟主机上启动消费者。

将剩余消息铲入新的虚拟主机

对于旧主机中的每个非空队列,都需要配置一个铲子。

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"}'

队列被清空后,铲子可以被删除。

rabbitmqctl clear_parameter shovel migrate-QUEUE_TO_MIGRATE

确保将来的队列声明成功

许多应用程序在使用之前会在多个地方声明队列。当迁移到经典镜像队列时,这会导致一个问题:如果客户端在没有显式提供队列类型的情况下声明队列,那么在完成移动定义步骤后,所有将来的声明尝试都会在重新声明现有队列时遇到PRECONDITION_FAILURE 通道错误。

为了避免这种情况,有三种选择。

  1. x-queue-type 声明重新添加到所有使用仲裁队列的客户端。
  2. 使用default_queue_type(一个在RabbitMQ 3.13.3 及更高版本中可用的rabbitmq.conf 设置)在节点范围内设置默认队列类型。
  3. 设置quorum_queue.property_equivalence.relaxed_checks_on_redeclaration = true,一个从RabbitMQ 3.11.16 开始可用的rabbitmq.conf 设置。

第三个选项,将quorum_queue.property_equivalence.relaxed_checks_on_redeclaration 设置为true,可以在迁移过程中的任何时间采用。

就地迁移

在这个版本的流程中,我们用停机时间来换取在现有虚拟主机和集群中执行迁移的能力。

对于要迁移的每个队列(或一些队列组),应该能够在迁移期间停止所有消费者和生产者。

准备生产者和消费者

所有不兼容的功能都应该被清理。除此之外,在每个声明队列的地方,最好使x-queue-type 参数可配置,而无需更改应用程序代码。

迁移步骤

  1. 首先,需要停止消费者和生产者。
  2. 应该将消息铲入一个新的临时队列。
  3. 应该删除旧的队列。
  4. 应该创建一个与原始队列同名的新的仲裁队列。
  5. 现在应该将临时队列的内容铲入新的仲裁队列。
  6. 现在可以将消费者重新配置为使用x-queue-typequorum 并启动。

结论

希望这篇博文已经表明,通过适当的准备,迁移可以是有益的,并且是一个相对简单的尝试。

迁移有很多好处。但也要记住,经典镜像队列已经过时一年多了,将在即将发布的版本中完全删除。因此,即使您现在不打算进行迁移,事先做好这些准备也是一个好主意。

我们已经尽力为您提供这方面的综合指南。也许现在是时候做点什么了?

© 2024 RabbitMQ. All rights reserved.