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 模型对世界的看法如下:消息发布到交换机,交换机通常比作邮局或邮箱。交换机然后使用称为绑定的规则将消息副本分发到队列。然后代理将消息传递给订阅队列的消费者,或者消费者根据需要从队列中获取/拉取消息。
发布消息时,发布者可以指定各种消息属性(消息元数据)。其中一些元数据可能会被代理使用,但是,其余元数据对代理完全不透明,仅供接收消息的应用程序使用。
网络不可靠,应用程序可能无法处理消息,因此 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 的队列
直连交换机可以用图形表示如下
扇出交换机
扇出交换机将消息路由到绑定到它的所有队列,并且忽略路由键。如果 N 个队列绑定到扇出交换机,当发布新消息到该交换机时,该消息的副本将传递到所有 N 个队列。扇出交换机非常适合消息的广播路由。
由于扇出交换机将消息的副本传递到绑定到它的每个队列,因此其用例非常相似
- 大型多人在线 (MMO) 游戏可以使用它来更新排行榜或其他全局事件
- 体育新闻网站可以使用扇出交换机来近乎实时地将比分更新分发到移动客户端
- 分布式系统可以广播各种状态和配置更新
- 群聊可以使用扇出交换机在参与者之间分发消息(尽管 AMQP 没有内置的存在概念,因此 XMPP 可能是更好的选择)
扇出交换机可以用图形表示如下
主题交换机
主题交换机根据消息路由键与用于将队列绑定到交换机的模式之间的匹配,将消息路由到一个或多个队列。主题交换机类型通常用于实现各种发布/订阅模式变体。主题交换机通常用于消息的多播路由。
主题交换机具有非常广泛的用例。每当问题涉及多个消费者/应用程序选择性地选择要接收的消息类型时,都应考虑使用主题交换机。
示例用途
- 分发与特定地理位置相关的数据,例如销售点
- 多个工作程序完成的后台任务处理,每个工作程序都能够处理特定的一组任务
- 股票价格更新(以及其他类型财务数据的更新)
- 涉及分类或标记的新闻更新(例如,仅针对特定运动或团队)
- 在云中编排不同类型的服务
- 分布式架构/操作系统特定的软件构建或打包,其中每个构建器只能处理一个架构或操作系统
头部交换机
头部交换机旨在根据更易于表示为消息头而不是路由键的多个属性进行路由。头部交换机忽略路由键属性。相反,用于路由的属性取自头属性。如果头的值等于绑定时指定的值,则消息被认为是匹配的。
可以使用多个头将队列绑定到头部交换机以进行匹配。在这种情况下,代理需要应用程序开发人员提供更多信息,即,它是否应该考虑具有任何匹配头的消息,还是所有消息?这就是“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。绑定可能具有可选的路由键属性,某些交换机类型会使用该属性。路由键的目的是选择发布到交换机以路由到绑定队列的某些消息。换句话说,路由键充当过滤器。
打个比方
- 队列就像你在纽约市的目的地
- 交换机就像肯尼迪机场
- 绑定是从肯尼迪机场到目的地的路线。可以有零条或多条到达它的方式
拥有这种间接层可以实现使用直接发布到队列而无法实现或难以实现的路由场景,并且还可以消除应用程序开发人员必须执行的某些重复工作。
如果消息无法路由到任何队列(例如,因为没有与其发布到的交换机绑定的绑定),则它将被丢弃或返回给发布者,具体取决于发布者设置的消息属性。
消费者
除非应用程序可以消费它们,否则将消息存储在队列中是毫无意义的。在 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 方法的逻辑分组。AMQP 0-9-1 参考提供了所有 AMQP 方法的完整详细信息。
让我们看一下交换机类,这是一个与交换机上的操作相关的众多方法的组。它包括以下操作
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-ok
方法进行响应。
exchange.declare-ok
不携带任何参数,除了通道号(通道将在本指南的后面部分介绍)。
在 AMQP 0-9-1 的队列方法类中,另一个方法对的事件序列非常相似: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 实体驻留。协议客户端在连接协商期间指定它们要使用的 vhosts。
AMQP 可扩展
AMQP 0-9-1 有几个扩展点。
- 自定义交换机类型 允许开发人员实现现成的交换机类型无法很好地涵盖的路由方案,例如基于地理数据的路由。
- 交换机和队列的声明可以包含代理可以使用的一些其他属性。例如,RabbitMQ 中的每个队列的消息 TTL 就是这样实现的。
- 协议的特定于代理的扩展。例如,请参阅RabbitMQ 实现的扩展。
- 新的 AMQP 0-9-1 方法类 可以被引入。
- 代理可以使用其他插件进行扩展,例如,RabbitMQ 管理前端和 HTTP API 是作为插件实现的。
这些特性使 AMQP 0-9-1 模型更加灵活,并适用于非常广泛的问题。
AMQP 0-9-1 客户端生态系统
有许多AMQP 0-9-1 客户端适用于许多流行的编程语言和平台。其中一些客户端严格遵循 AMQP 术语,并且仅提供 AMQP 方法的实现。另一些客户端则具有其他功能、便捷方法和抽象。一些客户端是异步的(非阻塞的),一些是同步的(阻塞的),一些客户端支持这两种模型。一些客户端支持特定于供应商的扩展(例如,RabbitMQ 特定的扩展)。
因为 AMQP 的主要目标之一是互操作性,所以开发人员了解协议操作而不是仅限于特定客户端库的术语是一个好主意。这样,与使用不同库的开发人员进行沟通将变得更加容易。