队列
什么是队列?
RabbitMQ 中的队列是消息的有序集合。消息以 FIFO(“先进先出”)方式入队和出队(传递给消费者)。(FIFO ("first in, first out") 方式。)
从广义上定义 队列,它是一种顺序数据结构,具有两个主要操作:项目可以在尾部 入队 (添加),并在头部 出队 (消费)。
队列在消息传递技术领域中起着重要作用。许多消息传递协议和工具都假定 发布者 和 消费者 使用类似队列的存储机制进行通信。
消息传递系统中的许多功能都与队列相关。一些 RabbitMQ 队列功能(例如优先级和消费者 重新排队)可能会影响消费者观察到的顺序。
本主题中的信息包括 RabbitMQ 中队列的概述,并链接到其他主题,以便您可以了解有关在 RabbitMQ 中使用队列的更多信息。
除了队列之外,现代 RabbitMQ 版本还支持两种替代数据结构,称为 流和超级流。
本指南主要介绍 AMQP 0-9-1 协议上下文中的队列,但是,大部分内容适用于其他受支持的协议。
某些协议(例如:STOMP 和 MQTT)是围绕主题的概念构建的。对于这些协议,队列充当消费者的数据累积缓冲区。但是,仍然重要的是要了解队列所起的作用,因为即使对于这些协议,许多功能仍在队列级别上运行。
流 是 RabbitMQ 中可用的另一种消息传递数据结构。流提供与队列不同的功能。
本主题中涵盖的有关 RabbitMQ 队列的信息包括
- 队列名称
- 队列属性
- 队列中的消息排序
- 队列持久性及其与消息持久性的关系
- 复制队列类型
- 客户端的透明操作路由
- 临时 队列和 独占 队列
- 队列副本的运行时资源 使用情况
- 可选队列参数(“x-arguments”)
- 声明和 属性等效性
- 队列指标
- TTL 和长度限制
- 优先级队列
有关消费者相关主题,请参阅《消费者指南》。经典队列、quorum 队列 和 流 也有专门的指南。
队列名称
队列具有名称,以便应用程序可以引用它们。
应用程序可以选择队列名称,或请求 Broker 为其 生成名称。队列名称最多可以包含 255 个字节的 UTF-8 字符。
以“amq.”开头的队列名称保留供 Broker 内部使用。尝试声明名称违反此规则的队列将导致 信道级异常,回复代码为 403 (ACCESS_REFUSED
)。
服务器命名队列
在 AMQP 0-9-1 中,Broker 可以代表应用程序生成唯一的队列名称。要使用此功能,请传递一个空字符串作为队列名称参数:通过在同一信道中使用空字符串(在期望队列名称的地方),可以在同一信道中的后续方法中获得相同的生成名称。这是可行的,因为信道会记住最后一个服务器生成的队列名称。
服务器命名队列旨在用于本质上是瞬态的且特定于特定消费者(应用程序实例)的状态。应用程序可以在消息元数据中共享此类名称,以使其他应用程序能够响应它们(如 教程六 所示)。否则,服务器命名队列的名称应仅由声明应用程序实例知道和使用。实例还应为队列设置适当的绑定(路由),以便发布者可以使用众所周知的 交换机,而不是直接使用服务器生成的队列名称。
队列属性
队列具有定义其行为的属性。有一组强制属性和一组可选属性的映射
- 名称
- 持久(队列将在 Broker 重启后幸存)
- 独占(仅由一个连接使用,并且当该连接关闭时队列将被删除)
- 自动删除(至少有一个消费者的队列在最后一个消费者取消订阅时被删除)
- 参数(可选;由插件和 Broker 特定功能使用,例如消息 TTL、队列长度限制等)
请注意,并非所有属性组合在实践中都有意义。例如,自动删除和独占队列应为 服务器命名。此类队列应供客户端特定或连接(会话)特定数据使用。
当自动删除或独占队列使用众所周知的(静态)名称时,在客户端断开连接并立即重新连接的情况下,将在 RabbitMQ 节点(将删除此类队列)和尝试重新声明它们的恢复客户端之间存在自然的竞争条件。这可能导致客户端连接恢复失败或异常,并造成不必要的混乱或影响应用程序的可用性。
声明和属性等效性
特别是对于队列类型属性,可以放宽属性等效性检查。或者,可以配置默认队列类型 (DQT)。
队列在使用前必须声明。声明队列将导致在队列尚不存在时创建它。如果队列已存在且其属性与声明中的属性相同,则声明将不起作用。当现有队列属性与声明中的属性不同时,将引发代码为 406 (PRECONDITION_FAILED
) 的信道级异常。
特别是对于队列类型属性,可以放宽属性等效性检查,或者配置为使用默认值。
请参阅《虚拟主机指南》以了解更多信息。
可选参数
可选队列参数,也称为“x-arguments”,因为它们在 AMQP 0-9-1 协议中的字段名称,是一个任意键/值对的映射(字典),客户端在声明队列时可以提供这些参数。
该映射由各种功能和插件使用,例如
等等。
相同的想法也用于其他协议操作,例如,注册消费者时
一些可选参数在队列声明时设置,并在队列的整个生命周期内保持不变。其他参数可以在队列声明后通过 策略 动态更改。
对于可以通过 策略 设置的键,请始终首先考虑使用策略,而不是在应用程序代码中设置这些值
例如,队列类型 (x-queue-type
) 和最大 队列优先级 数 (x-max-priority
) 必须在队列声明时设置,之后无法更改。
可选队列参数可以以不同的方式设置
前一种选项更灵活、非侵入性,不需要应用程序修改和重新部署。因此,强烈建议大多数用户使用。请注意,某些可选参数(例如队列类型或最大优先级数)只能由客户端提供,因为它们无法动态更改,并且必须在声明时已知。
客户端库之间提供可选参数的方式各不相同,但通常是声明队列的函数(方法)的 durable
、auto_delete
和其他参数旁边的参数。
可选参数和策略定义的键优先级
当客户端提供的 x-arguments
和 策略 都提供相同的键时,前者优先。
但是,如果也使用了 操作员策略,则操作员策略也将优先于客户端提供的参数。操作员策略是一种保护机制,它会覆盖客户端提供的值和用户策略值。
对于数值,例如 最大队列长度 或 TTL,将使用两者中的较小值。如果应用程序需要或选择使用较低的值,则操作员策略将允许这样做。但是,不能使用高于操作员策略中定义的值。
使用操作员策略为与资源使用相关的应用程序控制参数引入护栏(例如,峰值磁盘空间使用率)。
RabbitMQ 中的消息排序
RabbitMQ 中的队列是消息的有序集合。消息以 FIFO 方式 入队和出队(传递给消费者)。
根据定义,FIFO 排序对于 优先级 队列不保证。
排序也可能受到多个竞争 消费者、消费者优先级、消息重传的影响。这适用于任何类型的重传:信道关闭后的自动重传和 消费者否定确认。
应用程序可以假设在单个信道上发布的消息将按发布顺序在它们路由到的所有队列中入队。当发布发生在多个连接或信道上时,它们的消息序列将并发路由和交错。
消费应用程序可以假设,到单个消费者的 初始传递 (其中 redelivered
属性设置为 false
的传递)以与入队时相同的 FIFO 顺序执行。对于 重复传递 (redelivered
属性设置为 true
),原始排序可能会受到消费者确认和重传时间的影响,因此无法保证。
在有多个消费者的情况下,消息将以 FIFO 顺序出队以进行传递,但实际传递将发生在多个消费者身上。如果所有消费者都具有相同的优先级,则将以 轮询方式 选择它们。只会考虑信道上未超过其 预取值 (未完成的未确认传递的数量)的消费者。
持久性
队列可以是持久的或瞬态的。持久队列的元数据存储在磁盘上,而瞬态队列的元数据在可能的情况下存储在内存中。在某些协议(例如 AMQP 0-9-1 和 MQTT)中,发布时的消息 也做了同样的区分。
在持久性很重要的环境和用例中,应用程序必须使用持久队列,并确保 发布者 将发布的消息标记为持久。
瞬态队列将在节点启动时删除。因此,根据设计,它们无法在节点重启后幸存。瞬态队列中的消息也将被丢弃。
持久队列将在节点启动时恢复,包括其中发布为持久的消息。发布为瞬态的消息将在恢复期间被 丢弃 ,即使它们存储在持久队列中也是如此。
如何选择
在大多数其他情况下,持久队列是推荐的选择。对于 复制队列,唯一合理的选择是使用持久队列。
在大多数情况下,队列的吞吐量和延迟不受队列是否持久的影响。只有队列或绑定变更率非常高的环境(即,队列每秒被删除和重新声明数百次或更多次)才会看到某些操作(即绑定)的延迟改进。因此,持久队列和瞬态队列之间的选择取决于用例的语义。
对于具有瞬态客户端的工作负载,临时队列可能是一个合理的选择,例如,用户界面、移动应用程序和预期会离线或使用切换身份的设备中的临时 WebSocket 连接。此类客户端通常具有固有的瞬态状态,应在客户端重新连接时替换。
某些队列类型不支持瞬态队列。例如,由于底层复制协议的假设和要求,Quorum 队列 必须是持久的。
临时队列
对于某些工作负载,队列应该是短期的。虽然客户端可以在断开连接之前删除他们声明的队列,但这并不总是方便的。最重要的是,客户端连接可能会失败,可能会留下未使用的资源(队列)。
RabbitMQ 支持许多队列属性,这些属性对于本质上是瞬态或客户端特定的数据很有意义。其中一些设置可以应用于持久队列,但并非所有组合都有意义。
考虑对临时队列使用 服务器生成的名称。由于此类队列不应在 N 个消费者之间共享,因此使用唯一名称是有意义的。
共享临时队列可能导致 RabbitMQ 节点操作和恢复客户端之间出现自然的竞争条件。
有三种方法可以使队列自动删除
- 独占队列(下面介绍)
- TTL(下面也介绍)
- 自动删除队列
当自动删除队列的最后一个消费者被取消(例如,在 AMQP 0-9-1 中使用 basic.cancel
)或消失(信道或连接关闭,或与服务器的 TCP 连接丢失)时,它将被删除。
如果队列从未有过任何消费者,例如,当所有消费都 使用轮询 时,它不会自动删除。对于这种情况,请使用独占队列或队列 TTL。
独占(客户端连接特定)队列
独占队列只能由其声明连接使用(从中消费、清除、删除等)。此类队列根据定义本质上是 临时 的,在持久队列上设置 exclusive
属性没有逻辑意义,因为此类队列无法在其声明连接后幸存下来,因此在节点重启的情况下无法满足其持久性属性。
声明为独占的队列将始终声明为经典队列:独占 quorum 队列 和 流 没有逻辑意义,因为它们的生命周期将绑定到特定客户端连接的生命周期,从而绑定到单个节点(或应用程序实例)。
考虑对独占队列使用 服务器生成的名称。由于此类队列不能在 N 个消费者之间共享,因此使用服务器生成的名称最有意义。
尝试从不同的连接使用独占队列将导致信道级异常 RESOURCE_LOCKED
,并显示错误消息,指出 无法获得对锁定队列的独占访问权
。
独占队列在其声明连接关闭或消失时(例如,由于底层 TCP 连接丢失)被删除。因此,它们仅适用于客户端特定的瞬态状态。
通常将独占队列设为服务器命名。
独占队列在“客户端本地”节点(声明队列的客户端连接到的节点)上声明,而与 queue_leader_locator
值无关。
复制队列和分布式队列
Quorum 队列 是复制的、面向数据安全性和一致性的队列类型。经典队列历史上支持复制,但此功能已在 RabbitMQ 4.x 中 删除 。
任何客户端 连接 都可以使用任何队列,无论它是复制的还是非复制的,也无论队列副本托管在哪个节点上或客户端连接到哪个节点。RabbitMQ 将为客户端透明地将操作路由到适当的节点。
例如,在具有节点 A、B 和 C 的集群中,连接到节点 A 的客户端可以从托管在 B 上的队列 Q 中消费,而连接到节点 C 的客户端可以以将消息路由到队列 Q 的方式发布。
客户端库或应用程序可以选择连接到托管特定队列的当前 leader 副本的节点,以提高数据局部性。
此通用规则适用于 RabbitMQ 支持的所有消息传递数据类型,但有一个例外。流 是此规则的例外,并且要求客户端(无论他们使用哪种协议)连接到托管目标流的副本(leader 或 follower)的节点。因此,RabbitMQ Stream 协议客户端将 并行连接到多个节点。
队列也可以在松散耦合的节点或集群之间进行 联邦 。
请注意,集群内复制和联邦是正交功能,不应被视为直接替代方案。
流 是 RabbitMQ 支持的另一种复制数据结构,具有一组不同的受支持操作和功能。
非复制队列和客户端操作
任何客户端 连接 都可以使用任何队列,包括非复制(单副本)队列,无论队列副本托管在哪个节点上或客户端连接到哪个节点。RabbitMQ 将为客户端透明地将操作路由到适当的节点。
例如,在具有节点 A、B 和 C 的集群中,连接到节点 A 的客户端可以从托管在 B 上的队列 Q 中消费,而连接到节点 C 的客户端可以以将消息路由到队列 Q 的方式发布。
客户端库或应用程序可以选择连接到托管特定队列的当前 leader 副本的节点,以提高数据局部性。
此通用规则适用于 RabbitMQ 支持的所有消息传递数据类型,但有一个例外。流 是此规则的例外,并且要求客户端(无论他们使用哪种协议)连接到托管目标流的副本(leader 或 follower)的节点。因此,RabbitMQ Stream 协议客户端将 并行连接到多个节点。
生存时间和长度限制
这两个功能都可以用于数据过期,并作为限制队列最多可以使用的资源(RAM、磁盘空间)量的一种方法,例如,当消费者离线或其吞吐量落后于发布者时。
在持久和内存存储中
在现代 RabbitMQ 版本中,quorum 队列和经典队列 v2 都积极地将数据移动到磁盘,并且仅在内存中保留相对较小的工作集。
在某些协议(例如 AMQP 0-9-1)中,客户端可以将消息发布为持久消息或瞬态消息。瞬态消息仍将存储在磁盘上,但在下次节点重启期间将被丢弃。
在 AMQP 0-9-1 中,这是通过消息属性(delivery_mode
或在某些客户端中为 persistent
)完成的。
关于该主题的其他相关指南有:《Quorum 队列》、《流》、《推理内存使用情况》、《警报》、《内存警报》、《可用磁盘空间警报》、《部署指南》和《消息存储配置》。
优先级
队列可以有 0 个或更多 优先级。此功能是选择加入的:只有通过可选参数(见上文)配置了最大优先级数的队列才会进行优先级排序。
发布者使用消息属性中的 priority
字段指定消息优先级。
如果需要优先级队列,我们建议使用 1 到 10 之间的优先级。当前使用更多优先级将消耗更多资源(Erlang 进程)。
CPU 利用率和并行性考虑因素
目前,单个队列副本(无论是 leader 还是 follower)在其热代码路径上都限制为单个 CPU 核心。因此,此设计假定大多数系统在实践中使用多个队列。
通常认为单个队列是一种反模式,而不仅仅是出于资源利用率的原因。
对于将队列吞吐量推向极限的工作负载,请考虑将 流或分区流 与 RabbitMQ Stream 协议客户端 一起使用。
指标和监控
RabbitMQ 收集有关队列的多个指标。其中大多数可通过 RabbitMQ HTTP API 和管理 UI 获得,它们专为监控而设计。这包括队列长度、入口和出口速率、消费者数量、各种状态的消息数量(例如,准备好交付或 未确认)、RAM 中与磁盘上的消息数量等等。
rabbitmqctl 可以列出队列和一些基本指标。
运行时指标(例如 VM 调度程序使用率、队列 (Erlang) 进程 GC 活动、队列进程使用的 RAM 量、队列进程邮箱长度)可以使用 rabbitmq-top 插件和管理 UI 中的各个队列页面进行访问。
消费者和确认
可以通过注册消费者(订阅)来消费消息,这意味着 RabbitMQ 会将消息推送到客户端,或者对于支持此功能的协议(例如 basic.get
AMQP 0-9-1 方法),可以单独获取消息,类似于 HTTP GET。
交付的消息可以由消费者显式或在交付写入连接套接字后立即自动确认。
自动确认模式通常会提供更高的吞吐率并使用更少的网络带宽。但是,在出现 故障 时,它提供的保证最少。作为经验法则,请首先考虑使用手动确认模式。
预取和消费者过载
自动确认模式也可能使无法像消息交付速度一样快速处理消息的消费者不堪重负。这可能会导致消费者进程的内存使用量永久增长和/或 OS 交换。
手动确认模式提供了一种限制未完成(未确认)交付数量的方法:信道 QoS(预取)。
使用较高(数千个或更多)预取级别的消费者可能会遇到与使用自动确认的消费者相同的过载问题。
大量未确认的消息将导致 Broker 的内存使用量增加。
消息状态
因此,入队的消息可以处于两种状态之一
- 准备好交付
- 已交付但尚未被 消费者确认
按状态的消息细分可以在管理 UI 中找到。
确定队列长度
可以通过多种方式确定队列长度
- 对于 AMQP 0-9-1,可以使用
queue.declare
方法响应 (queue.declare-ok
) 上的属性。字段名称为message_count
。其访问方式因客户端库而异。 - 使用 RabbitMQ HTTP API。
- 使用 rabbitmqctl
list_queues
命令。
队列长度定义为准备好交付的消息数量。
避免使用众所周知的名称的临时队列
非独占的 临时队列 可以由客户端命名并在多个消费者之间共享。但是,不建议这样做,并且可能导致 RabbitMQ 节点操作和客户端恢复之间出现竞争条件。
考虑以下场景
- 消费者使用具有众所周知名称的自动删除队列
- 客户端的连接失败
- 客户端检测到并启动连接恢复
由于失败的连接是自动删除队列上唯一的消费者,队列必须被 RabbitMQ 删除。此操作将需要一些时间,在此期间消费者可能会恢复。
然后根据操作的时序,队列可以是
- 由恢复中的客户端声明,然后被删除
- 被删除,然后重新声明
在第一种情况下,客户端将尝试在其消费者在并发删除的队列上重新注册,这将导致通道异常。
有两种解决这个基本竞争条件的方法
- 引入连接恢复延迟。例如,一些 RabbitMQ 客户端库默认使用 5 秒的连接恢复延迟。
- 使用服务器命名的队列,这完全避开了问题,因为新的客户端连接将使用与其前任不同的队列名称。