扩展RabbitMQ:优化集群拓扑指南

设计RabbitMQ集群,避免混淆集群、复制和吞吐量。

扩展RabbitMQ:优化集群拓扑指南

扩展RabbitMQ从一个令人不安的事实开始:集群并不是一个神奇的更大代理。它是一组共享元数据的代理,并且根据队列类型,可能复制队列数据。如果单个队列过载,添加节点可能会提高可用性,但不会自动使该队列消费更快。

这一区别避免了许多糟糕的设计。我曾见过团队向繁忙的RabbitMQ部署添加两个节点,不移动任何内容,不更改队列布局,然后疑惑为什么同一个队列仍然在每天下午积压。队列领导者仍在同一节点上。相同的消费者仍在做相同的工作。集群有更多机器,但瓶颈没有移动。

RabbitMQ集群拓扑主要涉及决定队列的位置、需要多少份重要消息的副本,以及在吞吐量下降之前能容忍多少故障。短期指标管道的正确答案与支付、订单履行或审计事件的正确答案不同。

集群实际共享的内容

RabbitMQ集群中的节点共享定义:虚拟主机、用户、权限、交换器、队列、绑定、策略以及集群运行所需的运行时元数据。连接到某个节点的生产者可以发布到队列领导者位于另一节点上的交换器。消费者可以连接到与托管队列不同的节点。

这并不意味着每条消息都存在于每个地方。

经典队列在一个节点上有一个领导者。仲裁队列有一个领导者和副本。流有它们自己的复制模型。如果队列领导者远离大多数使用它的客户端,RabbitMQ必须通过集群互连移动流量。适度情况下这是可以的。当每个生产者连接到节点A,每个热队列位于节点B,每个消费者连接到节点C时,这就会变得昂贵。

一个简单的第一条规则很有效:将应用程序连接到它们使用的队列附近的节点,或者在集群前放置一个负载均衡器,并验证队列领导者合理平衡。不要假设轮询客户端连接会创建轮询队列负载。

在创新之前优先选择三个节点

对于大多数生产RabbitMQ集群,在一个低延迟区域或可用区组中的三个节点是清晰的起点。它为仲裁队列提供了一个多数模型,可以承受一个节点故障,并使集群协调足够简单,以便在事件期间进行推理。

两个节点的集群看起来更便宜,但对于复制队列来说很尴尬。在基于仲裁的系统中,需要多数。如果两个节点中的一个消失,就没有多数。您可以在某些分布式系统中添加一个见证风格的第三个节点,但对于RabbitMQ,通常更简单且更可靠的是运行三个具有足够磁盘和网络容量的真实节点。

当您有许多队列、需要更多放置选项或希望将负载分散到更多机器上时,五个节点可能是有意义的。它还会增加集群通信和操作表面区域的数量。在从三个节点迁移到五个节点之前,检查您是在解决节点饱和问题还是队列设计问题。如果一个队列很热,仅靠更多节点不会拆分该队列的工作。

仲裁队列用于复制可靠性,而非免费速度

对于新的高可用工作负载,当消息持久性重要时,仲裁队列通常是正确的默认选择。它们使用共识协议复制消息。具有三个成员的仲裁队列可以在一个成员不可用的情况下继续运行,只要多数保持健康。

权衡是写入成本。持久化消息必须复制到足够多的成员,才能被视为安全接受。这正是您对重要工作想要的,但它与临时经典队列的性能特征不同。

使用参数或策略声明仲裁队列,具体取决于您的应用程序如何管理拓扑:

rabbitmqadmin declare queue name=orders durable=true arguments='{"x-queue-type":"quorum"}'

对于策略,请仔细限定范围。不要仅仅因为宽泛的模式匹配了.*,就意外地将虚拟主机中的每个队列转换为仲裁队列。一个好的策略名称和狭窄的队列前缀是最佳的无聊方式:

rabbitmqctl set_policy qq-orders '^orders\.' '{"queue-type":"quorum"}' --apply-to queues

如果您从经典镜像队列迁移,请将其视为迁移,而不是标志翻转。经典镜像队列和仲裁队列在排序、毒消息、内存使用和故障转移方面行为不同。创建新的队列类型,路由受控的流量切片,观察确认和消费者延迟,然后移动其余部分。

经典队列仍有其位置

经典队列在不需要复制、消息是临时的或队列对服务来说是本地且可以从其他来源重建的工作负载中仍然有用。它们也适用于高容量低价值事件,在这些事件中,节点故障期间丢失一些消息是可以接受的。

有意使用经典队列。如果经典队列是持久化的并接收持久化消息,这些消息存储在托管该队列的节点上。如果该节点宕机,队列将不可用,直到节点返回。这对于后台协调任务可能没问题。但对于客户可见的订单状态通常不行。

对于长积压,考虑工作负载是否应该是流或不同的存储系统。RabbitMQ可以持有队列,但拥有数百万条旧消息的队列通常表明消费者规模不足、下游系统故障或业务流程需要重放语义而不是队列语义。

在集群周围放置延迟边界

RabbitMQ集群期望低延迟、可靠的网络链接。跨不同区域拉伸一个集群通常是一个糟糕的权衡。节点间流量变慢,故障转移更难预测,网络分区可能比您试图避免的故障更具破坏性。

一个实用的设计是每个区域一个RabbitMQ集群,当需要跨区域移动时,使用应用层路由或联合/铲子。这保持了本地发布和消费的快速。它还使故障域清晰:如果区域A不健康,区域B不会被拖入相同的集群成员问题。

一个区域内多可用区是不同的。如果区域之间的延迟低且稳定,跨三个可用区的三个节点可以很好地工作。在实际负载下测试。云提供商称之为可用区的事实并不能告诉您消息大小、确认和仲裁队列在繁忙时段的表现。

平衡队列领导者,而不仅仅是节点

一个集群在CPU图上可能看起来平衡,但在队列级别可能严重倾斜。一个节点可能拥有最繁忙队列的领导者,而其他节点主要持有安静的副本。

检查队列放置:

rabbitmqctl list_queues name type leader members messages_ready messages_unacknowledged

如果一个节点拥有大多数热领导者,使用RabbitMQ支持的适用于您的版本和队列类型的工具移动或重新平衡队列。对于仲裁队列,成员放置和领导者位置很重要。对于经典队列,队列主节点位置在旧术语中很重要,尽管较新版本更一致地使用领导者语言。

一个好的拓扑将不相关的热队列分散到不同节点。例如,email.sendimage.resizebilling.capture如果每个都有高流量,不应都由同一节点领导。如果billing.capture是唯一的热队列,仅当消费者可以安全地独立处理这些分片时,才按实际业务分片(如商户组或区域)拆分。

设计客户端连接行为

客户端连接放置是拓扑的一部分。如果每个应用程序永远连接到第一个DNS结果,即使队列分散在集群中,一个节点可能承载大部分客户端流量。负载均衡器可以提供帮助,但它应使用了解节点是否实际可用于AMQP流量的健康检查。

保持连接长期存在。RabbitMQ可以处理许多连接,但连接轮换消耗CPU、内存、文件描述符和TLS开销。Web请求不应打开新的AMQP连接、发布一条消息并关闭它。使用适合客户端库的连接或通道池。

还要决定客户端在节点故障期间做什么。好的客户端使用退避重新连接,重新打开通道,如果需要重新声明私有拓扑,并小心地恢复确认或消费。坏的客户端在紧密循环中重新连接,并将节点重启变成连接风暴。

对于消费者,考虑局部性但避免过度拟合。将消费者连接到与其队列领导者相同的节点可以减少节点间流量,但队列领导者可能在故障后移动。消费者应能在无需手动更改的情况下存活。

分区是操作事件,而不仅仅是设置

网络分区是集群图得到测试的地方。RabbitMQ有分区处理模式,但没有设置能消除对清晰操作决策的需求。如果集群的两侧无法通信,您必须决定对于该工作负载,可用性还是一致性更重要。

仲裁队列需要多数副本。这是关键:少数侧不应继续接受无法安全同意的写入。这可能会让期望每个幸存节点保持可写的团队感到惊讶。为此做好计划。将仲裁队列成员放置在您关心的故障中多数可以存活的地方。

不要将三个节点的仲裁队列分散到三个遥远区域,并期望在正常互联网延迟期间表现平稳。仲裁只会像成员之间的网络一样愉快。低延迟和低丢包是容量要求,而不是锦上添花。

在非生产环境中运行分区演练。阻止节点之间的流量,观察哪些队列仍然可用,观察客户端重新连接,并记录恢复步骤。您第一次了解分区行为不应是在真实的网络事件期间。

在扩展代理之前扩展消费者

当症状是队列增长时,代理并不总是瓶颈。通常消费者比发布者慢。在添加RabbitMQ节点之前,检查消费者利用率、未确认计数、处理时间和下游延迟。

如果消息已就绪但未确认,RabbitMQ有消息在等待,消费者没有足够快地获取它们。添加消费者,修复预取,或消除下游延迟。如果消息大多未确认,消费者已经收到工作并且花费太长时间来确认。添加代理节点不会使这些处理程序更快。

预取在这里很重要。慢速工作器上的500预取可以在消费者进程内隐藏积压。快速本地工作器上的1预取可能会浪费往返时间。从小值开始,测量端到端延迟和消费者内存,然后调整。

关注无聊的限制

扩展计划经常谈论拓扑,却忘记文件描述符、磁盘警报、内存警报、连接轮换和通道计数。RabbitMQ对所有这些都很敏感。

对于每个节点,监控使用的内存、可用磁盘、文件描述符、套接字、队列进程内存、消息速率、确认延迟和Erlang调度器利用率。在客户端,监控重新连接循环和通道创建速率。为每次发布打开新连接的服务可能在消息量看起来令人印象深刻之前就损害集群。

在客户端库支持的地方使用长期连接和通道。在设计中加入连接限制和心跳设置,而不是在中断期间的恐慌更改中。

通常有效的拓扑

对于典型的业务应用程序,我会从一个区域内的三个RabbitMQ节点开始,如果网络良好则跨可用区分布。对重要的持久工作流使用仲裁队列。对临时工作使用经典队列,其中故障行为是可接受的。保持发布者和消费者靠近集群。使用负载均衡器进行客户端访问,但验证队列领导者平衡,而不是假设负载均衡器解决了代理放置问题。

然后测试糟糕的情况:杀死一个节点,暂停一个消费者组,填满一个队列,减慢磁盘,并重启一个发布者。扩展RabbitMQ更多是关于当图表受到压力时会发生什么,而不是最漂亮的图表。