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