跳至主内容

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
Topic 交换机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

主题交换器

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

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

示例用途

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

头交换器

头交换器旨在根据比路由键更容易表达的消息属性进行路由。头交换器会忽略路由键属性。相反,用于路由的属性取自头属性。当头部的属性值等于绑定时指定的值时,消息被视为匹配。

可以使用多个头进行匹配,将队列绑定到头交换器。在这种情况下,消息代理需要应用程序开发人员提供另一条信息,即:它应该考虑匹配任何头部的值,还是所有头部的值?这就是“x-match”绑定参数的作用。当“x-match”参数设置为“any”时,只需要一个匹配的头部值即可。或者,将“x-match”设置为“all”则要求所有值都必须匹配。

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

头交换器可以被看作是“增强型的直接交换器”。因为它们基于头部值进行路由,所以它们可以像直接交换器一样使用,其中路由键不一定是字符串;例如,它可以是整数或哈希(字典)。

队列

队列在 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。绑定可以有一个可选的路由键属性,某些交换器类型会使用该属性。路由键的目的是选择发布到交换器的特定消息以路由到绑定的队列。换句话说,路由键充当过滤器。

打个比方

  • 队列就像你在纽约市的目的地。
  • 交换器就像 JFK 机场。
  • 绑定是从 JFK 到你目的地的路线。可能有一条或多条路径到达那里。

拥有这个间接层可以实现直接发布到队列不可能或非常难以实现的路由场景,并且还可以消除应用程序开发人员需要做的重复工作。

如果一条消息无法路由到任何队列(例如,因为它发布的交换器没有任何绑定),那么它将被丢弃或退回给发布者,这取决于发布者设置的消息属性。

消费者

将消息存储在队列中是毫无意义的,除非应用程序可以消耗它们。在 AMQP 0-9-1 模型中,应用程序有两种方式可以做到这一点:

  • 订阅以接收消息(“推送 API”):这是推荐的选项。
  • 轮询(“拉取 API”):这种方式效率极低在大多数情况下应避免

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

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

消息确认

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

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

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

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

如果消费者在未发送确认的情况下终止,消息代理将重新将其传递给另一个消费者,或者(如果当时没有其他可用消费者),消息代理将等待至少有一个同一队列的消费者注册后,再尝试重新传递。

拒绝消息

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

否定确认

消息使用 `basic.reject` 方法被拒绝。`basic.reject` 有一个限制:无法像确认那样拒绝多条消息。但是,如果您使用的是 RabbitMQ,那么有一个解决方案。RabbitMQ 提供了一个称为否定确认nack的 AMQP 0-9-1 扩展。有关更多信息,请参阅确认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 0-9-1 方法的逻辑分组。AMQP 0-9-1 参考包含所有 AMQP 0-9-1 方法(协议操作)的完整详细信息。

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

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

(请注意,RabbitMQ 网站参考也包含 RabbitMQ 特定的交换器类扩展,我们将在本指南中不讨论它们)。

上述操作形成逻辑对:“exchange.declare”和“exchange.declare-ok”,“exchange.delete”和“exchange.delete-ok”。这些操作是“请求”(由客户端发送)和“响应”(由消息代理响应上述“请求”而发送)。

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

exchange.declare

如上图所示,“exchange.declare”携带几个参数。它们允许客户端指定交换器名称、类型、持久性标志等。

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

exchange.declare-ok

除了通道号(稍后在本指南中将介绍通道)之外,“exchange.declare-ok”不携带任何参数。

对于 AMQP 0-9-1队列方法类中的另一对方法:“queue.declare”和“queue.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 包含了虚拟主机(vhosts)的概念。它们类似于许多流行 Web 服务器使用的虚拟主机,并提供了 AMQP 实体所在的完全隔离的环境。协议客户端在连接协商期间指定它们想要使用的虚拟主机。

AMQP 是可扩展的

AMQP 0-9-1 具有几个扩展点:

  • 自定义交换器类型允许开发人员实现开箱即用的交换器类型无法很好覆盖的路由方案,例如,基于地理数据的路由。
  • 交换器和队列的声明可以包含代理使用的附加属性。例如,RabbitMQ 中的每个队列的消息 TTL就是这样实现的。
  • 特定于代理的协议扩展。例如,请参阅RabbitMQ 实现的扩展
  • 每隔一段时间,新的 AMQP 0-9-1 方法类可以被引入用于特定情况,例如OAuth 2(JWT)令牌刷新。
  • 消息代理可以通过附加插件进行扩展,例如,RabbitMQ 管理前端和 HTTP API 就是作为插件实现的。

这些功能使得 AMQP 0-9-1 模型更加灵活,并适用于各种各样的问题。

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

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

由于 AMQP 的主要目标之一是互操作性,因此开发人员了解协议操作而不是局限于特定客户端库的术语是一个好主意。这样,与其他库的开发人员的沟通就会更容易。

© . This site is unofficial and not affiliated with VMware.