RabbitMQ 获得高可用性升级
这是关于仲裁队列(我们新的复制队列类型)系列的第一部分。我们将涵盖从仲裁队列是什么,到硬件要求,从镜像队列迁移以及最佳实践的方方面面。
介绍仲裁队列
多年来,镜像队列(也称为高可用性队列)一直是需要为消息提供额外数据安全保证的事实上的选择。仲裁队列是下一代复制队列,旨在取代镜像队列的大多数用例,并且从 3.8 版本开始提供。
在本系列博客中,我们将涵盖以下内容
- 第一部分 - 了解新复制队列类型的需求及其工作原理(本文)
- 第二部分 - 为什么您的存储驱动器选择对于仲裁队列很重要
- 第三部分 - 仲裁队列和流量控制。概念。
- 第四部分 - 仲裁队列和流量控制。单队列基准测试。
- 第五部分 - 仲裁队列和流量控制。压力测试。
- 第六部分 - 仲裁队列和集群/虚拟机大小调整(即将推出)
- 第七部分 - 何时以及如何从镜像队列迁移到仲裁队列(即将推出)
- 第八部分 - 最佳实践和注意事项(即将推出)
- 第九部分 - 监控和管理操作(即将推出)
为什么要提供一种新的复制队列类型?
镜像队列基于一种称为链式复制的复制算法。在队列的上下文中,链式复制形成一个队列环,其中有一个领导者(主节点)和一个或多个副本(镜像)。所有消息都发布到领导者并从领导者消费,领导者然后将这些操作复制到其相邻的镜像,再由该镜像复制到其相邻的镜像。这个过程一直持续到最后一个镜像,然后它会通知主节点操作已完全复制。

由于一些导致消息丢失的极端情况,该算法进行了修改,使得通道进程会额外地直接将消息发送到每个镜像。这不幸意味着接收来自发布者的消息的代理必须发送每条消息两次,这使网络负载加倍。

关于这种复制算法,还有一些痛点,主要集中在它如何处理离开环的镜像,无论是由于服务器重启、节点故障还是网络分区。
当一个镜像与同伴的通信时间超过某个时间限制后,它将被从环中移除,队列将继续可用。问题出现在镜像重新加入环时。首先,镜像会丢弃所有其数据,然后,可以选择性地开始一个称为同步的过程。
同步是指主节点将其当前消息复制到一个镜像。这是一个“停止世界”的过程,队列将冻结直到同步完成。如果队列非常大,这会成为一个问题,因为不可用时间可能会很长。

另一种选择是不对重新加入的镜像与主节点进行同步。在这种情况下,我们会得到较低的冗余度,但可以避免潜在的痛苦的同步。当然,如果队列为空或消息很少,同步就不会造成大问题。
另一个重要的话题是如何处理网络分区。当发生分区将集群分成两半时,我们将得到一个或多个失去与主节点通信的镜像。作为管理员,我们此时可以选择可用性或一致性。`cluster_partition_handling` 配置决定了 RabbitMQ 如何处理分区。
如果我们不想丢失消息,那么我们将配置集群使用 `pause-minority` 模式。这基本上会停止分区中少数方上的所有代理。在多数方(如果存在)上,队列将继续运行,只是冗余度降低。分区解决后,集群将恢复正常。这种策略选择了一致性(尽管冗余度较低)而不是可用性。

如果我们希望在分区的两边都能持续可用,那么我们可以选择 `ignore` 或 `auto-heal` 模式。这将允许一个镜像被提升为主节点,这意味着我们在两边都有一个主节点。这使得队列能够继续接收和传递消息,无论客户端连接到分区的哪一侧。不幸的是,在分区解决后,分区的一侧将被选为受害者并重新启动,导致该侧队列中的任何消息丢失。这不是一个理想的选择,但在某些情况下,它仍然可能比变得不可用更适合您的需求。

我们需要一种更好的复制算法,这就是仲裁队列(quorum queues)的诞生。
仲裁队列
仲裁队列 不使用链式复制,而是基于成熟且经过数学证明的 **Raft** 协议。Raft 是一种将操作日志复制到节点集群的共识算法。它要求大多数(quorum)参与节点可用,并就追加到分布式日志的每个新操作达成一致。这就是仲裁队列名称的由来。
队列有哪些操作?我们有入队(enqueue)操作和消费者确认(consumer acknowledgment)操作。与镜像队列一样,所有客户端都与一个领导者交互,领导者的任务是将入队和确认复制到其跟随者。

该算法比镜像队列更高效,可以实现更高的吞吐量。它的端到端延迟确实较高,并且这些延迟与磁盘的吞吐量/延迟密切相关。仲裁队列仅在消息写入多数节点磁盘后才确认消息,因此磁盘性能在仲裁队列的性能中起着重要作用。
没有艰难的决定
没有“停止世界”的同步,重新加入时不会丢弃数据,也没有关于自动与手动同步的艰难决定。不需要在可用性与一致性之间做出选择;仲裁队列仅在消息已复制到多数节点后才确认消息。如果多数节点宕机,您将失去可用性。
网络分区
在处理网络分区时,仲裁队列要简单得多。首先,它们使用一个独立且速度更快的故障检测器,可以快速检测分区并触发快速的领导者选举,这意味着可用性要么不受影响,要么会迅速恢复。`cluster_partition_handling` 配置不适用于仲裁队列,尽管 `pause_minority` 模式仍然可能影响仲裁队列,因为当少数方暂停时,托管在该节点上的任何仲裁队列领导者都将变得不可用。然而,由于故障检测器的速度,在发生分区时,领导者选举应该在暂停触发之前就已经选出了新的领导者。
仲裁队列适用于数据安全至上的场景。但请记住,数据安全始于应用程序的正确操作,正确使用发布者确认和消费者确认。仲裁队列确实有一些限制和注意事项,我们将在本系列的后续文章中进行探讨。
x-queue-type: classic and quorum
现在我们有了新的队列类型,我们将队列称为“经典”或“仲裁”队列。经典队列就是以前的旧队列。使用 `ha-mode`、`ha-params`、`ha-sync-mode` 属性声明一个经典队列将使其成为经典镜像队列,定义一个具有这些参数且与队列匹配的策略也是如此。
您不能通过策略将经典队列转换为仲裁队列。必须将 `x-queue-type` 属性设置为 `quorum` 才能包含在队列声明中。它从出生起就是仲裁队列。
管理 UI 中的仲裁队列
在下面的截图里,首先您会注意到 `x-queue-type: quorum` 参数和 `x-quorum-initial-group-size: 3` 参数,这表明该队列是一个仲裁队列,复制因子为 3(一个领导者和两个跟随者)。

图 7. 管理 UI 中看到的仲裁队列我们还可以看到队列的成员,哪些成员在线以及当前领导者是谁。
声明仲裁队列就像声明任何其他队列一样简单,只需使用 `x-queue-type` 参数即可。默认情况下,仲裁大小(复制因子)为 5,尽管在较小的集群中,大小将是集群本身的大小。
在管理 UI 中,您将看到添加经典队列或仲裁队列的选项。

选择 Quorum 后,您会看到可用的参数会发生变化。

仲裁队列有三个新参数可用。其中两个与内存中保留的消息数量有关。默认情况下,所有消息都保留在内存中,因此如果仲裁队列的长度增长,可能会对集群造成内存压力。我们可以通过设置以下一个或两个参数来限制消息占用的内存量:
- x-max-in-memory-length
- x-max-in-memory-bytes
队列索引仍然存储在内存中,因此持续增长的队列仍然可能对集群造成内存压力。我们将在本系列的后续内容中更详细地探讨这一点。
新的“毒药消息”(poison message)功能仅适用于仲裁队列,并使用 `x-delivery-limit` 参数设置。每次消息被重新传递给消费者时,计数器就会递增。一旦重新传递计数超过 `x-delivery-limit`,消息就会被丢弃或进入死信队列(如果配置了 DLX 交换)。
要点
仲裁队列提供了万无一失的安全保证,并在重启服务器方面减少了麻烦。它们对硬件的压力也与镜像队列不同,因此在您做出决定之前,请查看我们的后续文章,其中包含有关迁移和所需硬件的指导。
在 下一篇文章 中,我们将讨论为什么推荐 SSD 而不是 HDD,以及我们从每种存储驱动器类型获得的性能特征。