经典队列支持优先级
先决条件
本指南假设您熟悉 RabbitMQ 的基本知识
请参考以上指南。
什么是优先级队列
RabbitMQ 中的经典队列和仲裁队列支持优先级。队列的标准操作模式是 FIFO(先进先出)。这意味着,忽略预取、竞争消费者、重新入队和重新传递,RabbitMQ 会按照消息入队时的相同顺序将消息传递给消费者。
对于配置为使用优先级的队列,此标准行为会发生变化。为简洁起见,在本文档和 RabbitMQ 其他文档中,这类队列(包括经典队列和仲裁队列)统称为“优先级队列”。
优先级队列按消息优先级顺序传递消息。消息优先级是发布者在发布时设置的正整数值。
考虑一个队列中有三条消息 A、B 和 C,它们以相同的优先级入队到“常规”仲裁队列中。
| 消息 | 入队顺序 | 优先级 |
|---|---|---|
| A | 1 | 1 |
| B | 2 | 1 |
| C | 3 | 1 |
这些消息将按以下顺序分派(发送)给消费者(或多个消费者):A、B、C。
现在,考虑一个具有相同消息但优先级不同的优先级队列。
| 消息 | 入队顺序 | 优先级 |
|---|---|---|
| A | 1 | 1 |
| B | 2 | 3 |
| C | 3 | 2 |
与标准的 FIFO 传递行为不同,这些消息将按不同的顺序分派给消费者(或多个消费者):B、C、A,根据它们的优先级。
当优先级队列存在竞争消费者、重新入队传递或在消费者连接丢失或消费者应用程序失败时发生自动重新入队时,实际的传递顺序可能会略有不同。
采用优先级队列之前:考虑替代方案
优先级队列在消费者传递方面的行为比队列的标准 FIFO 行为更难理解,尤其是在消费者经常重新入队传递的环境中。因此,重要的是考虑一个更简单的替代方案是否更合适。
优先级队列通常用于避免队列系统中的一个经典问题——队头阻塞问题。然而,有几种可能的替代解决方案应该首先考虑。
- 使用多个队列而不是一个。单一巨型队列™是关于队列使用的最常见反模式之一。
- 对于单个队列上的竞争消费者,请使用具有不同预取值(大于 1)的独立通道,这样耗尽的预取就不会阻塞传递流。
- 使用流而不是队列。流提供不同的消费模式并支持重复消费。
- 在有限的场景中,消费者优先级比优先级队列更容易理解。
例如,一组三个队列priority.low、priority.medium和priority.high可以避免队头阻塞问题,同时保持标准的传递行为,并且作为积极的副作用,提供更好的运行时并行性。
声明和支持的优先级范围
经典队列支持 [0, 255] 范围内的优先级。仲裁队列支持的范围要小得多:1 到 4。
因此,以及其他原因,**强烈建议为经典队列使用 2 到 4 个优先级**(单个优先级没有实际意义)。
特别是在经典队列的情况下,较高的优先级值将使用更多的 CPU 和内存资源:RabbitMQ 需要为从 1 到给定队列配置的最大值之间的每个优先级内部维护一个子队列。
队列可以通过使用客户端提供的可选参数成为优先级队列。
通过策略将队列声明为优先级队列在设计上不受支持。原因请参阅为什么策略定义不支持优先级队列。
使用客户端提供的可选参数
要声明优先级队列,请使用 x-max-priority 可选队列参数。此参数应为 [1, 255] 范围内的正整数。但是,如上所述,强烈建议使用 2 到 4 个优先级。
例如,使用 Java 客户端
Channel ch = ...;
Map<String, Object> args = new HashMap<String, Object>();
// Recommendation: use values from 2 to 4 for the maximum number of priorities
args.put("x-max-priority", 4);
ch.queueDeclare("my-priority-queue", true, false, false, args);
发布者随后可以使用 basic.properties 的 priority 字段发布带优先级的消息。数字越大表示优先级越高。
优先级队列行为
AMQP 0-9-1 规范对于优先级如何工作有些模糊。它规定所有队列必须支持至少 2 个优先级,并可能支持高达 10 个。它没有定义没有优先级属性的消息如何处理。
默认情况下,RabbitMQ 经典队列不支持优先级。创建优先级队列时,可以根据需要选择最大优先级。选择优先级值时,需要考虑以下因素:
-
每个优先级级别每队列都有一些内存和磁盘成本。此外,还有额外的 CPU 成本,尤其是在消费时,因此您可能不希望创建大量的级别。
-
消息
priority字段定义为无符号字节,即其值不能超出 [0, 255] 范围。 -
没有
priority属性的消息被视为优先级为 0。优先级高于队列最大值的消息被视为以最大优先级发布。
最大优先级数量和资源使用情况
对于采用带优先级发布和优先级队列的环境,**强烈建议使用 2 到 4 个优先级**。如果必须高于 4,通常使用高达 10 个优先级就足够了(保持为个位数)。
对于经典队列,使用更多优先级会通过使用更多 Erlang 进程来消耗更多的 CPU 资源。运行时调度也会受到影响。
优先级队列如何与消费者协同工作
如果一个消费者连接到一个空的优先级队列,然后消息随后被发布到该队列,那么在消费者接收这些消息之前,消息可能不会在优先级队列中等待任何时间(所有消息立即被接收)。在这种情况下,优先级队列没有机会对消息进行优先级排序,不需要优先级。
然而,在大多数情况下,之前的情况并非如此,因此您应该在消费者的手动确认模式下使用 basic.qos(预取)方法来限制任何时候可以进行传递的消息数量,并允许消息进行优先级排序。basic.qos 是消费者连接到队列时设置的一个值。它指示消费者一次可以处理多少条消息。
以下示例试图更详细地解释消费者如何与优先级队列协同工作,并强调有时当优先级队列与消费者协同工作时,实际中优先级更高的消息可能需要等待优先级较低的消息首先被处理。
示例
-
一个新消费者连接到一个空的经典(非优先)队列,消费者预取(
basic.qos)值为 10。 -
一条消息被发布并立即发送给消费者进行处理。
-
随后快速发布了 5 条消息,并立即发送给消费者,因为消费者声明的 qos(预取)值为 10,但只有一个消息处于进行中(未确认)。
-
接下来,快速发布了 10 条消息并发送给消费者,只有 4 条消息被发送给消费者(因为原始
basic.qos(消费者预取)值 10 现已满),其余 6 条消息必须在队列中等待(就绪消息)。 -
消费者现在确认了 5 条消息,因此上面等待的 6 条消息中有 5 条被发送给消费者。
现在添加优先级
-
如上例所示,消费者连接的
basic.qos(消费者预取)值为 10。 -
发布了 10 条低优先级消息,并立即发送给消费者(
basic.qos(消费者预取)现已达到其限制)。 -
发布了一条最高优先级的消息,但由于预取已满,因此最高优先级的消息需要等待优先级较低的消息首先被处理。
与其他功能的交互
总的来说,优先级队列具有标准 RabbitMQ 队列的所有功能。有几处交互是开发人员应该注意的。
应过期的消息仍然只从队列头部过期。这意味着,与普通队列不同,即使是每个队列的 TTL 也可能导致过期的低优先级消息被卡在非过期的更高优先级消息后面。这些消息永远不会被传递,但它们会出现在队列统计信息中。
设置了最大长度的队列会像往常一样从队列头部丢弃消息以强制执行限制。这意味着更高优先级的消息可能会被丢弃以腾出空间给优先级较低的消息,这可能不是您期望的。
为什么策略定义不支持优先级队列
定义队列可选参数的最方便的方法是使用策略。策略是配置TTL、队列长度限制和其他可选队列参数的推荐方法。
但是,策略不能用于配置优先级,因为策略是动态的,可以在队列声明后更改。优先级队列在队列声明后永远无法更改其支持的优先级数量,因此策略不是一个安全的选择。