跳至主内容
版本:4.3

Quorum Queues

概述

RabbitMQ 仲裁队列是一种现代队列类型,它基于 Raft 一致性算法实现,是一种持久化且支持复制的队列。当需要高可用、支持复制的队列时,仲裁队列应作为首选方案。

仲裁队列专为数据安全而设计,具有可靠且快速的领导者选举特性,即便在升级或其他系统波动期间,也能确保高可用性。

某些应用场景比其他场景更适合使用仲裁队列,在系统设计时了解这一点非常重要。

与经典队列相比,仲裁队列在行为上存在差异,且有一些限制条件,在将使用经典队列的应用程序迁移至仲裁队列时,务必考虑这些因素。

仲裁队列具有一些特有的新功能,例如有害消息处理 (poison message handling)至少一次死信投递 (at least once dead-lettering)延迟重试 (delayed retry)消费者超时 (consumer timeouts),以及在使用 AMQP 时利用 modified 结果对消息进行标注的功能。

仲裁队列和流 (streams) 是目前可用的两种复制数据结构。从 RabbitMQ 4.0 开始,经典队列镜像功能已被移除。

涵盖主题

本文档涵盖的主题包括:

等等。

在深入了解仲裁队列时,熟悉 RabbitMQ 集群的基础知识会很有帮助。

什么是仲裁 (Quorum)?

分布式系统中的仲裁可以定义为系统大部分成员达成的一致协议((N/2)+1,其中 N 是系统参与者的总数)。

当应用于 RabbitMQ 集群中的队列复制时,这意味着大部分队列成员(领导者和跟随者)在队列的精确状态及其内容上达成一致。

用例

仲裁队列并非设计用于所有需要队列的应用场景。

仲裁队列旨在用于那些通常长期存在、需要高可用性,且对应用程序架构的某些方面至关重要的拓扑结构中。

良好的应用场景示例包括销售系统中的订单输入,或选举系统中的投票,在这些场景中,丢失消息可能会对整个系统的正确性和功能产生重大影响。

股票代码、即时通讯系统和 RPC 回复队列从仲裁队列中获益较少甚至无益。

对于需要复制和可重复读取的应用场景(例如扇出通知),流 (streams) 可能是比仲裁队列更好的选择。

系统能够支撑的仲裁队列数量没有硬性限制。然而,如果某个应用场景需要超过约 5000 个仲裁队列,建议重新审查其拓扑结构,看看是否可以用经典队列或流来替代部分仲裁队列。

无论系统中仲裁队列的数量多少,始终建议进行升级测试和故障测试。

发布者应使用发布者确认机制 (publisher confirms),这是客户端与仲裁队列一致性系统交互的方式。发布者确认仅在消息成功复制到仲裁数量的成员并被视为在队列上下文中“安全”后,才会发出。

消费者应使用手动确认 (manual acknowledgements),以确保未能成功处理的消息被返回到队列,以便另一个消费者可以重新尝试处理。

何时不使用仲裁队列

在某些情况下不应使用仲裁队列。这些情况通常涉及:

  • 临时队列:瞬态或排他队列、高队列流失率(声明和删除频繁)
  • 极低延迟需求:由于数据安全特性,底层的共识算法本身具有较高的延迟
  • 当数据安全不是优先级时(例如应用程序不使用手动确认和发布者确认
  • 非常长的队列积压(500 万+ 条消息)(流 (streams) 可能更适合)
  • 大型扇出:(流 (streams) 可能更适合)

特性

仲裁队列与其他队列类型共享大多数基本原理。

与经典队列的比较

以下操作对于仲裁队列和经典队列的工作方式相同:

某些队列操作存在细微差异:

  • 声明
  • 设置消费者的预取

功能矩阵

功能经典队列Quorum Queues
非持久化队列
消息复制
独占性
每消息持久化每消息始终
成员变更半自动
消息 TTL (生存时间)
队列 TTL部分(队列重新声明时租约不会续期)
队列长度限制是(x-overflow: reject-publish-dlx 除外)
将消息保存在内存中参见 经典队列从不(参见 资源使用
消息优先级是,严格执行
单活跃消费者
消费者排他性否(使用 单活跃消费者 (Single Active Consumer)
消费者优先级
死信交换器
遵守策略是(参见 策略支持
有害消息处理
消费者超时
延迟重试
服务器命名队列

现代仲裁队列还为许多工作负载提供了更高的吞吐量和更低的延迟波动

队列和每消息 TTL

仲裁队列同时支持 队列 TTL 和消息 TTL(包括队列级消息 TTL发布者级消息 TTL)。使用任何形式的消息 TTL 时,每条消息的内存开销会增加 16 字节。

长度限制

仲裁队列支持 队列长度限制

支持 drop-headreject-publish 溢出行为,但不支持 reject-publish-dlx 配置,因为仲裁队列采用了与经典队列不同的实现方法。

当前 reject-publish 溢出行为的实现并未严格强制执行限制,允许仲裁队列超出其限制至少一条消息,因此在需要精确限制的场景中应谨慎使用。

当仲裁队列达到最大长度限制且配置了 reject-publish 时,它会通知每个发布通道,从那时起通道将拒绝所有消息并返回给客户端。这意味着仲裁队列可能会超过其限制少量消息,因为在通知通道时可能有消息正在传输中。队列接受的额外消息数量取决于此时正在传输的消息数量。

死信处理

仲裁队列支持通过死信交换器 (DLX) 进行死信处理

传统上,在集群环境中使用 DLX 并不安全

仲裁队列支持一种更安全的死信处理形式,它在队列间传输消息时使用 at-least-once(至少一次)保证(具有下述限制和注意事项)。

这是通过实现一个特殊的、内部的死信消费者进程来完成的,它类似于带有手动确认功能的常规队列消费者,只是它只处理被放入死信队列的消息。

这意味着源仲裁队列将保留死信消息,直到它们被确认为止。内部消费者将消费死信消息,并使用发布者确认将它们发布到目标队列。只有在收到发布者确认后,它才会发送确认,从而提供 at-least-once 保证。

at-most-once(至多一次)仍然是仲裁队列的默认死信策略,适用于死信消息本质上是信息性的,且在队列间传输过程中丢失或下述溢出配置限制不适用时不太重要的场景。

激活“至少一次”死信处理

要为源仲裁队列激活或开启 at-least-once 死信处理,请调整启用死信的策略,如下所示:

  • 设置 dead-letter-strategyat-least-once(默认是 at-most-once)。
  • 设置 overflowreject-publish(默认是 drop-head)。
  • 配置 dead-letter-exchange
  • 如果未启用,请启用 stream_queue 特性标志

建议同时配置 max-lengthmax-length-bytes,以防止源仲裁队列中出现过多的消息积压(参见下文注意事项)。

可选地配置 dead-letter-routing-key

限制

即使未设置队列长度限制,at-least-once 死信处理也不适用于默认的 drop-head 溢出策略。因此,如果配置了 drop-head,死信处理将回退到 at-most-once。请改用 reject-publish 溢出策略。

注意事项

at-least-once 死信处理需要更多的系统资源,如内存和 CPU。因此,只有在死信消息不能丢失的情况下才开启 at-least-once

at-least-once 保证会引发一些需要处理的特定失败情况。由于死信消息现在由源仲裁队列保留,直到它们被死信目标队列安全接受,这意味着它们必须计入队列资源限制(如最大长度限制),以便队列能够拒绝接受更多消息,直到部分消息被删除。在理论上,如果目标死信队列长时间无法接收消息而正常消费者又消耗了大部分消息,队列可能只包含死信消息。

死信消息在被死信目标队列确认之前被视为“活动”状态。

在少数情况下,死信消息不会及时从源队列中删除:

  • 配置的死信交换器不存在。
  • 消息无法路由到任何队列(等同于 mandatory 消息属性)。
  • (可能多个中的)一个已路由目标队列未确认收到消息。这可能发生在目标队列不可用或目标队列拒绝消息时(例如由于超出队列长度限制)。

如果出现上述任一场景,死信消费者进程将定期重试,这意味着 DLX 目标队列中可能会出现重复消息。

对于每个开启了 at-least-once 死信处理的仲裁队列,将有一个内部死信消费者进程。该进程与仲裁队列的领导者节点位于同一处,并将所有死信消息体保存在内存中。它使用 32 条消息的预取大小来限制在未收到目标队列确认时保存在内存中的消息体数量。

如果需要高死信处理吞吐量(每秒数千条消息),可以通过高级配置文件rabbit 应用部分的 dead_letter_worker_consumer_prefetch 设置来增加该预取大小。

对于源仲裁队列,可以动态切换死信策略,从 at-most-onceat-least-once,反之亦然。如果死信策略发生更改(无论是直接从 at-least-once 变更为 at-most-once,还是间接通过将溢出策略从 reject-publish 变更为 drop-head),所有尚未被所有目标队列确认的死信消息都将被删除。

无论消息投递模式(瞬态或持久)如何,发布到源仲裁队列的消息都会持久化到磁盘。然而,由源仲裁队列进行死信处理的消息将保持原始的消息投递模式。这意味着如果目标队列中的死信消息在代理重启后应保留,则目标队列必须是持久的,并且在向源仲裁队列发布消息时,消息投递模式必须设置为持久。

消费者优先级

仲裁队列支持 消费者优先级

消息优先级

重要

自 RabbitMQ 4.3 起,仲裁队列支持严格的消息优先级。

从 RabbitMQ 4.3 开始,仲裁队列支持 32 个优先级水平(0-31)的严格消息优先级。与之前的“公平共享”方法(高优先级和普通优先级之间 2:1 的比例)不同,严格优先级确保高优先级消息总是先于低优先级消息投递。

支持 0-31 的优先级级别。高于 31 的优先级值将被限制为 31(最高优先级)。这允许应用程序使用标准优先级值,而不必担心超过最大值。

分优先级的消息计数

每个优先级级别维护单独的消息计数,可在管理 UI 中查看。这允许操作员按优先级监控队列深度,并确保高优先级消息不会被低优先级消息阻塞。

返回消息的重新投递

当消息被返回到队列(通过 rejectnackmodify)时,它们不会保留其原始优先级。相反,它们被添加到返回队列中,并按照它们被返回的精确顺序重新排队,而不考虑其优先级。

优先级感知消息过期

消息过期 (TTL) 扫描现在跨所有优先级级别运行,确保消息无论其优先级级别如何,都能按正确顺序过期。对于每个优先级级别,扫描将仅处理消息直到遇到第一个未过期的消息。这防止了低优先级消息阻塞已超过其 TTL 的高优先级消息的过期处理。

管理 UI 支持

管理 UI 显示每个仲裁队列的分优先级消息计数,允许操作员:

  • 监控按优先级级别细分的队列深度
  • 识别高优先级消息是否被低优先级消息延迟
  • 就消费者分配和队列配置做出明智决策

消费者超时

重要

自 RabbitMQ 4.3 起,仲裁队列支持消费者超时。

仲裁队列支持可配置的消费者超时,以处理缓慢或卡死的消费者。当消费者持有未确认消息超过配置的超时时间时,消息会自动返回到队列,并可供另一个消费者重新投递。

工作原理

当消费者超时触发时:

  1. 对于 AMQP 1.0 客户端:代理发送带有 state=releasedDISPOSITION 帧,而不是断开链路。这允许客户端自动完成交付并可能在无需重新建立链路的情况下恢复。在消费者确认超时消息之前,不会再向其分配任何消息。

  2. 对于 AMQP 0.9.1 客户端:消费者被取消(如果客户端支持 consumer_cancel_notify 能力),否则通道将以 precondition_failed 错误关闭。在这两种情况下,消息都会被返回到队列以供重新投递。

  3. 对于 MQTT 客户端:消费者订阅结束,消息返回到队列。

这种优雅的处理方式(特别是针对 AMQP 1.0)与强制断开链路相比,提供了更好的可靠性并减少了连接抖动。

配置选项

消费者超时可以在消费者级、队列级或全局级别进行配置:

消费者连接属性 (AMQP 1.0)
rabbitmq:consumer-timeout
消费者参数 (AMQP 0.9.1)
x-consumer-timeout

在创建消费者时设置,仅将超时应用于该消费者。

队列参数
x-consumer-timeout

在声明队列时设置,应用于该队列的所有消费者。

策略键
consumer-timeout

将消费者超时应用于匹配模式的所有队列。

全局设置

rabbitmq.conf 中设置。

consumer_timeout = 1800000

超时值以毫秒为单位。

断开的消费者节点超时

当消费者的节点由于网络分区而无法访问时,仲裁队列将等待一段时间(可配置)后再返回该消费者持有的消息。这由 consumer_disconnected_timeout 设置控制。

策略键
consumer-disconnected-timeout
全局设置

rabbitmq.conf 中设置。

consumer_disconnected_timeout = 60000

默认值为 60 秒(60000 毫秒)。

队列参数
x-consumer-disconnected-timeout

管理 UI 和监控

管理 UI 显示:

  • 每个出站链路的消费者超时状态(激活时以黄色高亮显示)
  • 断开的消费者状态
  • 解释消费者超时功能的有用工具提示

当发生消费者超时时,会记录警告,包括:

  • 连接名称
  • 链路名称和句柄
  • AMQP 容器 ID
  • 队列名称

延迟重试

重要

自 RabbitMQ 4.3 起,仲裁队列支持延迟重试。

仲裁队列支持为返回到队列的消息配置带线性退避的延迟重试。当消息被消费者拒绝、Nack 或修改时,它可以先保持在延迟状态,然后再变为可重新投递。这有助于防止快速重试循环并实现线性退避策略。

工作原理

当消息被返回且启用了延迟重试时,该消息会保存在一个按“就绪时间”时间戳索引的延迟队列中。延迟基于消息的投递计数进行计算。

delay = min(min_delay * delivery_count, max_delay)

例如,设置 min_delay=1000msmax_delay=30000ms

  • 第一次返回:延迟 1000ms
  • 第二次返回:延迟 2000ms
  • 第三次返回:延迟 3000ms
  • 第四次及以后:延迟 30000ms(上限为 max_delay)

注意:消息的 delivery-count 在故障(会话/通道崩溃,以及使用 delivery_failed=truemodified 结果)时递增。这意味着并非所有返回类型都会导致消息延迟时间超过最小值。

配置选项

延迟重试可以通过队列参数(在声明时)、队列策略或两者配置。

队列参数

在使用 AMQP 0.9.1 声明队列时可以设置队列参数:

  • x-delayed-retry-type - 重试类型:disabledallfailedreturned(默认:disabled
  • x-delayed-retry-min - 最小延迟(毫秒,如启用则必填)
  • x-delayed-retry-max - 最大延迟(毫秒,可选)
策略键

策略提供了一种跨多个队列配置延迟重试的方法:

  • delayed-retry-type - 重试类型:disabledallfailedreturned
  • delayed-retry-min - 最小延迟(毫秒)
  • delayed-retry-max - 最大延迟(毫秒)
重试类型
  • disabled - 不应用延迟重试(默认)
  • all - 所有返回的消息都会被延迟,无论投递计数是否递增
  • failed - 仅递增 delivery-count 的消息会被延迟(例如通过 reject、AMQP 1.0 的 delivery_failed=truemodify,或带有挂起消息的通道终止)
  • returned - 仅未递增 delivery-count 的消息会被延迟(例如通过 nack 或 AMQP 1.0 的 delivery_failed=falsemodify

配置示例

rabbitmqctl set_policy qq-delayed-retry \
"^critical\." \
'{"delayed-retry-type": "all", "delayed-retry-min": 1000, "delayed-retry-max": 30000}' \
--priority 10 \
--apply-to "quorum_queues"

高级功能

显式投递时间

可以在消息属性上设置 x-opt-delivery-time 注释,以指定显式的未来投递时间(自纪元以来的毫秒数)。此注释仅在使用 AMQP modify 结果时可用,且优先于计算出的线性退避延迟。

管理 UI 和监控

延迟重试配置、延迟消息计数和重试状态显示在管理 UI 中,允许操作员:

  • 查看每个队列的延迟重试配置
  • 监控每个队列的延迟消息数
  • 识别消息延迟过多的队列

有害消息处理

仲裁队列支持处理有害消息,即导致消费者反复将投递重新排队(可能由于消费者故障)的消息,以至于消息永远无法被完全消费和确认,从而无法被 RabbitMQ 标记为删除。

仲裁队列会跟踪不成功的(重新)投递尝试次数,并将其显示在随任何重新投递消息一起包含的 "x-delivery-count" 头部中。

从 RabbitMQ 4.3 开始,投递限制基于 delivery-count 而非 acquired-count。这意味着现在允许无限次的显式消息返回(通过 nack 或 AMQP 1.0 中 delivery_failed=falsemodify),且不计入投递限制。已引入 x-acquired-count 头部来跟踪消息被消费者获取的次数,以及跟踪实际失败投递次数的 x-delivery-count

提示

x-acquired-count 是跟踪消息被分配给消费者的次数的推荐头部。它提供了关于消息可能被消费者看到的次数的更准确信息。请注意,此计数器是在消息分配给消费者时递增的。这并不意味着消费者实际上已经看到了该消息。

重要

当消息被重新投递的次数超过限制时,该消息将被丢弃(删除)或进入死信队列(如果配置了 DLX)。

对于预取大于 1 的消费者,如果所有未确认的投递作为一组被多次重新排队(例如由于消费者应用程序实例故障),则它们都可能被此机制丢弃。

建议所有仲裁队列都有某种死信配置,以确保消息不会被无意中丢弃和丢失。为低优先级死信策略使用单个流 (stream) 是一种很好的、低资源消耗的方式,可以确保丢弃的消息在一段时间内得以保留。

重要

从 RabbitMQ 4.0 开始,仲裁队列的投递限制默认设置为 20。

可以通过在声明队列时设置 x-delivery-limit=-1 可选队列参数来恢复 3.13.x 时代没有限制的行为。

建议这样做。

何时递增投递计数?

触发器acquired-count 递增delivery-count 递增(投递尝试失败
AMQP 1.0 released disposition
AMQP 1.0 rejected disposition
AMQP 1.0 modified disposition (delivery-failed=false)
AMQP 1.0 modified disposition (delivery-failed=true)
AMQP 0.9.1 basic.nack
AMQP 0.9.1 basic.reject
客户端崩溃 / 连接丢失
集群内网络分区(怀疑消费者节点宕机)
消费者超时(消费者未及时确认)

配置限制

可以使用策略参数 delivery-limit 为队列设置投递限制。

-1 完全禁用该限制。建议用户禁用此限制,因为反复的重新排队可能会威胁队列或 RabbitMQ 集群的稳定性。

注意,自 4.3 起,投递计数(用于评估投递限制)在真正的故障(如会话/通道崩溃,或使用 delivery-failed=true 的 modified 结果,或 AMQP 0.9.1 中的 basic.reject)时才会递增。

覆盖限制

以下示例为名称以 qq 开头的队列将限制设置为 50。

rabbitmqctl set_policy qq-overrides \
"^qq\." '{"delivery-limit": 50}' \
--priority 123 \
--apply-to "quorum_queues"

禁用限制

以下示例为名称以 qq.unlimited 开头的队列禁用限制。

rabbitmqctl set_policy qq-overrides \
"^qq\.unlimited" '{"delivery-limit": -1}' \
--priority 123 \
--apply-to "quorum_queues"

配置限制并设置死信处理

重新投递次数超过限制的消息将被丢弃(删除)或进入死信队列

以下示例配置了限制和一个用于死信(重新发布)此类消息的交换器。此示例中的目标交换器称为 "redeliveries.limit.dlx"。声明它并设置其拓扑(绑定队列和/或流)不在本示例中介绍。

rabbitmqctl set_policy qq-overrides \
"^qq\." '{"delivery-limit": 50, "dead-letter-exchange": "redeliveries.limit.dlx"}' \
--priority 123 \
--apply-to "quorum_queues"

要了解有关死信处理的更多信息,请查阅其专门指南

策略支持

仲裁队列可以通过 RabbitMQ 策略进行配置。下表总结了它们所遵守的策略键。

定义键类型
max-length数字
max-length-bytes数字
overflow"drop-head" 或 "reject-publish"
expires数字(毫秒)
dead-letter-exchange字符串
dead-letter-routing-key字符串
delivery-limit数字
delayed-retry-type"disabled"、"all"、"failed" 或 "returned"
delayed-retry-min数字(毫秒)
delayed-retry-max数字(毫秒)
consumer-timeout数字(毫秒)
consumer-disconnected-timeout数字(毫秒)

不支持的功能

瞬态(非持久)队列

经典队列可以是非持久的。仲裁队列根据其假设的应用场景始终是持久的。

独占性

排他队列与其声明连接的生命周期绑定。仲裁队列在设计上是可复制且持久的,因此在它们的上下文中排他属性没有意义。因此仲裁队列不能是排他的。

仲裁队列不打算用作临时队列

全局 QoS

仲裁队列不支持全局 QoS 预取,即通道为使用该通道的所有消费者设置单个预取限制。如果尝试从开启了全局 QoS 的通道消费仲裁队列,将返回通道错误。

请使用消费者级 QoS 预取,这是许多流行客户端的默认设置。

用法

仲裁队列与其他队列类型共享大多数基本原理。任何可以在声明时指定可选队列参数的 AMQP 0.9.1 客户端库都可以使用仲裁队列。

首先,我们将介绍如何使用 AMQP 0.9.1 声明仲裁队列。

声明

要声明仲裁队列,请将 x-queue-type 队列参数设置为 quorum(默认是 classic)。此参数必须由客户端在声明队列时提供;不能使用策略设置或更改。这是因为策略定义可以动态更改,但队列类型不能。它必须在声明时指定。

声明 x-queue-type 参数为 quorum 的队列,将声明一个最多包含三个成员(默认组大小)的仲裁队列,每个集群节点一个。

例如,一个由三个节点组成的集群将有三个成员,每个节点一个。在由五个节点组成的集群中,三个节点各有一个成员,但两个节点不承载任何成员。

声明后,仲裁队列可以像任何其他 RabbitMQ 队列一样绑定到任何交换器。

如果使用 管理 UI 声明,则必须使用队列类型下拉菜单指定队列类型。

客户端操作

以下操作对于仲裁队列和经典队列的工作方式相同:

某些队列操作存在细微差异:

组大小和成员管理

声明仲裁队列时,必须配置初始成员数量(即“组大小”)。RabbitMQ 默认将其设置为 3,这是提供高可用的实际最小成员数。

仲裁队列的成员绝不会共享同一个 RabbitMQ 节点,因此要在声明时创建一个 3 成员的仲裁队列,至少需要 2 个 RabbitMQ 节点处于在线状态。

控制初始组大小

例如,一个三节点集群将有三个成员,每个节点一个。在七节点集群中,三个节点各有一个成员,但其余四个节点将不承载新声明队列的任何成员。

可以使用 x-quorum-initial-group-size 队列参数为仲裁队列配置组大小(初始成员数)。提供的组大小参数应为大于 0 且小于或等于当前配置的 RabbitMQ 集群大小的整数。

x-quorum-initial-group-size 的实际最小值为 3,这将支持 1 个 RabbitMQ 节点的故障。

对于 RabbitMQ 集群大小,强烈建议仲裁队列组大小为奇数。

有关仲裁语义的更多详细信息,请参阅 容错和在线的最少成员数 部分。

注意:如果 RabbitMQ 节点在声明期间不可用且被选中托管仲裁队列成员,则该成员将在节点下次启动时启动。

管理成员

仲裁队列的副本由操作员显式管理。当新节点添加到集群时,除非操作员显式为仲裁队列添加成员,否则它不会托管任何仲裁队列成员。

当节点必须退役(永久从集群中删除)时,forget_cluster_node 命令将自动尝试删除退役节点上的所有仲裁队列成员。或者,可以在移除节点之前使用下面的 shrink 命令将任何成员移动到新节点。

另请参阅 持续成员调解,了解扩展仲裁队列的更自动化方式。

提供了几个 CLI 命令来执行上述操作。

rabbitmq-queues add_member [-p <vhost>] <queue-name> <node>
rabbitmq-queues delete_member [-p <vhost>] <queue-name> <node>
rabbitmq-queues grow <node> <all | even> [--vhost-pattern <pattern>] [--queue-pattern <pattern>]
rabbitmq-queues shrink <node> [--errors-only]

要成功添加和删除成员,必须有一个仲裁数量的队列成员可用,因为成员变更被视为队列状态变更。

执行涉及成员变更的维护操作时,需要小心不要因为失去仲裁而意外导致队列不可用。

在替换集群节点时,更安全的方法是先添加一个新节点,扩展需要在新节点上拥有成员的任何仲裁队列,然后再退役被替换的节点。

队列领导者位置

仲裁队列上的所有操作(状态变更)都发送给主要成员(称为领导者),该成员进而将操作复制给其余成员(称为跟随者)。

为了避免集群中的某些节点托管大部分队列领导者成员从而处理大部分负载,如果队列领导者能够均匀分布在集群节点上是有益的。

声明新仲裁队列时,承载其成员的节点集是随机选择的,但总是包括声明队列的客户端所连接的节点。

哪个成员成为初始领导者可以通过三个选项进行控制:

  1. 设置 queue-leader-locator 策略键(推荐)
  2. 配置文件中定义 queue_leader_locator 键(推荐)
  3. 使用 x-queue-leader-locator 可选队列参数

支持的队列领导者定位器值为:

  • client-local:选择声明队列的客户端所连接的节点。这是默认值。
  • balanced:如果总队列数少于 1000 个(经典队列、仲裁队列和流),选择托管仲裁队列领导者数量最少的节点。如果总队列数超过 1000 个,则随机选择一个节点。

重平衡成员

声明后或集群重启后,RabbitMQ 仲裁队列领导者可能会在 RabbitMQ 集群中分布不均。

要进行重平衡,请使用 rabbitmq-queues rebalance 命令。

重要的是要知道这不会改变仲裁队列跨越的节点。要修改成员资格,请参阅管理成员

# rebalances all quorum queues
rabbitmq-queues rebalance quorum

可以重平衡按名称选择的队列子集

# rebalances a subset of quorum queues
rabbitmq-queues rebalance quorum --queue-pattern "orders.*"

或特定虚拟主机集中的仲裁队列

# rebalances a subset of quorum queues
rabbitmq-queues rebalance quorum --vhost-pattern "production.*"

持续成员调解 (CMR)
重要

持续成员调解 (CMR) 功能是显式成员管理的补充,而非替代。在某些节点被永久从集群中移除的情况下,显式移除仲裁队列成员可能仍然是必要的。

除了通过初始目标大小和显式成员管理来控制仲裁队列成员外,还可以通过启用持续成员调解功能,配置节点自动尝试将仲裁队列成员资格增加到配置的目标组大小。

激活后,每个仲裁队列领导者将定期检查其当前的成员组成员数量(配置的成员数),并将其与目标值进行比较。

如果队列低于目标值,RabbitMQ 将尝试将队列扩展到当前不承载该队列成员的可用节点(如果有)上,直至达到目标值。

何时触发持续成员调解?

默认调解间隔为 60 分钟。此外,自动调解由集群中的某些事件触发,例如添加新节点、永久移除节点或更改仲裁队列相关策略。

警告

请注意,节点或仲裁队列成员故障不会触发自动成员调解。

如果节点以不可恢复的方式故障且无法恢复,则必须将其从集群中显式移除,或者操作员必须选择并启用 quorum_queue.continuous_membership_reconciliation.auto_remove 设置。

这也意味着升级不会触发自动成员调解,因为预计节点会恢复,且升级时通常一次只会停止少数(通常只有一个)节点。

CMR 配置

通过 rabbitmq.conf
持续成员调解 (CMR) 设置
rabbitmq.conf 配置键描述

quorum_queue.continuous_membership_reconciliation.enabled

启用或禁用持续成员调解。

  • 数据类型:布尔值
  • 默认:false

quorum_queue.continuous_membership_reconciliation.target_group_size

队列成员的目标成员数(组大小)。

  • 数据类型:正整数
  • 默认:无

quorum_queue.continuous_membership_reconciliation.auto_remove

启用或禁用自动移除不再属于集群但仍是仲裁队列成员的成员节点。

  • 数据类型:布尔值
  • 默认:false

quorum_queue.continuous_membership_reconciliation.interval

默认评估间隔(毫秒)。

  • 数据类型:正整数
  • 默认:3600000(60 分钟)

quorum_queue.continuous_membership_reconciliation.trigger_interval

reconciliation 延迟(毫秒),在发生触发事件(例如节点被添加或从集群中移除,或适用策略发生变更)时使用。此延迟将仅应用一次,之后将使用常规间隔。

  • 数据类型:正整数
  • 默认:10000(10 秒)
策略键
策略驱动的 CMR 设置
策略键描述

target-group-size

定义匹配队列的目标成员数(组大小)。此策略可由用户和操作员设置。

  • 数据类型:正整数
  • 默认:无
可选参数驱动的 CMR 设置
可选参数键描述

x-quorum-target-group-size

定义匹配队列的目标成员数(组大小)。此键可被操作员策略覆盖。

  • 数据类型:正整数
  • 默认:无

仲裁队列行为

仲裁队列依赖于名为 Raft 的共识协议来确保数据一致性和安全性。

每个仲裁队列都有一个主要成员(Raft 术语中的领导者)和零个或多个辅助成员(称为跟随者)。

当集群首次形成时以及随后领导者不可用时,会选举领导者。

领导者选举和故障处理

仲裁队列需要已声明节点中的仲裁数量可用才能运行。当托管仲裁队列领导者的 RabbitMQ 节点故障或停止时,托管该仲裁队列跟随者的另一个节点将被选举为领导者并恢复接受操作。

故障并重新加入的跟随者将自动从它们离开的点恢复日志复制,这意味着无需特殊的“追赶”过程,也不会像经典镜像队列那样影响领导者的可用性。

当添加新成员时,将执行相同的日志复制过程,同样对仲裁队列集群的其余部分影响极小。

容错和在线的最少成员数

共识系统可以在数据安全方面提供某些保证。这些保证确实意味着在它们变得相关之前需要满足某些条件,例如需要至少三个集群节点来提供容错能力,并且需要超过一半的成员可用才能工作。

不同大小集群的故障容错特性可以用表格描述:

集群节点数可容忍的节点故障数容忍网络分区的能力
10不适用
20
31
41是,如果多数在其中一侧存在
52
62是,如果多数在其中一侧存在
73
83是,如果多数在其中一侧存在
94

如上表所示,少于三个节点的 RabbitMQ 集群不能完全受益于仲裁队列的保证。具有偶数个 RabbitMQ 节点的集群不会从将仲裁队列成员分散在所有节点上受益。对于这些系统,仲裁队列大小应限制在较小的奇数个节点上。

对于大于 5 的仲裁队列节点大小,性能会大幅下降。我们不建议在超过 7 个 RabbitMQ 节点上运行仲裁队列。默认仲裁队列大小为 3,并可使用 x-quorum-initial-group-size 队列参数进行控制。

数据安全

仲裁队列旨在在网络分区和故障场景下提供数据安全。只要至少托管仲裁队列的 RabbitMQ 节点的大多数没有永久不可用,使用 发布者确认 功能成功确认回发布者的消息就不应该丢失。

通常,仲裁队列优先考虑数据一致性而非可用性。

重要

仲裁队列无法为尚未确认给发布者的消息提供任何安全保证。此类消息可能在“传输中”、在操作系统缓冲区中丢失,或未能到达目标节点或队列领导者。

仲裁队列通过了重构且要求更严格版本原始 Jepsen 测试。这确保了它们在网络分区和故障场景下表现如预期。新测试持续运行以发现可能的回归,并定期增强以测试新功能(例如死信处理)。

可用性

仲裁队列应该能够容忍少数队列成员不可用,而对可用性几乎没有影响。

请注意,根据所使用的分区处理策略,RabbitMQ 可能会在恢复期间自行重启并重置节点,但只要不发生这种情况,此可用性保证就应成立。

例如,具有三个成员的队列可以容忍一个节点故障而不损失可用性。具有五个成员的队列可以容忍两个,依此类推。

如果无法恢复仲裁数量的节点(例如 3 个 RabbitMQ 节点中有 2 个永久丢失),则队列将永久不可用,需要进行强制删除并重新创建。

性能特性
重要

仲裁队列在 RabbitMQ 4.1.x 中得到了进一步优化,现在具有更小的内存占用和改进的消费者投递率、峰值负载下的并行性

仲裁队列旨在用延迟换取吞吐量,并已在 3、5 和 7 节点配置以及多种不同消息大小下进行了测试。

在同时使用消费者确认和发布者确认的场景中,观察到仲裁队列的吞吐量优于经典镜像队列(2021 年弃用,2024 年 RabbitMQ 4.0 中移除)。例如,请查看这些使用 3.10 的基准测试以及另一个使用 3.12 的测试

由于仲裁队列在做任何事情之前都会将所有数据持久化到磁盘,因此建议使用尽可能快的磁盘和特定的性能调优设置。

仲裁队列还受益于使用更高预取值的消费者,以确保消费者不会在确认流经系统时被饿死,并允许消息及时投递。

由于仲裁队列的磁盘 I/O 密集型性质,其吞吐量会随着消息大小的增加而降低。

仲裁队列的吞吐量也受到成员数量的影响。仲裁队列的成员越多,其吞吐量通常越低,因为需要做更多的工作来复制数据和达成共识。

从 RabbitMQ 3.13.x 镜像经典队列使用蓝绿部署迁移到仲裁队列

rabbitmqadmin v2 提供了命令,用于自动化从使用镜像经典队列的 3.13.x 集群迁移到使用蓝绿部署策略的仲裁队列。

参见 使用蓝绿升级策略将镜像经典队列迁移到仲裁队列

可配置设置

有一些新的配置参数可以使用高级配置文件进行调整。

请注意,所有与资源占用相关的设置都在单独的部分中记录。

ra 应用程序(仲裁队列使用的 Raft 库)有其自己的一组可调参数

rabbit 应用程序有几个与仲裁队列相关的配置项可用。

advanced.config 配置键描述默认值
rabbit.quorum_cluster_size

设置默认仲裁队列集群大小(可在声明时被 x-quorum-initial-group-size 队列参数覆盖)。

3
rabbit.quorum_commands_soft_limit

这是一个与流控制相关的参数,定义了通道进入流控制前接受的最大未确认消息数。当前默认值旨在在多个发布者发送到同一个仲裁队列时提供良好的性能和稳定性。如果应用程序通常每个队列只有一个发布者,则可以增加此限制以提供更好的入口速率。

32
rabbit.consumer_timeout

所有仲裁队列的全局默认消费者超时(毫秒)。可被策略或队列参数覆盖。设置为 undefined 以禁用。

undefined
rabbit.consumer_disconnected_timeout

当消费者节点由于网络分区而无法访问时返回消息的超时(毫秒)。超时后,消息返回到队列。

60000

rabbitmq.conf 中,可以这样配置:

consumer_timeout = 1800000
consumer_disconnected_timeout = 60000

仲裁队列配置示例

以下 rabbitmq.conf 示例展示了遗留和新的配置选项:

quorum_queue.initial_cluster_size = 3

quorum_queue.commands_soft_limit = 32

consumer_timeout = 1800000

consumer_disconnected_timeout = 60000

高级仲裁队列配置

从 RabbitMQ 4.3 开始,所有仲裁队列 Ra 系统参数都可以通过 rabbitmq.conf 使用 quorum_queue.* 命名空间显式配置。这允许操作员专门针对仲裁队列微调 Raft 日志和 WAL 行为,而不会影响其他基于 Raft 的功能(如流或 Khepri)。

警告

这些配置选项用于高级用例,除非 RabbitMQ 核心团队成员或支持人员建议,否则通常应由用户更改。

rabbitmq.conf描述默认值
quorum_queue.segment_max_size_bytesRaft 日志段文件的最大大小64000000 (64 MB)
quorum_queue.segment_max_entriesRaft 日志段文件中的最大条目数4096
quorum_queue.wal_max_size_bytes预写日志 (WAL) 的最大大小536870912 (512 MB)
quorum_queue.wal_max_entries预写日志中的最大条目数500000
quorum_queue.wal_max_batch_sizeWAL 写入的最大批次大小4096
quorum_queue.wal_compute_checksums是否为 WAL 条目计算校验和true
quorum_queue.segment_compute_checksums是否为段条目计算校验和true
quorum_queue.max_append_entries_rpc_batch_size追加条目 RPC 的最大批次大小16
quorum_queue.compress_mem_tables是否压缩内存表true
quorum_queue.snapshot_chunk_size快照传输的块大小1000000 (1 MB)

放宽属性等效性检查的选项

当客户端重新声明队列时,RabbitMQ 节点执行属性等效性检查。如果某些属性不等效,声明将失败并返回通道错误

在应用程序通过 x-queue-type 参数显式设置队列类型且无法快速更新和/或重新部署的环境中,可以通过选择性设置忽略 x-queue-type 的等效性检查:

quorum_queue.property_equivalence.relaxed_checks_on_redeclaration = true

如果设置 quorum_queue.property_equivalence.relaxed_checks_on_redeclarationtrue,则在队列重新声明时将忽略 'x-queue-type' 头部(不进行等效性比较)。

这可以简化那些由于历史原因将 'x-queue-type' 显式设置为 'classic' 但未设置任何其他可能冲突或显著改变队列行为和语义(如 'exclusive' 字段)的属性的应用程序的升级。

资源使用
重要

仲裁队列在 RabbitMQ 4.1.x 中得到了进一步优化,现在具有更小的内存占用和改进的消费者投递率、峰值负载下的并行性

仲裁队列针对数据安全性和性能进行了优化。每个仲裁队列进程都维护一个队列中消息的内存索引,每条消息至少需要 32 字节的元数据(如果消息被返回或设置了 TTL,则需要更多)。因此,仲裁队列进程每 30000 条消息至少使用 1MB(消息大小无关紧要)。您可以根据队列数量以及队列中预期或最大消息数量进行粗略计算。保持队列短小是维持低内存使用的最佳方法。为所有队列设置最大队列长度是限制总内存使用(如果队列因任何原因变长)的好方法。

内存、WAL 和段文件如何交互

给定节点上的仲裁队列共享一个用于所有操作的预写日志 (WAL)。WAL 包含队列上有限数量的近期操作。WAL 条目既存储在内存中,也写入磁盘。当当前 WAL 文件达到预定义限制时,它会为节点上的每个仲裁队列成员刷新到磁盘上的段文件,系统将开始释放该批日志条目所使用的内存。

随着消费者确认投递,段文件会随时间进行压缩。压缩是回收磁盘空间的过程。

WAL 文件刷新到磁盘的大小限制是可以控制的。

# Flush current WAL file to a segment file on disk once it reaches 64 MiB in size
raft.wal_max_size_bytes = 64000000

该值默认为 512 MiB。这意味着在稳定负载下,WAL 表的内存占用可能会达到 512 MiB。

重要

由于运行时不保证内存会被立即解除分配,我们建议 RabbitMQ 节点分配至少 3 倍于有效 WAL 文件大小限制的内存。在高吞吐量系统中需要更多。对于那些系统,4 倍是一个很好的起点。

仲裁队列的内存占用模式通常如下所示:

Quorum Queues memory usage pattern

日志压缩

仲裁队列维护所有操作(入队、确认等)的日志。随着时间的推移,随着消费者确认消息,大部分此类日志数据变得陈旧,可以被回收。

从 RabbitMQ 4.3 开始,仲裁队列即使在以下情况下也可以回收磁盘空间:

  • 消息被无序确认
  • 部分消息在队列中保留较长时间(例如,由慢消费者导致,或作为延迟重试被持有)
  • 确认到达的速率不一致

以往,单条未确认的消息就可能导致日志无法截断,从而引发磁盘空间无限制增长。在 Ra v3 的新日志压缩机制中,状态机跟踪一个活跃 Raft 索引列表(即仍未确认或因其他原因被需要的消息)。当进行快照时,日志会进行压缩

  1. 轻量压缩 (Minor Compaction):在每次快照后运行。它识别并删除包含零条活跃记录的整个段文件。
  2. 重量压缩 (Major Compaction):定期在后台运行。它通过仅将剩余的活跃条目复制到新的、较小的段中并删除旧段,来整合多个稀疏的段文件。

这允许已确认消息的数据从快照中被忽略并从磁盘移除,从而减小快照大小和写入放大,并安全地回收磁盘空间。

有关压缩架构的更多技术细节,请参阅 Ra 日志压缩文档

快照限流

为防止过度的磁盘 I/O,快照频率会根据预写日志 (WAL) 的填充率进行智能限流。这确保每个队列在每个 WAL 周期内大约进行一次快照,从而优化浅层快速流动队列和深层队列的性能。

磁盘空间

重要

在大量使用仲裁队列和/或流经大量消息的环境中,超额配置磁盘空间(即预留额外的备用容量)非常重要。这遵循现代发布版本中关于存储的一般性建议

对于大型消息(例如 1 MiB 及以上),仲裁队列的磁盘占用量可能很大。虽然 RabbitMQ 4.3 显著改进了日志压缩,但从段文件中删除过时数据仍需要后台处理。

这引出了几点建议

  1. 在大量使用仲裁队列和/或流经大型消息的环境中,超额配置磁盘空间(即预留额外的备用容量)非常重要
  2. 虽然仲裁队列不再严格要求消费者及时确认消息以防止磁盘空间无限制增长,但仍建议设置合理的低投递确认超时,以减少在重量压缩期间复制未确认消息带来的写入放大开销
  3. 考虑使用延迟重试来管理带退避策略的消息处理,以减少快速重复投递的循环
  4. 作为替代方案,大型消息可以存储在 Blob 存储中,相关元数据则通过流经仲裁队列的消息进行传递

重复重入队列投递(投递-重入队列循环)

当消费者遇到瞬时错误或无法处理某条消息时,可能会发生同一消息的重复重入队列(即消费者持续拒绝或 nack 消息并将 requeue 标志设为 true)。RabbitMQ 提供了保障机制来防止这种模式引发问题。

内置保护机制

从 RabbitMQ 4.0 开始,仲裁队列基于 delivery-count 强制执行默认 delivery-limit 为 20。当消息的投递计数超过此限制时,该消息会被丢弃或进入死信队列(如果配置了 DLX)。

然而,需要注意的是,重入队列操作可能不会增加投递计数。不增加计数器的重入队列(例如通过 nack 返回,或 AMQP 1.0 的 modify 并设置 delivery_failed=false)不会计入投递限制,从而允许应用层路由逻辑进行无限的重入队列循环。只有实际的投递失败(例如通过 reject,AMQP 1.0 的 modify 并设置 delivery_failed=true,或者当通道/会话终止且存在挂起的未确认消息时)才会增加投递计数。

与其依赖投递限制来捕获异常消息,不如使用延迟重试来实现智能退避

  • 自动退避:延迟基于投递计数线性应用,在发生瞬时错误时降低服务器负载
  • 可配置行为:选择延迟所有返回、仅延迟失败投递或仅延迟显式返回
  • 更好的可观测性:在管理界面中监控延迟消息计数,以识别有问题的消费者
  • 灵活恢复:使用超时机制或重试策略优雅地进行恢复

这可以防止快速重入队列循环使队列不堪重负,并允许更高效地进行日志压缩。

消息返回行为(RabbitMQ 4.3+)

从 RabbitMQ 4.3 开始,投递限制显式基于 delivery-count 而非 acquired-count。这意味着

  • 允许无限次的显式返回(通过 nack 或 AMQP 1.0 的 modify 并设置 delivery_failed=false),且不计入投递限制
  • 只有实际的重新投递(通过 reject、AMQP 1.0 的 modify 并设置 delivery_failed=true,或因通道/会话终止且存在挂起消息时)才会增加计数器
  • 这为应用层的消息路由和处理逻辑提供了灵活性

禁用投递限制

可以通过将 x-delivery-limit=-1 设置为队列参数来禁用默认投递限制

x-delivery-limit = -1

这仅在极少数情况下为了与 RabbitMQ 3.13.x 行为兼容时推荐使用。现代部署应改为使用延迟重试

原子(Atom)使用增加

仲裁队列的内部实现将队列名称转换为 Erlang 原子。如果持续创建和删除具有任意名称的队列,当原子表大小达到 500 万的默认限制时,可能会威胁 RabbitMQ 系统的长期稳定性。

虽然仲裁队列的设计并非用于高流失率环境(非镜像经典队列是此类场景的最佳选择),但如果确实有必要,可以增加该限制。

请参阅运行时指南以了解更多信息。

性能调优

重要

仲裁队列在 RabbitMQ 4.1.x 中得到了进一步优化,现在具有更小的内存占用和改进的消费者投递率、峰值负载下的并行性

本节旨在介绍一些可能提高部分工作负载下仲裁队列吞吐量的可调参数。对于其他工作负载,使用这些设置可能不会看到任何提升,甚至可能导致吞吐量下降。

将此处的值和建议作为起点,并进行您自己的基准测试(例如使用 PerfTest),以确定哪种参数组合最适合特定的工作负载。

调优:Raft 段文件条目计数

注意

此调优参数不再推荐使用。段现在具有大小限制(由 quorum_queue.segment_max_size_bytes 控制,默认值为 64000000 字节或 64 MB),这是段管理的主要因素。增加每个段的条目数可能会对日志压缩效率产生负面影响。

在现代 RabbitMQ 版本中,段文件大小是性能和压缩的主要限制因素。手动调优条目计数可能会导致段在符合压缩条件之前变得非常大,从而可能减慢过时数据的清除速度。

如果您需要控制仲裁队列的 Raft 日志段文件的最大大小,请改为调整 quorum_queue.segment_max_size_bytes 配置。

如果您目前在生产环境中使用自定义的 raft.segment_max_entries 设置,请考虑将其移除并依赖默认值和基于大小的限制。

调优:Linux 预读取 (Readahead)

此外,上述具有较高小消息处理速率的工作负载可能会受益于更高的 readahead,这是 Linux 上存储设备的一个可配置块设备参数。

要检查有效的 readahead 值,请使用 blockdev --getra 并指定托管 RabbitMQ 节点数据目录的块设备

# This is JUST AN EXAMPLE.
# The name of the block device in your environment will be different.
#
# Displays effective readahead value device /dev/sda.
sudo blockdev --getra /dev/sda

要配置 readahead,请对托管 RabbitMQ 节点数据目录的块设备使用 blockdev --setra

# This is JUST AN EXAMPLE.
# The name of the block device in your environment will be different.
# Values between 256 and 4096 in steps of 256 are most commonly used.
#
# Sets readahead for device /dev/sda to 4096.
sudo blockdev --setra 4096 /dev/sda
© . This site is unofficial and not affiliated with VMware.