跳到主要内容

AMQP 0-9-1 模型解释

概述

本指南概述了 AMQP 0-9-1 协议,它是 RabbitMQ 支持的协议之一。

AMQP 0-9-1 和 AMQP 模型的高级概述

什么是 AMQP 0-9-1?

AMQP 0-9-1(高级消息队列协议)是一种消息传递协议,使符合标准的客户端应用程序能够与符合标准的消息传递中间件代理进行通信。

代理及其作用

消息代理从发布者(发布消息的应用程序,也称为生产者)接收消息,并将它们路由到消费者(处理消息的应用程序)。

由于它是一种网络协议,发布者、消费者和代理都可以驻留在不同的机器上。

AMQP 0-9-1 模型概述

AMQP 0-9-1 模型对世界的看法如下:消息发布到交换机,交换机通常被比作邮局或邮箱。交换机然后使用名为绑定的规则将消息副本分发到队列。然后,代理将消息传递给订阅队列的消费者,或者消费者按需从队列中获取/拉取消息。

Publish path from publisher to consumer via exchange and queue

在发布消息时,发布者可以指定各种消息属性(消息元数据)。其中一些元数据可能会被代理使用,但是,其余的元数据对代理来说是完全不透明的,只有接收消息的应用程序才会使用它们。

网络不可靠,应用程序可能无法处理消息,因此 AMQP 0-9-1 模型具有消息确认的概念:当消息传递给消费者时,消费者会通知代理,无论是自动完成还是在应用程序开发人员选择执行此操作时。当使用消息确认时,代理只有在收到该消息(或一组消息)的通知后才会从队列中完全删除该消息。

在某些情况下,例如,当消息无法路由时,消息可能会返回给发布者、被丢弃,或者,如果代理实现了扩展,则被放置到所谓的“死信队列”中。发布者通过使用某些参数发布消息来选择如何处理这种情况。

队列、交换机和绑定统称为AMQP 实体

AMQP 0-9-1 是可编程协议

AMQP 0-9-1 是一种可编程协议,因为 AMQP 0-9-1 实体和路由方案主要由应用程序本身定义,而不是由代理管理员定义。因此,为声明队列和交换机、定义它们之间的绑定、订阅队列等协议操作提供了规定。

这为应用程序开发人员提供了很大的自由度,但也要求他们意识到潜在的定义冲突。在实践中,定义冲突很少见,通常表明配置错误。

应用程序声明它们需要的 AMQP 0-9-1 实体,定义必要的路由方案,并可以选择在不再使用时删除 AMQP 0-9-1 实体。

交换机和交换机类型

交换机是消息发送到的 AMQP 0-9-1 实体。交换机接收消息并将消息路由到零个或多个队列。使用的路由算法取决于交换机类型和名为绑定的规则。AMQP 0-9-1 代理提供四种交换机类型

交换机类型默认预声明名称
直接交换机(空字符串) 和 amq.direct
扇出交换机amq.fanout
主题交换机amq.topic
头文件交换机amq.match(以及 RabbitMQ 中的 amq.headers)

除了交换机类型外,交换机还用许多属性声明,其中最重要的属性是

  • 名称
  • 持久性(交换机在代理重启后依然存在)
  • 自动删除(当最后一个队列从交换机解绑时,交换机被删除)
  • 参数(可选,由插件和代理特定功能使用)

交换机可以是持久性的,也可以是瞬时的。持久性交换机在代理重启后依然存在,而瞬时交换机则不会(它们必须在代理恢复联机时重新声明)。并非所有场景和用例都需要交换机是持久性的。

默认交换机

默认交换机是一个没有名称(空字符串)的直接交换机,由代理预声明。它有一个特殊属性,使它非常适合简单的应用程序:创建的每个队列都自动使用与队列名称相同的路由键绑定到它。

例如,当您声明一个名为“search-indexing-online”的队列时,AMQP 0-9-1 代理将使用“search-indexing-online”作为路由键(在这种情况下有时被称为绑定键)将其绑定到默认交换机。因此,发布到默认交换机且路由键为“search-indexing-online”的消息将被路由到队列“search-indexing-online”。换句话说,默认交换机使它看起来好像可以将消息直接传递给队列,尽管从技术上讲这不是正在发生的事情。

默认交换机在 RabbitMQ 中不允许绑定/解绑操作。对默认交换机的绑定操作将导致错误。

直接交换机

直接交换机根据消息路由键将消息传递给队列。直接交换机非常适合消息的单播路由。它们也可以用于多播路由。

以下是它的工作原理

  • 队列使用路由键 K 绑定到交换机
  • 当带有路由键 R 的新消息到达直接交换机时,如果 K = R,交换机将消息路由到队列
  • 如果多个队列使用相同的路由键 K 绑定到直接交换机,交换机将把消息路由到所有 K = R 的队列

直接交换机可以用图形表示如下

exchange delivering messages to  queues based on routing key

扇出交换机

扇出交换机将消息路由到绑定到它的所有队列,并且忽略路由键。如果 N 个队列绑定到扇出交换机,当发布到该交换机的新消息时,该消息的副本将传递给所有 N 个队列。扇出交换机非常适合消息的广播路由。

由于扇出交换机将消息的副本传递给绑定到它的每个队列,因此它的用例非常相似

  • 大型多人在线 (MMO) 游戏可以使用它来更新排行榜或其他全局事件
  • 体育新闻网站可以使用扇出交换机来近乎实时地将比分更新分发给移动客户端
  • 分布式系统可以广播各种状态和配置更新
  • 群聊可以使用扇出交换机在参与者之间分发消息(尽管 AMQP 没有内置的存在概念,因此 XMPP 可能是更好的选择)

扇出交换机可以用图形表示如下

exchange delivering messages to three queues

主题交换机

主题交换机根据消息路由键与用于将队列绑定到交换机的模式之间的匹配情况,将消息路由到一个或多个队列。主题交换机类型通常用于实现各种发布/订阅模式变体。主题交换机通常用于消息的多播路由。

主题交换机有非常广泛的用例。每当一个问题涉及多个消费者/应用程序,这些消费者/应用程序可以选择性地选择它们想要接收的消息类型时,都应该考虑使用主题交换机。

示例用法

  • 分发与特定地理位置相关的数据,例如销售点
  • 多个工作程序完成的后台任务处理,每个工作程序能够处理特定的一组任务
  • 股票价格更新(以及其他类型金融数据的更新)
  • 涉及分类或标记的新闻更新(例如,仅针对特定运动或团队)
  • 云中不同类型服务的编排
  • 分布式架构/特定于操作系统的软件构建或打包,其中每个构建器只能处理一个架构或操作系统

头文件交换机

头文件交换机旨在针对更容易用消息头文件而不是路由键表示的多个属性进行路由。头文件交换机忽略路由键属性。相反,用于路由的属性取自头文件属性。如果头文件的 value 等于绑定时指定的 value,则消息被认为是匹配的。

可以使用多个头文件来绑定队列到头文件交换机以进行匹配。在这种情况下,代理需要来自应用程序开发人员的另外一条信息,即,它应该考虑所有头文件匹配的消息还是其中任何一个?这就是“x-match”绑定参数的作用。当“x-match”参数设置为“any”时,仅需一个匹配的头文件 value 就足够了。或者,将“x-match”设置为“all”要求所有 value 都必须匹配。

对于“any”和“all”,以字符串“x-”开头的头文件将不会用于评估匹配。将“x-match”设置为“any-with-x”或“all-with-x”也将使用以字符串“x-”开头的头文件来评估匹配。

头文件交换机可以看作是“加强版的直接交换机”。因为它们是根据头文件 value 进行路由,所以它们可以用作直接交换机,其中路由键不必是字符串;例如,它可以是整数或散列(字典)。

队列

AMQP 0-9-1 模型中的队列与其他消息和任务队列系统中的队列非常相似:它们存储由应用程序消费的消息。队列与交换机共享一些属性,但也具有一些其他属性

  • 名称
  • 持久化(队列将在代理重启后继续存在)
  • 独占(仅供一个连接使用,并且在该连接关闭时将删除队列)
  • 自动删除(至少有一个消费者的队列将在最后一个消费者取消订阅时删除)
  • 参数(可选;由插件和特定于代理的功能使用,例如消息 TTL、队列长度限制等)

在使用队列之前,必须先声明它。声明队列将导致在队列不存在时创建它。如果队列已经存在并且其属性与声明中的属性相同,则声明将不会有任何影响。当现有队列属性与声明中的属性不同时,将引发带有代码 406 (PRECONDITION_FAILED) 的通道级异常。

队列名称

应用程序可以选择队列名称,也可以让代理为其生成名称。队列名称可以包含最多 255 个字节的 UTF-8 字符。AMQP 0-9-1 代理可以代表应用程序生成唯一的队列名称。要使用此功能,请将空字符串作为队列名称参数传递。生成的名称将通过队列声明响应返回给客户端。

以“amq.”开头的队列名称保留供代理内部使用。尝试声明名称违反此规则的队列将导致带有回复代码 403 (ACCESS_REFUSED) 的通道级异常。

队列持久性

在 AMQP 0-9-1 中,队列可以声明为持久化或瞬时。持久化队列的元数据存储在磁盘上,而瞬时队列的元数据尽可能存储在内存中。

发布时消息也做出了相同的区分。

在持久性很重要的环境和用例中,应用程序必须使用持久化队列以及确保发布者将发布的消息标记为已持久化。

本主题在队列指南中有更详细的介绍。

绑定

绑定是交换机用来(除其他外)将消息路由到队列的规则。要指示交换机 E 将消息路由到队列 Q,Q 必须绑定到 E。绑定可能具有可选的路由键属性,由某些交换机类型使用。路由键的目的是选择发布到交换机的某些消息以路由到绑定的队列。换句话说,路由键充当过滤器。

为了类比

  • 队列就像你在纽约市的目的地
  • 交换机就像肯尼迪机场
  • 绑定是从肯尼迪机场到目的地的路线。可以有零条或多条路线到达那里

拥有这种间接层使路由场景成为可能,而这些场景在直接发布到队列时无法实现或难以实现,并且还消除了应用程序开发人员必须执行的某些重复工作量。

如果消息无法路由到任何队列(例如,因为没有为其发布的交换机绑定),它将根据发布者设置的消息属性被丢弃或返回给发布者

消费者

除非应用程序可以消费它们,否则将消息存储在队列中毫无用处。在 AMQP 0-9-1 模型中,应用程序有两种方法可以做到这一点

  • 订阅将消息传递给他们(“推送 API”):这是推荐的选择
  • 轮询(“拉取 API”):这种方法在大多数情况下非常低效并且应该避免

使用“推送 API”,应用程序必须表明它们有兴趣从特定队列消费消息。当他们这样做时,我们说他们注册了一个消费者,或者简单地说,订阅了一个队列。可以为每个队列拥有多个消费者,或者注册一个独占消费者(在其消费期间将其他所有消费者排除在队列之外)。

每个消费者(订阅)都有一个称为消费者标签的标识符。它可以用来取消订阅消息。消费者标签只是字符串。

消息确认

消费者应用程序——即接收和处理消息的应用程序——可能会偶尔无法处理单个消息,失去与服务器的连接,或者以多种其他方式失败。

还可能出现网络问题导致故障。这提出了一个问题:代理何时应该从队列中删除消息?AMQP 0-9-1 规范赋予消费者对此的控制权。有两种确认模式

  • 在代理将消息发送到应用程序(使用 basic.deliverbasic.get-ok 方法)后。
  • 在应用程序发送回确认(使用 basic.ack 方法)后。

前一种选择称为自动确认模型,而后一种称为显式确认模型。使用显式模型,应用程序选择何时发送确认。它可以在收到消息后立即发送,或者在将消息持久化到数据存储区之前发送,或者在完全处理消息后发送(例如,成功获取网页,处理并将其存储到某个持久化数据存储区)。

如果消费者在没有发送确认的情况下死亡,代理会将其重新传递给另一个消费者,或者,如果当时没有消费者可用,代理会等待至少有一个消费者为同一个队列注册,然后才会尝试重新传递。

拒绝消息

当消费者应用程序收到消息时,该消息的处理可能会成功,也可能不成功。应用程序可以通过拒绝消息来指示代理消息处理已失败(或当时无法完成)。当拒绝消息时,应用程序可以要求代理丢弃或重新排队。当队列上只有一个消费者时,请确保不要通过反复从同一个消费者拒绝和重新排队消息来创建无限消息传递循环。

否定确认

消息使用 basic.reject 方法拒绝。basic.reject 有一个限制:无法像使用确认那样拒绝多条消息。但是,如果您使用的是 RabbitMQ,那么有一个解决方案。RabbitMQ 提供了一个 AMQP 0-9-1 扩展,称为否定确认nacks。有关更多信息,请参阅确认basic.nack 扩展 指南。

预取消息

对于多个消费者共享一个队列的情况,能够指定每个消费者在发送下一个确认之前可以一次接收多少条消息非常有用。这可以作为一种简单的负载均衡技术,或者在消息倾向于分批发布时提高吞吐量。例如,如果生产应用程序由于其正在执行的工作的性质而每分钟发送一次消息。

请注意,RabbitMQ 仅支持通道级预取计数,不支持连接或大小级预取。

消息属性和有效载荷

AMQP 0-9-1 模型中的消息具有属性。某些属性非常常见,因此 AMQP 0-9-1 规范定义了它们,应用程序开发人员不必考虑确切的属性名称。以下是一些示例

  • 内容类型
  • 内容编码
  • 路由键
  • 传递模式(持久化或非持久化)
  • 消息优先级
  • 消息发布时间戳
  • 过期时间
  • 发布者应用程序 ID

某些属性由 AMQP 代理使用,但大多数属性对接收它们的应用程序来说是开放的解释。某些属性是可选的,称为标头。它们类似于 HTTP 中的 X-Headers。消息属性在发布消息时设置。

消息还具有一个有效载荷(它们携带的数据),AMQP 代理将其视为不透明的字节数组。代理不会检查或修改有效载荷。消息可以只包含属性,不包含有效载荷。通常使用 JSON、Thrift、Protocol Buffers 和 MessagePack 等序列化格式来序列化结构化数据,以便将其发布为消息有效载荷。协议对等方通常使用“content-type”和“content-encoding”字段来传递此信息,但这仅仅是一种约定。

消息可以发布为持久化,这使代理将它们持久化到磁盘。如果服务器重新启动,系统将确保已接收的持久化消息不会丢失。简单地将消息发布到持久化交换机或将其路由到的队列是持久化的,并不能使消息持久化:这完全取决于消息本身的持久化模式。将消息发布为持久化会影响性能(就像数据存储一样,持久化会带来一定的性能成本)。

发布者指南中了解更多信息。

AMQP 0-9-1 方法

AMQP 0-9-1 被构造为多个方法。方法是操作(如 HTTP 方法),并且与面向对象编程语言中的方法无关。AMQP 0-9-1 中的协议方法被分组到中。类只是 AMQP 方法的逻辑分组。AMQP 0-9-1 参考包含所有 AMQP 方法的完整详细信息。

让我们看一下交换机类,这是一个与交换机上的操作相关的组方法。它包括以下操作

  • exchange.declare
  • exchange.declare-ok
  • exchange.delete
  • exchange.delete-ok

(请注意,RabbitMQ 网站参考还包括 RabbitMQ 特定的交换机类扩展,我们不会在本指南中讨论这些扩展)。

以上操作形成逻辑对:exchange.declareexchange.declare-okexchange.deleteexchange.delete-ok。这些操作是“请求”(由客户端发送)和“响应”(由代理作为对上述“请求”的响应发送)。

例如,客户端使用 exchange.declare 方法要求代理声明一个新的交换机

exchange.declare

如上图所示,exchange.declare 携带多个参数。它们使客户端能够指定交换机名称、类型、持久化标志等。

如果操作成功,代理将使用 exchange.declare-ok 方法进行响应

exchange.declare-ok

exchange.declare-ok 除了通道号(通道将在本指南的后面部分进行描述)之外,不携带任何参数。

事件顺序对于 AMQP 0-9-1 队列方法类上的另一个方法对非常相似:queue.declarequeue.declare-ok

queue.declare

queue.declare-ok

并非所有 AMQP 0-9-1 方法都有对应的方法。有些方法(basic.publish 是最常用的一个)没有相应的“响应”方法,而另一些方法(例如 basic.get)具有多个可能的“响应”。

连接

AMQP 0-9-1 连接通常是长期的。AMQP 0-9-1 是一种应用程序级协议,它使用 TCP 进行可靠的传递。连接使用身份验证,可以使用 TLS 进行保护。当应用程序不再需要连接到服务器时,它应该优雅地关闭其 AMQP 0-9-1 连接,而不是突然关闭底层 TCP 连接。

通道

某些应用程序需要与代理建立多个连接。但是,同时保持多个 TCP 连接打开是不希望的,因为这样做会消耗系统资源,并使防火墙配置更加困难。AMQP 0-9-1 连接是与通道多路复用的,可以将通道视为“共享单个 TCP 连接的轻量级连接”。

客户端执行的每个协议操作都在一个通道上进行。特定通道上的通信与另一个通道上的通信完全独立,因此每个协议方法也带有一个通道 ID(也称为通道号),这是一个整数,代理和客户端都使用它来确定该方法属于哪个通道。

通道仅存在于连接的上下文中,而不会单独存在。当连接关闭时,其上的所有通道也将关闭。

对于使用多个线程/进程进行处理的应用程序,通常的做法是为每个线程/进程打开一个新通道,并且不共享它们之间的通道。

虚拟主机

为了使单个代理能够托管多个隔离的“环境”(用户组、交换机、队列等),AMQP 0-9-1 包含虚拟主机(vhost)的概念。它们类似于许多流行的 Web 服务器使用的虚拟主机,并提供完全隔离的环境,其中 AMQP 实体存在。协议客户端在连接协商期间指定他们想要使用哪些 vhost。

AMQP 是可扩展的

AMQP 0-9-1 具有多个扩展点

这些功能使 AMQP 0-9-1 模型更加灵活,适用于更广泛的问题。

AMQP 0-9-1 客户端生态系统

许多 AMQP 0-9-1 客户端 用于许多流行的编程语言和平台。其中一些客户端密切遵循 AMQP 术语,只提供 AMQP 方法的实现。另一些则具有额外的功能、便利方法和抽象。有些客户端是异步的(非阻塞的),有些是同步的(阻塞的),有些支持这两种模型。有些客户端支持供应商特定的扩展(例如,RabbitMQ 特定的扩展)。

由于 AMQP 的主要目标之一是互操作性,因此开发人员了解协议操作而不是局限于特定客户端库的术语是一个好主意。这样,与使用不同库的开发人员进行通信将变得容易得多。