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