AMQP 0-9-1模型详解
AMQP 0-9-1模型详解
cc
概述
本指南将概述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代理提供四种交换器类型: |交换器类型|默认预声明名称| |—-|—-| |直连交换器|(空字符串)和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模型中,应用程序有两种方式来消费消息:
- 订阅以接收消息投递(“push API”):这是推荐的选项。
- 轮询(“pull API”):这种方式效率非常低,在大多数情况下应该避免使用。
使用“push API”时,应用程序必须表明有兴趣从特定队列消费消息。当它们这样做时,我们说它们注册了一个消费者,或者简单地说,订阅了一个队列。一个队列可以有多个消费者,也可以注册一个排他消费者(在其消费期间,排他消费者会将所有其他消费者排除在队列之外)。
每个消费者(订阅)都有一个称为消费者标签的标识符。它可用于取消消息订阅。消费者标签只是字符串。
消息确认
消费消息的应用程序(即接收并处理消息的应用程序)偶尔可能无法处理单个消息、与服务器失去连接或以其他多种方式出现故障。
网络问题也可能导致问题。这就引出了一个问题:代理应该何时从队列中删除消息?AMQP 0-9-1规范让消费者控制这个问题。有两种确认模式:
- 代理将消息发送到应用程序之后(使用
basic.deliver或basic.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 0-9-1方法的逻辑分组。AMQP 0-9-1参考文档包含所有AMQP 0-9-1方法(协议操作)的完整详细信息。
让我们看一下交换器类,这是一组与交换器操作相关的方法。它包括以下操作:
exchange.declareexchange.declare-okexchange.deleteexchange.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实体提供了完全隔离的环境。协议客户端在连接协商期间指定它们想要使用的虚拟主机。
AMQP是可扩展的
AMQP 0 - 9 - 1有几个扩展点:
- 自定义交换器类型:开发人员可以实现开箱即用的交换器类型无法很好覆盖的路由方案,例如基于地理数据的路由。
- 交换器和队列声明:可以包含代理可以使用的额外属性。例如,RabbitMQ中每个队列的消息TTL就是通过这种方式实现的。
- 特定于代理的协议扩展:例如,可以查看RabbitMQ实现的扩展。
- 新的AMQP 0 - 9 - 1方法类:偶尔会为特定情况引入新的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的主要目标之一是实现互操作性,开发人员理解协议操作并不仅限于特定客户端库的术语是个好主意。这样,与使用不同库的开发人员进行沟通会更加容易。





