RabbitMQ-精要-全-

RabbitMQ 精要(全)

原文:zh.annas-archive.org/md5/f8f0edec482cfe5545e615b07bfd2715

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

RabbitMQ 是一个消息代理,提供组件和服务之间的通信结构。得益于高级消息队列协议AMQP)和所有可用的 RabbitMQ 客户端库,大多数主要编程语言和服务都能够无缝地、异步地协同工作。

本书通过深入探索虚构的出租车调度公司完整汽车CC)的用户旅程,探讨了 RabbitMQ 的强大可能性,该公司具有现实生活中的用户需求。

本书面向对象

如果你是一名专业的企业级开发者或只是喜欢编程的人,RabbitMQ 基础教程是关于开源消息队列架构的有价值资源。即使那些已经熟悉微服务和消息的人,在阅读本书探索最佳实践和资源效率时也会发现其价值。本书将为你提供开始创建新和令人兴奋的应用程序或迁移现有单体到微服务架构所需的推动力。

本书涵盖内容

第一章,一只兔子跃然纸上,是 RabbitMQ 的入门介绍,包括如何开始使用以及消息队列的好处。本章接着指导你如何安装和配置 RabbitMQ,以及为开发应用程序做准备。

第二章,创建出租车应用程序,讨论了使用 RabbitMQ 创建一个简单的出租车订单应用程序。到本章结束时,你将了解如何连接到 RabbitMQ,使用直接和主题交换进行发布,以及从队列中消费消息。本章还解释了消息确认和否定确认(acks 和 nacks)。

第三章,向多个出租车司机发送消息,继续 CC 项目,提供了关于预取值设置的说明,这些设置控制同时发送给消费者的消息数量。本章还涵盖了消费者如何手动确认消息,开发零消息损失设计的建议,以及如何接收不带确认的消息。本章最后为你提供了对扇出交换的良好理解。

第四章,调整消息投递,是关于消息生存时间TTL),使用消息属性名称过期以及其他关于调整消息投递的重要主题,包括死信交换和队列。

第五章,消息路由,深入探讨了消息流,包括如何响应发送者以及如何使用头部交换来执行基于属性的消息路由。此外,还解释了请求-响应风格的交互。

第六章,将 RabbitMQ 投入生产,介绍了可以用来处理 RabbitMQ 代理故障的不同策略。主题包括集群、法定队列和联邦。本章还涵盖了 RabbitMQ 的一个非常重要的方面——日志处理和数据分析。

第七章,最佳实践和代理监控,将前几章中的所有精彩信息汇总为最佳实践和关键要点,这些可以在实际应用开发中使用。本章还解释了 RabbitMQ 中的常见错误,并提供了实施系统性能监控和避免灾难性情况的策略。

为了最大限度地利用本书

如何在 Ubuntu 和 Docker 上安装 RabbitMQ,以及如何通过 CloudAMQP 设置托管版本的 RabbitMQ,在第一章[4f4722d3-131f-4c38-9ea0-4c03e8545175.xhtml],兔子跃然而生中进行了说明。它还提供了关于如何设置 RabbitMQ 托管版本的建议。

即使您不熟悉 Ruby 或 Python,书中的代码也很容易理解。所有 Ruby 代码示例都使用 Ruby 2.7 进行了测试,所有 Python 代码示例都使用 Python 2.7 进行了测试,在 macOS 上。然而,它们可能也适用于未来的版本发布。

软件/硬件 操作系统要求
Python 2.7Ruby 2.7 网络浏览器 macOSUbuntu

如果您正在使用本书的数字版,我们建议您自己输入代码或通过 GitHub 仓库(下一节中提供链接)访问代码。这样做将帮助您避免与代码复制和粘贴相关的任何潜在错误。

下载示例代码文件

您可以从www.packt.com上的账户下载本书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

您可以通过以下步骤下载代码文件:

  1. www.packt.com上登录或注册。

  2. 选择支持选项卡。

  3. 点击代码下载。

  4. 在搜索框中输入书籍名称,并遵循屏幕上的说明。

文件下载完成后,请确保使用最新版本的软件解压缩或提取文件夹:

  • Windows 系统上的 WinRAR/7-Zip

  • Mac 系统上的 Zipeg/iZip/UnRarX

  • Linux 系统上的 7-Zip/PeaZip

该书的代码包也托管在 GitHub 上,地址为github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition。如果代码有更新,它将在现有的 GitHub 仓库中更新。

我们还有来自我们丰富的书籍和视频目录中的其他代码包,可在github.com/PacktPublishing/找到。查看它们吧!

下载彩色图像

我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:static.packt-cdn.com/downloads/9781789131666_ColorImages.pdf

使用的约定

本书使用了多种文本约定。

CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“目前,cc-admincc-dev用户都没有在cc-dev-vhost上执行任何操作的权限。”

代码块设置如下:

connection = Bunny.new ENV['RABBITMQ_URI']
# Start a session with RabbitMQ 
connection.start

任何命令行输入或输出都应如下所示:

sudo apt install curl gnupg -y
sudo apt install apt-transport-https

粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“点击控制台的“Admin”选项卡。”

警告或重要注意事项看起来是这样的。

小贴士和技巧看起来是这样的。

联系我们

我们始终欢迎读者的反馈。

一般反馈:如果您对本书的任何方面有疑问,请在邮件主题中提及书名,并将邮件发送至customercare@packtpub.com

勘误:尽管我们已经尽最大努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/support/errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。

盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过copyright@packt.com与我们联系,并提供材料的链接。

如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com

评论

请留下评论。一旦您阅读并使用过这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!

如需了解更多关于 Packt 的信息,请访问packt.com

第一章:一只兔子跃然纸上

消息传递消息队列是应用程序或组件之间的一种通信方式。得益于消息队列,这些应用程序可以在处理各自任务时保持完全独立。消息通常是小型请求、回复、状态更新,甚至是仅仅信息。消息队列提供了一个临时的地方让这些消息停留,允许应用程序根据需要发送和接收它们。

RabbitMQ 是一个开源的消息代理,作为独立应用程序的中介或中间人,为它们提供了一个共同的平台来通信。RabbitMQ 主要使用基于 Erlang 的 高级消息队列协议AMQP)的实现,它支持诸如集群和消息的复杂路由等高级功能。

本章包括有关如何开始使用 RabbitMQ 以及它为何会惠及架构的信息。本书通过一个虚构的出租车公司,完整汽车CC),来展示他们如何将 RabbitMQ 集成到架构中。本章展示了如何安装和配置 RabbitMQ,使其易于启动和运行。

本章将涵盖以下主题:

  • 解释消息队列

  • 发现 AMQP 和 RabbitMQ

  • 在现实生活中使用 RabbitMQ

  • 探索消息队列的好处

  • RabbitMQ 场景

  • 准备使用 RabbitMQ

让我们开始吧!

技术要求

本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition/tree/master/Chapter01

解释消息队列

烟雾信号、信使、信鸽和信号旗:如果这是一个谜语,那么“信息”这个词会立刻浮现在脑海中。人类始终有连接的需求,寻找新的方法来克服不同需要沟通的人群之间的距离带来的挑战。人类在现代技术的帮助下已经走了很长的路,但本质上,基础仍然如此。发送者、接收者和信息是所有通信基础设施的核心。

软件应用也有同样的需求;系统需要相互通信和发送信息。它们有时需要确保发送的信息已经到达目的地,有时它们需要立即收到回应。在某些情况下,它们甚至可能需要收到多个回应。基于这些不同的需求,系统之间出现了不同的通信风格。

AMQP,RabbitMQ 的默认协议,将在下一节中解释。

发现 AMQP 和 RabbitMQ

消息队列是一种单向通信风格,它为系统之间提供异步交互。随着本章继续描述消息队列的工作方式,其优势将变得明显。关于请求-响应消息交换模式的背景信息将阐明 RabbitMQ 的工作原理。

请求-响应消息交换模式

存在许多种消息交换模式,但请求-响应式是最常见的。一个系统作为客户端,与另一个远程系统交互,该系统作为服务器。客户端发送数据请求,服务器响应请求,如下面的图所示:

图片

图 1.1:客户端与服务器之间的请求-响应交互

当客户端必须立即得到响应或希望服务立即完成任务时,例如在打电话给餐厅预订桌子时,会使用请求-响应式:

图片

图 1.2:客户端与餐厅之间的请求-响应

无论其形式是远程过程调用、Web 服务调用还是资源消费,模型都是相同的:一个系统向另一个系统发送消息并等待远程方的响应。系统以点对点的方式相互通信,其中事件和进程同时发生或具有依赖性或与时间相关的事件;客户端与服务器之间的交互是同步的。

一方面,这种请求-响应式风格为开发者提供了一个简单的编程模型,因为所有事情都是按程序发生的。另一方面,双方之间的紧密耦合对整个系统的架构产生了深远的影响,因为它难以演进,难以扩展,也难以独立发布。

消息队列交换模式

消息队列是一种单向交互风格,其中一个系统通过消息异步地与另一个系统交互,通常通过消息代理。在异步通信模式下,请求系统不需要等待答案或要求返回信息;无论发生什么情况,它都会继续处理。这种交互的最常见例子是电子邮件。关键是,异步通信不涉及等待响应以继续处理。事实上,可能没有响应,或者发送响应可能需要一些时间。无论哪种情况,系统都不会依赖于响应来继续进程。

消息单向流动,从发布者到代理,最后到消费者:

图片

图 1.3:基于消息队列的单向交互的基本组件

系统和应用程序同时扮演消息发布者(生产者)和消息消费者(订阅者)的角色。发布者将消息发布给一个代理,他们依赖这个代理将数据传递给目标消费者。如果需要响应,它将通过相同的机制在某个时间点到达,但方向相反(消费者和生产者的角色将互换)。

松散耦合架构

消息队列方法的一个大优点是系统之间变得松散耦合。它们不需要知道网络上其他节点的位置;仅仅一个名字就足够了。因此,系统可以独立地发展,不会相互影响,因为消息传递的可靠性委托给了代理。

以下图表展示了发布者和消费者之间的松散耦合:

图片

图 1.4:消息队列实现松散耦合架构

如果系统因任何原因关闭,系统的另一部分仍然可以运行,它们之间应该发送的消息将等待在队列中。

通过消息队列表示的架构允许以下功能:

  • 发布者或消费者可以逐个更新,而不会相互影响。

  • 每一方的性能都不会影响另一方。

  • 发布者或消费者允许失败而不会相互影响。

  • 发布者和消费者实例的数量可以根据需要扩展,以独立地适应它们的工作负载。

  • 消费者和发布者之间的技术混合。

这种方法的主要缺点是程序员不能依赖于程序性编程的心理模型,其中事件一个接一个地发生。在消息传递中,事情是随着时间的推移发生的。系统必须被编程来处理这种情况。

如果这一切有点模糊不清,可以参考一个众所周知的协议,简单邮件传输协议SMTP)。在这个协议中,电子邮件被发布(发送)到 SMTP 服务器。这个初始服务器随后存储并转发电子邮件到下一个 SMTP 服务器,依此类推,直到到达收件人的电子邮件服务器。此时,消息将在收件箱中排队,等待消费者取走(通常是通过 POP3 或 IMAP)。在 SMTP 中,发布者不知道电子邮件何时会被投递,或者是否最终会被投递。在投递失败的情况下,发布者会在稍后得知问题。

唯一确定的事实是,代理已经成功接收了最初发送的消息。整个过程可以在以下图表中看到:

图片

图 1.5:将电子邮件基础设施作为消息队列的类比

此外,如果需要响应,它将使用相同的交付机制异步到达,但发布者和消费者的角色相反。

在确立了这些基本概念之后,现在是深入探讨本书将使用的消息协议——AMQP 的最佳时机。

了解 AMQP

AMQP是一个开放标准协议,它定义了系统如何交换消息。该协议定义了一套规则,这些规则需要由将要相互通信的系统遵循。除了定义消费者/生产者和代理之间的交互外,它还定义了交换的消息和命令的表示。AMQP 真正实现了互操作性,因为它指定了消息的线格式,没有留给特定供应商或托管平台任何解释的空间。由于它是开源的,AMQP 社区非常活跃,并在多种语言中实现了代理和客户端实现。

RabbitMQ 基于 AMQP 0-9-1 规范构建,但提供了支持 AMQP 1.0 的插件。

可以在www.rabbitmq.com/resources/specs/amqp0-9-1.pdf下载 AMQP 0-9-1 规范。

以下列出了 AMQP 的核心概念,这些概念将在后续章节中详细解释:

  • 代理或消息代理:代理是一段软件,它从应用程序或服务接收消息,并将它们传递给另一个应用程序、服务或代理。

  • 虚拟主机,vhost:虚拟主机存在于代理中。它是将使用相同 RabbitMQ 实例的应用程序分离的一种方式,类似于代理内部的一个逻辑容器;例如,将工作环境分离到开发在一个 vhost 上和预发布在另一个 vhost 上,而不是设置多个代理。用户、交换、队列等都在一个特定的 vhost 上隔离。连接到特定 vhost 的用户无法访问另一个 vhost 上的任何资源(队列、交换等)。用户可以对不同的 vhost 有不同的访问权限。

  • 连接:应用程序(发布者/消费者)和代理之间的物理网络(TCP)连接。当客户端断开连接或发生系统故障时,连接将被关闭。

  • 通道:通道是连接中的一个虚拟连接。它重用连接,无需重新授权和打开新的 TCP 流。当发布或消费消息时,是在通道上完成的。可以在单个连接中建立多个通道。

  • 交换:交换实体负责应用消息的路由规则,确保消息达到最终目的地。换句话说,交换确保接收到的消息最终进入正确的队列。消息最终进入哪个队列取决于交换类型定义的规则。一个队列至少需要绑定到一个交换,才能接收消息。路由规则包括直接(点对点)、主题(发布订阅)、广播(多播)和头部交换。

  • 队列:队列是一系列项目;在这种情况下,是消息。队列存在于代理中。

  • 绑定:绑定是代理中交换机和队列之间的虚拟链接。它使得消息可以从交换流向队列。

以下图表展示了 AMQP 中一些概念的整体概述:

图片

图片

本书详细展示的开源代理是从头开始构建以支持 AMQP 的,但 RabbitMQ 也支持许多其他协议,例如 MQTT、HTTP 和 STOMP。

现在,是时候将焦点转向 RabbitMQ 了。

RabbitMQ 代理

RabbitMQ 是 AMQP 代理的 Erlang 实现。它实现了 AMQP 的 0-9-1 版本,并添加了协议允许的自定义扩展。选择 Erlang 是因为它内在的支持构建高度可靠和分布式应用的能力。实际上,Erlang 被用于运行几个大型电信系统中的电信交换机,并报告了整个系统的九个九的可用性(这意味着每年只有 32 毫秒的停机时间)。Erlang 能够在任何操作系统上运行。

对于数据持久性,RabbitMQ 依赖于 Erlang 的内存/文件持久化嵌入式数据库 Mnesia。Mnesia 存储有关用户、交换、队列、绑定等信息。队列索引存储消息位置以及消息是否已投递的信息。消息存储在队列索引或消息存储中,这是一个在所有队列之间共享的键值存储。

对于集群,它主要依赖于 Erlang 固有的集群能力。RabbitMQ 可以通过添加插件轻松扩展。例如,可以通过这种机制在 RabbitMQ 上部署基于 Web 的管理控制台。

可以使用插件来扩展核心代理功能。RabbitMQ 有许多可用的插件,如果需要,也可以开发插件:www.rabbitmq.com/plugins.html

RabbitMQ 可以设置在单个独立实例上,或者作为多个服务器上的集群:

图片

图 1.7:独立实例,或作为多个服务器上的集群

RabbitMQ 代理可以使用不同的技术,如联邦和铲子,连接在一起,以形成跨代理的智能消息路由的消息拓扑,并具有跨越多个数据中心的容量。

以下截图显示了位于世界各地不同位置的 RabbitMQ 代理之间的联邦:

图片

图 1.8:参与各种拓扑的 RabbitMQ 代理

RabbitMQ 通过插件支持 AMQP 1.0

AMQP 1.0 于 2011 年底发布,在 AMQP 的开发和维护转移到 OASIS 之后。AMQP 在 0-9-1 和 1.0 之间经历了大幅修订。这次修订如此剧烈,以至于一些核心概念,如交换,不再存在。因此,AMQP 1.0 是一个与 0-9-1 不同的协议,但没有真正令人信服的理由去采用它。它并不比 0-9-1 更强大,有些人也会争论说,它失去了最初使其具有吸引力的关键方面。

因此,RabbitMQ 在何时何地被使用?下一节将描述一些常见的 RabbitMQ 使用案例。

在现实生活中使用 RabbitMQ

RabbitMQ 最常见的使用案例是单生产者、单消费者队列。想象它就像一个管道,一个应用程序将消息放入管道的一端,另一个应用程序读取从另一端出来的消息。消息按照先入先出的顺序交付。这些消息可能是命令或包含重要数据。这听起来很简单,但这种类型的架构可以应用在哪里?现在是时候了解何时以及为什么消息队列会发光了!

微服务之间的消息队列

消息队列通常用于微服务之间,但这意味着什么?

微服务架构风格将应用程序划分为小的服务,完成的应用程序是其微服务的总和。服务之间并不严格连接。相反,它们使用,例如,消息队列来保持联系。一个服务异步地将消息推送到队列,当消费者准备好时,这些消息被发送到正确的目的地。

微服务架构通常与单体架构进行比较和对比,在单体架构中,整个系统被捆绑在一起成为一块软件。一个应用程序不仅负责特定的任务;它实际上执行完成特定功能所需的每一步。单体在系统中进行通信,因为所有部分都在同一个进程中运行。这个系统高度耦合,因为每个功能都依赖于其他功能。

在一个基于单体架构风格构建的网店示例中,一个系统处理所有功能,包括库存、支付、评论和评分等,如下所示:

图片

图 1.9:采用单体架构风格构建的网店

建立在微服务架构之上的在线商店,另一方面意味着系统的每个部分都是一个独立的活动。一个微服务处理评论和评分。然后是库存,然后是支付,等等,如下面的图所示:

图片

图 1.10:一种微服务架构风格,其中每个部分都专注于单一的业务能力

请求和响应的每一对都是独立通信的。这被称为无状态通信。虽然涉及许多微服务,但它们并不直接相互依赖。

RabbitMQ 的另一个典型用例是作为任务队列,我们将在下一节中介绍。

事件和任务

事件是通知,告诉应用程序何时发生了某事。一个应用程序可以订阅来自另一个应用程序的事件,并通过为自己创建和处理任务来响应。一个典型的用例是当 RabbitMQ 作为处理 操作的任务队列时。

让我们看看两个这样的例子:

  • 想象一个社交媒体应用程序,例如 Instagram。每次有人发布一条新帖子时,网络(关注者)都需要了解新的帖子。这可能是一个非常耗时的操作。数百万的人可能同时尝试执行相同的任务。应用程序可以使用消息队列,在每条帖子到达时将其任务排队。当工作者收到请求时,它会检索发送者的关注者列表,并更新他们。

  • 作为另一个例子,考虑一个发送数千封电子邮件给数千用户的电子邮件新闻通讯工具。在一个可能的场景中,许多用户同时触发大量消息。电子邮件新闻通讯工具需要能够处理这种消息量。所有这些电子邮件都可以添加到一个推送队列中,并给工作者提供发送给谁以及发送什么的指令。每封电子邮件都会逐个处理,直到所有电子邮件都发送完毕。

下面的图显示了任务队列,其中消息首先进入队列,然后被处理。然后,新任务被添加到另一个队列中:

图片

图 1.11:事件和任务队列

通过这样,我们已经查看并回顾了两个典型用例。在每一个用例中,RabbitMQ 的优势都显而易见。我们将在下一节中通过探索消息队列的优势使其更加明显。

探索消息队列的优势

在分布式系统中,各种应用程序之间的通信起着重要作用。有许多可以使用消息队列的例子,所以让我们突出消息队列在微服务架构中的某些特性和优势:

  • 开发和维护变得简单:将应用程序分割成多个服务允许分离责任,并给开发者提供了在任意选择的任何语言中为特定服务编写代码的自由。这将更容易维护编写的代码并对系统进行更改;当更新单个认证方案时,只需为测试添加认证模块的代码,而不会干扰任何其他功能。

  • 故障隔离:故障可以隔离到单个模块,因此不会影响其他服务。例如,一个报告服务暂时无法使用的应用程序不会影响认证或支付服务。作为另一个例子,对报告服务进行更改仍然允许客户执行基本交易,即使他们无法查看报告。

  • 提高速度和生产力:不同的开发者可以同时在不同的模块上工作。除了加快开发周期外,微服务和消息队列的使用也对测试阶段产生了影响。这是因为每个服务都可以单独测试,以确定整个系统的就绪状态。

  • 提高可扩展性:微服务还允许随意轻松地进行扩展。如果消息队列在增长,可以添加更多消费者。只需向一个服务添加新组件,而无需更改任何其他服务,这很容易做到。

  • 易于理解:由于微服务架构中的每个模块都代表一个单一的功能,因此了解任务的相关细节很容易。例如,为单个服务聘请顾问不需要他们了解整个系统。

现在已经拥有了足够的知识去冒险,所以现在是深入到为本书其余部分设定场景的 RabbitMQ 场景公司的最佳时机。

一个 RabbitMQ 场景

CC 是一家具有巨大潜力的新出租车公司。今天,公司只有两名出租车司机和两名开发者,但他们希望在即将到来的这一年里大幅扩张。CC 已经用 Ruby 构建了一个网站,并从后端开始,也是用 Ruby 编写的,用于在数据库中存储 CC 的行程。CC 还有一些用 Python 编写的脚本,用于生成路线报告。

到目前为止,CC 的系统运行如下:

  • 公司的网站和博客运行在 Ruby 上。

  • 存储路线数据(如行程的起点和终点)的富互联网应用程序是用 Ruby 编写的。

  • 有一个后台办公室向司机发送路线更新,是用 Ruby 编写的。

  • 使用多个临时的 Python 脚本提取和发送数据以生成路线报告。

  • 出租车应用程序是用 Python 编写的。

以下是对旧架构的说明:

图片

图 1.12:CC 软件架构

为什么 CC 在已经繁忙的环境中考虑添加 RabbitMQ?主要原因是 CC 希望向客户提供一项新功能——他们想构建一个处理即时预订的出租车应用程序。CC 还希望能够轻松扩展。计划是构建一个应用程序,用户可以通过智能手机预订汽车,接收预订确认,并查看汽车接近行程的起点。

由于 CC 已经在不同的语言中拥有一些服务,并且由于 CC 希望能够轻松扩展,他们决定使用现成的面向消息的中间件,如 RabbitMQ,以实现应用程序、客户端和后端之间的异步通信。

随着 CC 对 RabbitMQ 的知识和使用的增加,他们将在环境中发现新的机会来利用它。现在,让我们跟随 CC,看看它在与 RabbitMQ 合作的第一步。

准备安装 RabbitMQ

要开始,需要完成以下三个安装和配置步骤:

  • 安装 RabbitMQ 代理

  • 安装管理插件(Web UI)

  • 配置 vhost 和用户

让我们从安装代理程序开始!

安装代理程序

CC 在其生产服务器上运行 Ubuntu Linux。一位开发者有 macOS 和 Linux,而另一位则是全部 Windows。这种异构性对 RabbitMQ 来说不是问题,因为它可以在所有这些操作系统上本地运行。

RabbitMQ 为所有支持的操作系统提供完整的在线安装指南,可以在以下位置找到:www.rabbitmq.com/download.html。本书包含 Debian/Ubuntu 的安装说明,其中 RabbitMQ 是从apt仓库安装的。它还包含本章后面的 Docker 安装说明。

Ubuntu 上的 RabbitMQ 安装

安装 RabbitMQ 所需的步骤相对较少。它们如下:

  1. 更新 Ubuntu。

  2. 下载并安装仓库密钥。

  3. 确保密钥在仓库中。

  4. 从软件仓库安装 RabbitMQ。

在开始下载过程之前,请确保 Ubuntu 是最新的。请确保操作系统正在使用所有软件的最新版本,因为过时的依赖项会创建安全漏洞。

运行apt update命令以下载已安装软件的最新版本:

apt upgrade

RabbitMQ 需要几个软件包。通过运行以下命令来验证系统上是否有curlapt-transport-httpsGnuPG

sudo apt install curl gnupg -y
sudo apt install apt-transport-https

-y选项接受这些依赖项的任何许可证。Ubuntu 会安装所有必需的子包。

通过运行以下任何命令来发现操作系统的名称:

  • cat /etc/os-release

  • lsb_release -a

  • hostnamectl

版本名称是非技术性的。之前的名称包括focalbionic。Ubuntu 默认不包含 RabbitMQ,因此在继续之前必须将其添加到仓库密钥中。在终端中执行以下命令集:

curl -fsSL https://github.com/rabbitmq/signing-keys/releases/download/2.0/rabbitmq-release-signing-key.asc 
sudo apt-key add -
sudo tee /etc/apt/sources.list.d/bintray.rabbitmq.list <<EOF
deb https://dl.bintray.com/rabbitmq-erlang/debian [os release name] erlang
deb https://dl.bintray.com/rabbitmq/debian [os release name] main
EOF

这些命令在添加代理和 Erlang 的适当操作系统包之前,会下载密钥并将其添加到存储库列表中。

RabbitMQ 是用 Erlang 编写的,这是一种功能语言,它为创建分布式网络提供了强大的内置支持。开发者维护了一个列表,列出了语言的最小版本,这些版本适用于代理的最新支持版本。在撰写本文时,RabbitMQ 3.8 支持 Erlang 21.3 到 23。

现在,RabbitMQ 可以正确安装了。

虽然使用 RabbitMQ 并非绝对必要,但鼓励探索这种强大的语言和平台。您可以在 www.erlang.org/ 上了解更多关于 Erlang 的信息。或者,您可以考虑将 Elixir 作为 Erlang 虚拟机VM)的可选语言。您可以在 elixir-lang.org 上了解更多信息。

运行以下命令来安装 RabbitMQ:

sudo apt install -y rabbitmq-server
sudo apt install librabbitmq-dev 

librabbitmq-dev 库包括一个用于与代理交互的客户端。然而,服务器可能只是唯一的要求。

RabbitMQ 在 Docker 上的安装

Docker 容器允许分离和控制资源,而不会风险损坏操作系统。有关安装 Docker 的说明,请参阅官方网站:docs.docker.com/get-docker/。安装 Docker 后,拉取 RabbitMQ 镜像:

docker pull rabbitmq

使用合理的默认值运行代理:

docker run -d --hostname my-rabbit --name my-rabbit -p 5672:5672 -p 15672:15672 -e RABBITMQ_ERLANG_COOKIE='cookie_for_clustering' -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password  --name some-rabbit rabbitmq:3-management

需要创建一个 Docker 容器,以便可以从 localhost 访问它,并启用管理控制台。这将在稍后被发现。

启动 RabbitMQ

从存储库安装 RabbitMQ 服务器也会安装一套用于首次启动服务器的命令行工具。这是通过执行以下命令完成的:

rabbitmq-server start 

服务器在前台启动。要将代理作为服务运行,请使用以下命令:

sudo systemctl enable rabbitmq-server
sudo systemctl start rabbitmq-server
sudo systemctl status rabbitmq-server 

systemctl 命令也可以用于管理 Ubuntu 中的服务。最终命令的输出应显示代理正在运行。如果未显示,请参阅 RabbitMQ 文档 (www.rabbitmq.com/troubleshooting.html)。

下载示例代码

下载本书的所有示例代码文件。它们可以从 www.packtpub.com 购买。如果您在其他地方购买了此书,请访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

验证 RabbitMQ 代理是否正在运行

现在,通过使用 status service 命令来验证 RabbitMQ 代理实际上是否正在运行。

在终端中写下以下行:

$ sudo service rabbitmq-server status
  rabbitmq-server.service - RabbitMQ broker
   Loaded: loaded (/lib/systemd/system/rabbitmq-server.service; enabled; vendor preset: enabled)
  Drop-In: /etc/systemd/system/rabbitmq-server.service.d
           └─10-limits.conf, 90-env.conf
   Active: active (running) since Mon 2019-04-29 13:28:43 UTC; 1h 43min ago
  Process: 27474 ExecStop=/usr/lib/rabbitmq/bin/rabbitmqctl shutdown (code=exited, status=0/SUCCESS)
 Main PID: 27583 (beam.smp)
   Status: "Initialized"
    Tasks: 87 (limit: 1121)
   CGroup: /system.slice/rabbitmq-server.service
           ├─27583 /usr/lib/erlang/erts-10.2.2/bin/beam.smp -W w -A 64 -MBas ageffcbf -MHas ageffcbf -MBlmbcs 512 -MHlmbcs 512 -MMmcs 30 -P 1048576 -t 5000000 
           ├─27698 /usr/lib/erlang/erts-10.2.2/bin/epmd -daemon
           ├─27854 erl_child_setup 1000000
           ├─27882 inet_gethost 4
           └─27883 inet_gethost 4

Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:   ##  ##
Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:   ##  ##      RabbitMQ 3.7.14\. Copyright (C) 2007-2019 Pivotal Software, Inc.
Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:   ##########  Licensed under the MPL.  See https://www.rabbitmq.com/
Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:   ######  ##
Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:   ##########  Logs: /var/log/rabbitmq/rabbit@test-young-mouse-01.log
Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:                     /var/log/rabbitmq/rabbit@test-young-mouse-01_upgrade.log
Apr 29 13:28:42 test-young-mouse-01 rabbitmq-server[27583]:               Starting broker...
Apr 29 13:28:43 test-young-mouse-01 rabbitmq-server[27583]: systemd unit for activation check: "rabbitmq-server.service"
Apr 29 13:28:43 test-young-mouse-01 systemd[1]: Started RabbitMQ broker.
Apr 29 13:28:43 test-young-mouse-01 rabbitmq-server[27583]:  completed with 9 plugins.

包安装文件的默认文件夹是 /etc/rabbitmq 用于配置文件,/usr/lib/rabbitmq 用于应用程序文件,/var/lib/rabbitmq 用于数据文件 (mnesia)。

查看 RabbitMQ 的运行进程,找到正在运行的服务包装器和 Erlang VM(也称为 BEAM),如下所示:

$ pgrep -fl rabbitmq
27583 beam.smp

$ ps aux | grep rabbitmq
ubuntu   10260  0.0  0.1  14856  1004 pts/0    S+   15:13   0:00 grep --color=auto rabbitmq
rabbitmq 27583  0.5  8.5 2186988 83484 ?       Ssl  13:28   0:36 /usr/lib/erlang/erts-10.2.2/bin/beam.smp -W w -A 64 -MBas ageffcbf -MHas ageffcbf -MBlmbcs 512 -MHlmbcs 512 -MMmcs 30 -P 1048576 -t 5000000 -stbt db -zdbbl 128000 -K true -- -root /usr/lib/erlang -progname erl -- -home /var/lib/rabbitmq -- -pa /usr/librabbitmq/lib/rabbitmq_server-3.7.14/ebin  -noshell -noinput -s rabbit boot -sname rabbit@test-young-mouse-01 -boot start_sasl -config /etc/rabbitmq/rabbitmq -kernel inet_default_connect_options [{nodelay,true}] -sasl errlog_type error -sasl sasl_error_logger false -rabbit lager_log_root "/var/log/rabbitmq" -rabbit lager_default_file "/var/log/rabbitmq/rabbit@test-young-mouse-01.log" -rabbit lager_upgrade_file "/var/log/rabbitmq/rabbit@test-young-mouse-01_upgrade.log" -rabbit enabled_plugins_file "/etc/rabbitmq/enabled_plugins" -rabbit plugins_dir "/usr/lib/rabbitmq/plugins:/usr/lib/rabbitmq/lib/rabbitmq_server-3.7.14/plugins" -rabbit plugins_expand_dir "/var/lib/rabbitmq/mnesia/rabbit@test-young-mouse-01-plugins-expand" -os_mon start_cpu_sup false -os_mon start_disksup false -os_mon start_memsup false -mnesia dir "/var/lib/rabbitmq/mnesia/rabbit@test-young-mouse-01" -kernel inet_dist_listen_min 25672 -kernel inet_dist_listen_max 25672
rabbitmq 27698  0.0  0.1   8532  1528 ?        S    13:28   0:00 /usr/lib/erlang/erts-10.2.2/bin/epmd -daemon
rabbitmq 27854  0.0  0.1   4520  1576 ?        Ss   13:28   0:00 erl_child_setup 1000000
rabbitmq 27882  0.0  0.1   8264  1076 ?        Ss   13:28   0:00 inet_gethost 4
rabbitmq 27883  0.0  0.1  14616  1808 ?        S    13:28   0:00 inet_gethost 4

有可能,当 RabbitMQ 运行时,一个名为epmd的进程也在运行。这是 Erlang 端口映射器守护进程,负责协调集群中的 Erlang 节点。即使集群中的 RabbitMQ 应用程序没有运行,它也会启动。

注意,默认情况下,代理服务配置为在 Linux 主机启动时自动启动。

跳过安装和配置 RabbitMQ 的麻烦,并使用托管 RabbitMQ 解决方案。CloudAMQP 是托管 RabbitMQ 集群的最大提供商:www.cloudamqp.com

安装管理插件(Web UI)

RabbitMQ 默认不安装管理控制台,但本例中使用的可选基于 Web 的插件使得查看正在运行的 RabbitMQ 实例变得容易。

Debian 软件包安装了几个脚本。其中之一是rabbitmq-plugins。它的目的是允许我们安装和删除插件。使用它来安装管理插件,如下所示:

$ sudo rabbitmq-plugins enable rabbitmq_management 
Enabling plugins on node rabbit@host:
rabbitmq_management
The following plugins have been configured:
 rabbitmq_consistent_hash_exchange
 rabbitmq_event_exchange
 rabbitmq_federation
 rabbitmq_management
 rabbitmq_management_agent
 rabbitmq_shovel
 rabbitmq_web_dispatch
Applying plugin configuration to rabbit@host...
The following plugins have been enabled:
  rabbitmq_management
  rabbitmq_management_agent
  rabbitmq_web_dispatch

是的,就这么简单!

使用 Web 浏览器通过导航到http://<hostname>:15672来访问管理控制台的主页,如下面的截图所示:

图片

图 1.13:管理控制台的登录屏幕

请继续关注下一集——创建和配置用户!

配置用户

Debian 软件包安装的脚本之一是rabbitmqctl,这是一个用于管理 RabbitMQ 节点并用于配置代理所有方面的工具。使用它来配置代理中的管理员用户,如下所示:

$ sudo rabbitmqctl add_user cc-admin taxi123
Adding user "cc-admin" ...

$ sudo rabbitmqctl set_user_tags cc-admin administrator
Setting tags for user "cc-admin" to [administrator] ...

默认情况下,RabbitMQ 附带一个使用 guest 密码认证的 guest 用户。将其密码更改为其他密码,如下所示:

$ sudo rabbitmqctl change_password guest guest123

返回管理控制台登录屏幕,我们可以使用用户名cc-admin和密码taxi123登录。

欢迎屏幕提供了对代理内部结构的概述,如下面的截图所示:

图片

图 1.14:管理控制台的主仪表板

注意,在此阶段,cc-admin用户无法在任何 vhost 中检查任何交换机或队列。目前,必须创建另一个用户用于开发目的,以便应用程序可以连接到 RabbitMQ。

创建名为cc-dev的用户,如下所示:

$ sudo rabbitmqctl add_user cc-dev taxi123
Adding user "cc-dev" ...

如本章前面所讨论的,RabbitMQ 支持 vhosts 的概念,这是不同用户可以有不同的访问权限的地方。CC 开发环境将有一个 vhost,也称为 vhost。在未来的任何其他环境中(如 QA 环境)创建的任何内容都将与 vhost 中的内容隔离。在 RabbitMQ 的后续版本(3.7+)中,可以对每个 vhost 的队列数量和并发客户端连接数设置限制。

创建一个名为cc-dev-vhost的 vhost,如下所示:

$ sudo rabbitmqctl add_vhost cc-dev-vhost
Adding vhost "cc-dev-vhost" ...

这将创建一个用户和一个用于开发的虚拟主机。

配置专用虚拟主机

RabbitMQ 自带一个默认的虚拟主机/,访客用户对其拥有完全权限。虽然这对于快速测试来说很方便,但建议创建一个专门的虚拟主机来保持关注点的分离,这样就可以完全删除虚拟主机并从头开始,而不会产生意外的后果。

目前,cc-admincc-dev用户都没有在cc-dev-vhost上执行任何操作的权限。你可以通过授予虚拟主机完全权限来修复这个问题,如下所示:

$ sudo rabbitmqctl set_permissions -p cc-dev-vhost cc-admin ".*" ".*" ".*"
Setting permissions for user "cc-admin" in vhost "cc-dev-vhost" ... $ sudo rabbitmqctl set_permissions -p cc-dev-vhost cc-dev ".*" ".*" ".*"
Setting permissions for user "cc-dev" in vhost "cc-dev-vhost" ...

为了回顾刚才所做的工作,大部分命令都很直接,但".*" ".*" ".*"部分看起来有点神秘,所以让我们来分析一下。

这是对考虑的虚拟主机的三个权限组合,它为考虑的用户和虚拟主机在指定的资源上授予了配置写入读取权限。资源包括交换和队列,由匹配其名称的正则表达式指定。在这种情况下,通过.*正则表达式请求的任何资源都是允许的。

授予的实际命令取决于资源类型和授予的权限。有关 RabbitMQ 支持的完整访问控制策略列表,请参阅www.rabbitmq.com/access-control.html

作为所有命令行的替代方案,转向管理控制台的用户管理功能。点击控制台的“管理员”标签,然后在“用户”标签中点击cc-dev用户,查看以下截图所示的信息。从命令行设置的整个用户配置在管理控制台中可见,并可进行编辑:

图 1.15:从 RabbitMQ 管理控制台进行用户管理

通过在管理控制台中点击特定用户的名称,可以找到单个用户的详细信息:

图 1.16:管理控制台中单个用户的详细信息

RabbitMQ 代理和管理插件(Web UI)已安装,并已配置虚拟主机和用户。

摘要

本章探讨了消息传递的架构和设计承诺,包括 AMQP 和 RabbitMQ 如何实现这些承诺。此外,还发现了出租车代理机构 Complete Car 决定在其软件环境中引入 RabbitMQ 的原因。最后,安装了一个 RabbitMQ 代理,并为它配置了用户和多个虚拟主机。在掌握了消息队列和 RabbitMQ 的基本理解之后,下一章将在此基础上构建,并探讨 Complete Car 出租车应用程序背后的架构。

是时候动手编写代码了。转向下一章,开始构建一个由 RabbitMQ 驱动的应用程序!

第二章:创建出租车应用程序

在日常对话中,人们互相问候,交换闲聊,然后最终结束对话并继续前行。在 RabbitMQ 的轻量级通道中,低级 TCP 连接以同样的方式工作。将要通过 RabbitMQ 交换消息的应用程序需要与消息代理建立永久连接。当建立此连接时,需要创建一个通道,以便执行面向消息的交互,例如发布和消费消息。

在演示了这些基础知识之后,本章将介绍经纪人如何使用交易所来确定每条消息应该发送到何处。交易所就像邮递员一样:它将消息递送到适当的队列(邮箱)供消费者在稍后找到。

基本 RabbitMQ 概念如下所示:

图 2.1:基本 RabbitMQ 概念

到本章结束时,你将深入理解完整汽车CC)平台背后的应用程序架构以及它们如何通过 RabbitMQ 发送第一条消息。这需要介绍两种不同类型的交易所:直接交换,它将消息发送到单个队列,以及主题交换,它根据模式匹配路由键将消息发送到多个队列。

为了获得最佳开始,以下主题将得到介绍:

  • CC 背后的应用程序架构

  • 建立与 RabbitMQ 的连接

  • 发送第一条消息

  • 添加主题消息

让我们开始吧!

技术要求

本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition/tree/master/Chapter02

CC 背后的应用程序架构

CC 需要一款供出租车司机使用的应用程序和一款供客户使用的应用程序。客户应能够通过应用程序请求出租车,出租车司机应能够接受请求(行程):

图 2.2:客户通过 CC 应用程序请求出租车

客户应能够输入关于行程起点和终点的信息。活跃的司机收到请求并能够接受它们。最终,客户应能够在整个行程中跟踪出租车的位置。

下图显示了 CC 想要实现的消息架构:

图 2.3:CC 的主要应用程序架构

如前图所示,此流程可以用 10 个步骤来解释:

  1. 客户使用 CC 的移动应用程序预订出租车。现在,请求从移动应用程序发送到应用程序服务。此请求包含客户想要预订的行程信息。

  2. 应用程序服务将请求存储在数据库中。

  3. 应用程序服务将有关行程的信息消息添加到 RabbitMQ 的一个队列中。

  4. 已连接的出租车订阅消息(预订请求)。

  5. 一辆出租车通过向 RabbitMQ 发送消息来响应客户。

  6. 应用程序服务订阅消息。

  7. 再次,应用程序服务将信息存储在数据库中。

  8. 应用程序服务将信息转发给客户。

  9. 出租车应用程序开始自动以给定间隔将出租车的地理位置发送到 RabbitMQ。

  10. 然后,将出租车的位置直接通过 WebSockets 传递给客户的移动应用程序,以便他们知道出租车何时到达。

让我们首先仔细看看前面图中所示的步骤1234,其中客户请求出租车(消息发布到 RabbitMQ),出租车司机接收请求(从 RabbitMQ 消费消息)。

建立到 RabbitMQ 的稳固连接

如第一章“兔子跃然纸上”中所述,应用程序服务器和 RabbitMQ 之间必须建立物理网络连接。高级消息队列协议AMQP)连接是客户端和代理之间的链接,执行底层网络任务,包括初始身份验证、IP 解析和网络:

图片

图 2.4:应用程序和 RabbitMQ 之间的 AMQP 连接

每个 AMQP 连接维护一组底层通道。通道重用连接,无需重新授权和打开新的 TCP 流,使其更有效率。

以下图示说明了应用程序与 RabbitMQ 之间连接中的一个通道:

图片

图 2.5:通道允许您更有效地使用资源

与创建通道不同,创建连接是一个成本较高的操作,非常类似于数据库连接。通常,数据库连接是池化的,其中每个池实例由单个执行线程使用。AMQP 的不同之处在于,单个连接可以通过多个复用通道被许多线程使用。

AMQP 连接的握手过程至少需要七个 TCP 数据包,使用 TLS 时甚至更多。如果需要,通道可以更频繁地打开和关闭:

  • AMQP 连接:7 个 TCP 数据包

  • AMQP 通道:2 个 TCP 数据包

  • AMQP 发布:1 个 TCP 数据包(对于更大的消息可能有更多)

  • AMQP 关闭通道:2 个 TCP 数据包

  • AMQP 关闭连接:2 个 TCP 数据包

  • 总计 14-19 个数据包(包括确认包)

以下图示说明了发送到连接和通道的信息概述:

图片

图 2.6:AMQP 连接的握手过程

在应用程序服务和 RabbitMQ 之间建立一个单一的长连接是一个良好的开始。

必须决定使用哪种编程语言和客户端库。本书的前几个示例是用 Ruby 编写的,并使用 Bunny 客户端库(github.com/ruby-amqp/bunny)来发布和消费消息。Ruby 是一种易于阅读和理解的语言,即使你对它不熟悉。

应用程序必须配置为使用特定的连接端点,通常称为连接字符串;例如,主机和端口。连接字符串包含建立连接所需的信息。AMQP 分配的端口号是5672。可以通过 AMQPS 使用 TLS/SSL 加密的 AMQP;它是分配端口号5671的 AMQP 协议的安全版本。

该库是打开目标 IP 地址和端口的 TCP 连接的元素。连接参数已被添加为一个名为RABBITMQ_URI的环境变量的 URI 字符串。AMQP URI 没有 URI 标准,但此格式被广泛使用:

 RABBITMQ_URI="amqp://user:password@host/vhost"

根据 Ruby(Bunny)文档,连接到 RabbitMQ 很简单。这段代码被分为代码块,可以在本章后面找到:

  1. 添加在第一章“兔子跃动起来”中设置的用户名、密码和vhost,然后将字符串添加到机器上的环境变量中:
RABBITMQ_URI="amqp://cc-dev:taxi123@localhost/cc-dev-vhost"
  1. 需要引入bunny客户端库:
# Require client library
require "bunny"
  1. 从环境变量中读取连接 URI 并启动连接:
connection = Bunny.new ENV['RABBITMQ_URI']
# Start a session with RabbitMQ 
connection.start

到目前为止,这似乎很简单,但 CC 需要能够优雅地处理失败的生成级代码。如果 RabbitMQ 没有运行会怎样?显然,如果整个应用程序都崩溃了,那就很糟糕。如果需要重启 RabbitMQ 呢?CC 希望其应用程序在出现任何问题时都能优雅地恢复。实际上,CC 希望其应用程序无论整个消息子系统是否工作都能继续运行。用户体验必须平滑、易于理解,并且可靠。

总结来说,CC 希望实现的行为如下:

  • 如果与 RabbitMQ 的连接丢失,它应该自动重新连接。

  • 如果连接断开,发送或获取消息应该优雅地失败。

当应用程序连接到代理时,它需要处理连接失败。没有网络总是可靠的,配置错误和错误也会发生;代理可能已经关闭,等等。虽然不是自动的,但在这个情况下,错误检测应该在过程早期发生。

要处理 Bunny 中的 TCP 连接失败,必须捕获异常:

begin
  connection = Bunny.new ENV['RABBITMQ_URI']
  connection.start
rescue Bunny::TCPConnectionFailed => e
  puts "Connection to server failed"
end

如果应用程序无法从网络连接失败中恢复,检测网络连接失败几乎是无用的。恢复是错误处理的重要部分。

一些客户端库提供自动连接恢复功能,包括消费者恢复。对已关闭通道的任何尝试操作都将失败并抛出异常。如果 Bunny 检测到 TCP 连接故障,它将尝试每 5 秒重新连接一次,没有关于重连尝试次数的限制。可以通过在Bunny.new中添加automatic_recovery => false来禁用自动连接恢复。此设置仅在您以其他方式重新连接或测试连接字符串时使用。

消息可以在不同的语言、平台和操作系统之间发送。您可以从多种不同语言的客户端库中进行选择。客户端库有很多,但以下是一些推荐的:

  • Python:Pika

  • Node.js:amqplib

  • PHP:php-amqplib

  • Java:amqp-client

  • Clojure:Langohr

本节展示了 CC 如何建立与 RabbitMQ 的连接。我们演示了为什么推荐长期连接以及如何处理一些常见错误。现在,是时候在连接中创建一个通道了。

与通道一起工作

每个 AMQP 协议相关的操作都在通道上发生。通道实例是由连接实例创建的。如上所述,通道是在(TCP)连接内的虚拟(AMQP)连接。客户端执行的所有操作都在通道上,队列在通道上声明,消息通过通道发送。

通道本身并不存在;它总是在连接的上下文中:

# Declare a channel
channel = connection.create_channel

一旦连接关闭或发生通道错误,连接中的通道就会被关闭。客户端库允许我们观察并响应通道异常。

通常在通道级别而不是在连接级别抛出更多异常。通道级别的异常通常指示应用程序可以从中恢复的错误,例如,当它没有权限,或者尝试从一个已删除的队列中消费时。对已关闭通道的任何尝试操作都将通过异常失败。

尽管通道实例在技术上是无线程安全的,但强烈建议避免多个线程同时使用同一个通道。

CC 现在能够以线程安全和异常安全的方式连接到 RabbitMQ 代理、打开通道并发出一系列命令。现在是时候在此基础上构建了!

构建出租车请求工具

现在,是时候构建消息流了。

首先,客户将从移动应用程序向应用程序服务发送一个简单的 HTTP 请求。此消息将包含元信息,如时间戳、发送者和接收者 ID 以及目的地和请求的出租车 ID。

消息流看起来可能如下所示:

图片

图 2.7:CC 主应用程序的前端/后端交互

应用程序服务将信息存储在数据库中,以便所有数据在后续状态中都对数据分析脚本可见。

在这些示例中,没有处理数据在数据库中的存储方式,因为这不是本章所关注的主题。最简单的方法是允许应用程序服务将信息添加到数据库中。另一种选择是将应用程序服务卸载,并将新消息放入数据库和应用程序服务之间的消息队列中,让另一个服务订阅这些消息并处理它们;也就是说,将它们存储在数据库中。

以下图示说明了移动设备、应用程序服务和 RabbitMQ 之间的流程:

图片

图 2.8:移动设备、应用程序服务和 RabbitMQ 之间的流程

关于我们主要流程的讨论,第一章,“兔子跃然于生”,详细说明了消息在被路由到队列以供消费后是如何发布到交换的。

路由策略决定了消息将被路由到哪个队列(或多个队列)。路由策略基于路由键(一个自由形式的字符串)以及可能的消息元信息做出决策。将路由键想象成交换用来决定消息如何路由的地址。它还需要在交换和队列之间建立一个绑定,以使消息可以从前者流向后者。

现在,让我们来探讨直接交换。

直接交换

直接交换根据消息路由键将消息传递到队列。一条消息将发送到与消息路由键匹配的绑定路由键的队列(或队列)。

CC 只有两辆车,因此它从一个简单的通信系统开始,其中一个客户可以请求一位司机的出租车。在这种情况下,需要将一条消息路由到该司机的收件箱队列。因此,将要使用的交换-路由策略是直接的,将目标队列名称与消息产生时使用的路由键相匹配,如下面的图示所示:

图片

图 2.9:直接交换将消息路由到特定的队列

直接交换的一个示例用例可能如下所示:

  1. 客户订购名为 taxi.1 的出租车。一个 HTTP 请求从客户的移动应用程序发送到应用程序服务。

  2. 应用程序服务使用路由键 taxi.1 向 RabbitMQ 发送消息。消息路由键与队列名称匹配,因此消息最终进入 taxi.1 队列。

以下图示演示了直接交换消息路由将如何发生:

图片

图 2.10:根据路由键将直接交换路由消息到特定的队列

这可能不是扩展效率最高的方法。实际上,一旦 CC 有更多的车辆,就会对其进行审查,但这是快速开始和启动应用程序的最简单方法。

让我们跟随 CC 创建的第一个代码块作为初始应用程序,并在学习不同概念的同时进行。代码块开头的代码是从连接和通道部分取出的:

  1. 需要使用bunny客户端库。

  2. 从环境变量中读取URI连接并启动连接。

  3. 与 RabbitMQ 启动通信会话。

  4. 声明taxi.1队列。

  5. 声明taxi.1直接交换。

  6. taxi.1队列绑定到taxi-direct交换,使用taxi.1路由键:

# 1\. Require client library
require "bunny"

# 2\. Read RABBITMQ_URI from ENV
connection = Bunny.new ENV["'RABBITMQ_URI"]

# 3\. Start a communication session with RabbitMQ
connection.start
channel = connection.create_channel

def on_start(channel)
 # 4\. Declare a queue for a given taxi
 queue = channel.queue("taxi.1", durable: true)
 # 5\. Declare a direct exchange, taxi-direct
 exchange = channel.direct("taxi-direct", durable: true, auto_delete: true)

 # 6\. Bind the queue to the exchange
 queue.bind(exchange, routing_key: "taxi.1")

 # 7\. Return the exchange
 exchange
end

exchange = on_start(channel)                  

对于每个发送的消息都声明队列和交换有些过度,并且是不必要的,因此强烈建议创建一个处理应用程序设置的方法。这个方法应该创建连接并声明队列、交换等。本例中的方法简单地称为on_start,它声明队列并将交换绑定到队列。

如果在交换上发布消息时交换不存在,它将引发异常。如果交换已经存在,它将不执行任何操作;否则,它实际上会创建一个。这就是为什么每次应用程序启动或发布消息之前声明队列都是安全的。

通道由异常终止。在 CC 的情况下,向不存在的交换发送消息不仅会引发异常,还会终止发生错误的通道。任何尝试使用已终止通道的后续代码都将失败。

除了使用直接类型外,CC 还配置了交换的durable类型、autoDeleteargument属性。这个交换在 RabbitMQ 重启后不应消失,也不应在未使用时消失,这解释了配置中使用的值。

交换声明仅在交换属性相同时才是幂等的。尝试使用不同属性声明已存在的交换将会失败。在交换声明中始终使用一致的属性。如果你正在更改属性,请在声明新属性之前删除交换。同样的规则也适用于队列声明。

在创建交换之后,创建 taxi 队列并将其绑定到它。

队列的声明方法与交换类似,但属性略有不同,如下所示:

  • durable: True – 队列必须在代理重启后仍然声明。

  • autoDelete: False – 保持队列,即使它不再被消费。

  • exclusive: False – 此队列应该能够被其他连接消费(多个应用服务器可以连接到 RabbitMQ,并通过不同的连接访问)。

  • arguments: Null – 无需自定义配置队列。

队列使用其自己的名称作为路由键绑定到交换,以便直接路由策略可以将消息路由到它。当这样做时,向taxi-direct交换发布消息实际上会将消息传递到名称与发布路由键匹配的出租车队列。

如果没有队列绑定到交换,或者如果路由策略找不到匹配的目标队列,则发布到交换的消息将被静默丢弃。作为选项,当无法路由的消息被丢弃时,可以通知到,如后续章节所示。

再次强调,当使用相同的属性时,这些操作是幂等的,因此队列可以安全地声明并绑定到交换,一次又一次。

尽管直接交换在本章中已经介绍过,但 AMQP 0-9-1 代理提供了四种不同类型的交换。根据队列和参数之间的绑定设置,这些交换以不同的方式路由消息。接下来的章节将更详细地介绍其他类型的交换。现在,这里简要说明每种交换:

  • 扇出:消息将被路由到绑定到扇出交换的所有队列。

  • 主题:通配符必须与绑定特定路由模式的路由键匹配。

  • 头部:使用消息头部属性进行路由。

现在,是时候向 RabbitMQ 发送我们的第一条消息了!

发送第一条消息

基本概念和初始设置已经介绍过了,所以让我们直接发送第一条消息!

首先,让我们看看order_taxi方法,它负责发送初始车辆请求的消息:

def order_taxi(taxi, exchange)
  payload = "example-message"
  message_id = rand
 exchange.publish(payload,
    routing_key: taxi,
    content_type: "application/json",
    content_encoding: "UTF-8",
    persistent: true,
    message_id: message_id)
end

exchange = on_start(channel)
order_taxi("taxi.1", exchange)

order_taxi将在用户想要叫出租车时被调用。无法保证收件人是否曾登录过系统,因此对于发送者来说,无法确定目标队列是否存在。最安全的做法是在每条消息发送时声明队列,记住这个声明操作是幂等的,所以如果队列已经存在,它将不会做任何事情。这乍一看可能有些奇怪,但如果发送者想要确保消息不会丢失,那么确保收件人的队列存在是发送者的责任。

当事件之间没有强烈的发生之前关系时,这是 AMQP 中的一种常见模式。重新声明是可行的方案。相反,检查-然后-行动模式是不被推荐的;试图检查交换或队列的存在并不能保证在 AMQP 通常使用的典型分布式环境中成功。

发布消息的方法非常简单;向exchange调用publish。然后,使用队列名称作为路由键(按照直接路由),以及表示实际消息有效载荷的字节数组。还可以添加一些可选的消息属性,可能包括以下内容:

  • content_type字符串):消息以字节数组的形式发布和消费,但并没有真正说明这些字节代表什么。在当前情况下,发布者和消费者都在同一个系统中,因此可以假设内容类型是预期的。话虽如此,始终指定内容类型,以便消息是自包含的;无论哪个系统最终接收或检查消息,都将确切知道它包含的字节数组代表什么。

  • content_encoding字符串):在将字符串消息序列化为字节数组以便发布时使用特定的编码(UTF-8)。再次强调,为了使消息自解释,提供所有必要的元信息,以便它们可以被读取。

  • message_id字符串):正如本书后面所展示的,消息标识符是消息和分布式应用程序中可追溯性的重要方面。在示例中是一个随机生成的消息 ID。

  • persistent布尔值):指定消息是否应该持久化到磁盘。

不要混淆交换和队列的持久性与消息的持久性;存储在持久队列中的非持久消息在代理重启后将消失,留下一个空队列。

此外,在非持久队列中的持久消息在代理重启后也将消失,同样会留下一个空队列。

通过将队列声明为持久的并将消息投递模式设置为持久,确保消息不会丢失。

但如果发送消息失败会怎样,比如当与 RabbitMQ 的连接断开时?

为什么你会使用非持久投递模式呢?难道消息代理如 RabbitMQ 的整个目的不是保证消息不会丢失吗?这是真的,但在某些情况下,这种保证可以放宽。考虑一个场景,其中发布者向代理发送大量非关键消息。在这种情况下使用非持久投递意味着 RabbitMQ 不需要不断访问磁盘,从而在此情况下提供更好的性能。

在继续之前,让我们看看 AMQP 消息的结构。

AMQP 消息结构

以下截图展示了 AMQP 消息的结构,包括刚刚使用的四个 AMQP 消息属性,以及一些新的属性。请注意,此图使用的是字段的规范名称,并且每种语言的实现都会稍微重命名它们,以便它们可以成为有效的名称。例如,content-type在 Java 中变为contentType,而在 Ruby 中变为content_type

图片

图 2.11:AMQP 消息的属性

除了reserved之外,所有这些属性都可以自由使用,除非另有说明,否则 AMQP 代理会忽略它们。在 RabbitMQ 的情况下,代理支持的字段只有user-id字段,该字段被验证以确保它与建立连接的代理用户名称匹配。注意headers属性如何允许我们在没有标准属性符合要求的情况下添加额外的键值对。

下一节将解释消息是如何被消费的。

消费消息

现在,让我们将注意力转向负责检索消息的方法,这是 CC 主架构中的第 4 步,可以在“CC 背后的应用架构”部分找到。

在这里,出租车应用程序可以定期检查队列以获取新消息。这是一种所谓的同步方法。这意味着将处理轮询请求的应用程序线程保持,直到所有挂起的消息都从队列中移除,如图所示:

图 2.13

图 2.12:客户端在代理中请求新消息

一个前端定期轮询后端以获取消息,很快就会在负载方面造成影响,这意味着整体解决方案将开始遭受性能下降。

相反,CC 明智地决定采用服务器推送的方法来构建解决方案。想法是从代理服务器将消息推送到客户端。幸运的是,RabbitMQ 提供了两种接收消息的方式:基于轮询的basic.get方法和基于推送的basic.consume方法。如图所示,消息被推送到消费者:

图 2.13

图 2.13:消费者从代理订阅消息

subscribe方法将消费者添加到队列中,然后订阅接收消息传递。

确保消费者从队列中消费消息,而不是使用基本的 GET 操作。当涉及到资源时,basic.get命令相对昂贵。

使用subscribe,当有新消息准备好且客户端有可用性时,消息将从代理发送到客户端。这通常允许消息的平稳处理。此外,使用subscribe意味着消费者只要声明的通道可用或直到客户端取消它,就会保持连接。

消息处理正在平稳且无障碍地进行,几乎就像什么都没发生一样!当然,直到设置了警报来确认或否定确认某个过程是否按预期或未按计划运行。

确认和否定确认

RabbitMQ 需要知道何时可以将消息视为成功,即在预期中将消息发送给消费者。一旦代理收到响应,代理应从队列中删除消息;否则,队列会溢出。客户端可以通过在收到消息时或消费者完全处理消息时确认消息来回复代理。在任何情况下,一旦消息被确认,它就会从队列中移除。

因此,消费者是否确认消息取决于它是否已经完成处理,或者它确定如果异步处理则没有丢失消息的风险。

为了避免消息可能永远丢失的情况(例如,工作者崩溃、异常等),消费应用程序应在完全完成消息处理之前不确认消息。

当应用程序向代理指示处理失败或无法在此时完成时,应用程序会拒绝消息。否定确认(Nack)告诉 RabbitMQ 消息没有被按指示处理。默认情况下,否定确认的消息会被送回队列以进行另一次尝试。

确认将在第三章中详细说明,向多个出租车司机发送消息

准备好了吗?设定好了吗?是时候 RUN,Rabbit!

运行代码

现在,是时候为消费者设置一些代码了。你将能够从上一节发送第一条消息中识别出大部分代码。

  1. 需要客户端库。

  2. ENV中读取RABBITMQ_URI

  3. 与 RabbitMQ 启动通信会话。

  4. 为特定出租车声明一个队列。

  5. 声明一个直接交换,taxi-direct

  6. 将队列绑定到交换。

  7. 订阅队列。

接下来是初始消费者设置所需的代码:

# example_consumer.rb
# 1\. Require client library
require "bunny"

# 2\. Read RABBITMQ_URI from ENV
connection = Bunny.new ENV["RABBITMQ_URI"]

# 3\. Start a communication session with RabbitMQ
connection.start
channel = connection.create_channel

# Method for the processing
def process_order(info)

  puts "Handling taxi order"
  puts info
  sleep 5.0
  puts "Processing done"
end

def taxi_subscribe(channel, taxi)
  # 4\. Declare a queue for a given taxi
  queue = channel.queue(taxi, durable: true)

  # 5\. Declare a direct exchange, taxi-direct
  exchange = channel.direct("taxi-direct", durable: true, auto_delete: true)

  # 6\. Bind the queue to the exchange
  queue.bind(exchange, routing_key: taxi)

  # 7\. Subscribe from the queue
  queue.subscribe(block: true, manual_ack: false) do |delivery_info, properties, payload|
    process_order(payload)
  end
end

taxi = "taxi.1"
taxi_subscribe(channel, taxi)                            

在这里,subscribe方法中添加了两个需要解释的标志。让我们详细看看它们:

  • block(布尔值,默认false):调用是否应该阻塞调用线程?此选项对于保持脚本的主线程活动可能很有用。它与自动连接恢复不兼容,并且通常不推荐使用。

  • manual_ack(布尔值,默认false):在 CC 的情况下,由于在此阶段丢失消息的风险是可以接受的,系统不会手动确认消息。相反,它会在获取消息后立即通知代理将其视为已确认(关于手动确认的更多内容将在本书的后面部分介绍)。

就这样!CC 现在有一个可测试的工作订单收件箱。接下来,我们将查看当激活出租车运行时的管理控制台。

运行应用程序

当应用程序运行并且服务器连接到 RabbitMQ 时,可以从管理控制台中看到以下已建立的连接:

图片

图 2.14:管理控制台提供连接信息

注意,上游和下游的网络吞吐量被清楚地表示,而那些打开和关闭非常快的通道在管理控制台中很难看到。因此,让我们看看以下交换:

图 2.15:在管理控制台中出现的 taxi-direct 直接交换

用户交换和进出消息的速率显示在管理控制台中。它们被以与接收速度相同的速度消耗,这是一个好兆头,表明当前架构足以满足 CC 的需求,并且消息没有堆积。但是,所有这些未由代码创建的交换是什么?它们从哪里来?以(AMQP 默认)表示的无名交换以及所有以 amq.开头的交换名称都是由 AMQP 规范定义的,因此 RabbitMQ 必须默认提供。现在,关于队列呢?让我们看看:

图 2.16:每个客户到出租车收件箱队列在管理控制台中可见

如预期的那样,每辆出租车都有一个队列,还有一些巧妙的用法统计信息。注意,ack 列是空的,这是意料之中的事,因为消息确认的工作方式就是这样。队列正在接收消息,同时通知 RabbitMQ 它不会进行确认,因此没有与确认消息相关的活动。

如果有足够的 RAM,RabbitMQ 可以处理数百个队列和绑定而不会出现问题,所以多个队列不是问题。

对其架构和实施充满信心,CC 推出了客户到出租车订购子系统。客户可以发送请求,出租车可以处理请求。

CC 通过两款新的环保汽车迅速扩大了公司规模。与之前的解决方案一样,客户需要向特定司机发送订单请求消息。现在,客户请求了一个新功能——向一组出租车发送消息的能力。客户应该能够选择普通出租车或环保出租车。让我们看看 CC 将通过 RabbitMQ 的力量如何实现这一新功能。

添加主题消息

CC 的应用程序允许其出租车通过注册它们感兴趣的主题来组织成组。即将推出的新功能将允许客户向特定主题内的所有出租车发送订单请求。结果证明,这个功能与一个特定的交换路由规则相匹配,不出所料,被称为 topic!这种类型的交换允许我们将消息路由到所有与路由键匹配消息路由键的队列。因此,与最多将消息路由到一个队列的直接交换不同,主题交换可以将消息路由到多个队列。基于主题的路由还可以应用于其他两个场景,例如特定位置的数据,如交通警告广播,或行程价格更新。

路由模式由几个用点分隔的单词组成。遵循的最佳实践是从最一般元素到最具体元素来构建路由键,例如news.economy.usaeurope.sweden.stockholm

主题交换支持严格的路由键匹配,并且还会使用*#作为占位符来执行通配符匹配,分别代表恰好一个单词和零个或多个单词。

以下图表展示了在 CC 应用中如何使用主题交换。注意,单个收件箱队列保持不变,只是通过额外的绑定连接到主题交换,每个绑定都反映了用户的一个兴趣点:

图片

图 2.17:主题交换向生态队列发送主题消息

由于使用相同的收件箱处理所有事务,因此用于检索消息的现有代码无需更改。实际上,这个整个功能只需添加少量代码即可实现。这些添加中的第一个是负责在现有的on_start方法中声明主题交换,如下所示:

def on_start(channel)
  # Declare and return the topic exchange, taxi-topic
  channel.topic("taxi-topic", durable: true, auto_delete: true)
end

这里并没有什么真正新颖或复杂的地方;主要区别在于这个交换被命名为taxi-topic,并且是一个topic类型的交换。发送消息甚至比客户端到出租车功能还要简单,因为没有尝试创建收件人的队列。发送者遍历所有用户以创建和绑定他们的队列是没有意义的,因为只有那些在发送消息时已经订阅了目标主题的用户会收到消息,这正是预期的功能。order_taxi方法如下所示:

# Publishing an order to the exchange
def order_taxi(type, exchange)
  payload = "example-message"
  message_id = rand
  exchange.publish(payload,
                   routing_key: type,
                   content_type: "application/json",
                   content_encoding: "UTF-8",
                   persistent: true,
                   message_id: message_id)
end

exchange = on_start(channel)
# Order will go to any eco taxi
order_taxi('taxi.eco', exchange) 
# Order will go to any eco taxi
order_taxi('taxi.eco', exchange) 
# Order will go to any taxi
order_taxi('taxi', exchange) 
# Order will go to any taxi
order_taxi('taxi', exchange) 

不同之处在于,现在消息是发布到taxi-topic交换的。创建和发布消息的其余代码与客户端到出租车消息的代码完全相同。最后,当新的出租车订阅或取消订阅某些主题时,需要添加信息:

# example_consumer.rb

def taxi_topic_subscribe(channel, taxi, type)
  # Declare a queue for a given taxi
  queue = channel.queue(taxi, durable: true)

  # Declare a topic exchange
  exchange = channel.topic('taxi-topic', durable: true, auto_delete: true)

  # Bind the queue to the exchange
  queue.bind(exchange, routing_key: type)

  # Bind the queue to the exchange to make sure the taxi will get any order
  queue.bind(exchange, routing_key: 'taxi')

  # Subscribe from the queue
  queue.subscribe(block:true,manual_ack: false) do |delivery_info, properties, payload|
    process_order(payload)
  end
end

taxi = "taxi.3"
taxi_topic_subscribe(channel, taxi, "taxi.eco.3")

taxi.3是新的环保出租车,现在准备好接收想要环保车型的客户的订单。

AMQP 规范没有提供任何方法来检查队列的当前绑定,因此无法迭代它们并删除不再需要的绑定,以反映出租车感兴趣的主题的变化。这不是一个严重的问题,因为应用程序无论如何都需要维护这个状态。

RabbitMQ 管理控制台提供了一个 REST API,可以用来执行队列绑定自省,以及其他许多 AMQP 规范未涵盖的功能。更多内容将在后续章节中介绍。

在放置了新的代码之后,一切按预期工作。不需要对代码进行更改来检索新的客户端到出租车订单,因为它们与之前的消息一样,到达同一个收件箱队列。主题消息被出租车正确发送和接收,所有这些都是在最小更改和队列数量没有增加的情况下发生的。当连接到管理控制台时,点击“交换”选项卡;唯一可见的差异是新交换的主题;即taxi-topic

摘要

本章介绍了如何连接到 RabbitMQ 以及如何发送和接收订单消息。汽车订单系统已成功创建,并在 CC 的客户端到出租车和客户端到多辆出租车功能背景下启动了直接和主题交换。

随着完整汽车(Complete Car)的成长,对出租车应用中新增功能的需求也在增加。当 CC(Complete Car)满足用户需求时,接下来会是什么?下一章将解释如何使用通道和队列来扩展应用的功能。

第三章:向多个出租车司机发送消息

第二章,创建出租车应用程序,包含了如何连接到并从 RabbitMQ 接收消息的信息。本章演示了设置预取值,该值指定同时发送给消费者的消息数量。它还涵盖了消费者可以手动确认消息或接收无需确认的消息,前者允许零消息损失的设计。

完整汽车CC)团队请求一个新功能,因为后台希望能够一次性向所有出租车发送信息消息。这是一个介绍扇出交换的绝佳机会,该交换将消息路由到所有绑定到它们的队列,而不考虑路由键。

本章将涵盖以下主题:

  • 与通道和队列一起工作

  • 指定消费者预取计数

  • 确认消息

  • 向所有队列发布

技术要求

本章的代码文件可以在 GitHub 上找到,网址为github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition/tree/master/Chapter03

与通道和队列一起工作

CC 的驱动程序和客户正在享受在第二章创建出租车应用程序中推出的请求出租车功能。首先,解释了向直接交换发布客户订购单辆出租车的消息,然后提供了实现主题交换的说明,客户在订购具有特定要求的出租车时使用该交换。在这两种情况下,消费者绑定到用于消费特定队列的通道。如果该通道关闭,消费者将停止接收消息。因为通道不能重新打开,必须从头开始创建,所以如果出现任何问题,必须重新建立通道及其消费。

让我们回顾一下关于 RabbitMQ 中消费者和队列的一些重要点:

  • 队列可以有多个消费者(除非使用了独占标签)。

  • 每个通道可以有多个消费者。

  • 每个消费者都会使用服务器资源,因此最好确保不要使用过多的消费者。

  • 通道是全双工的,这意味着一个通道可以用于发布和消费消息。

RabbitMQ 代理可以处理的通道或消费者数量没有逻辑限制。然而,有一些限制因素,例如可用内存、代理 CPU 功率和网络带宽。

由于每个通道都会动员内存并消耗 CPU 功率,因此在某些环境中可能需要考虑限制通道或消费者的数量。管理员可以通过使用channel_max参数来配置每个连接的最大通道数。

现在是时候探讨如何通过设置预取计数来最大限度地利用消费者了。

指定消费者预取计数

可以通过预取计数值来指定同时发送给消费者的消息数量。预取计数值用于尽可能多地从消费者那里获取信息。

如果预取计数太小,可能会对 RabbitMQ 的性能产生负面影响,因为平台通常在等待发送更多消息的权限。以下图表显示了一个长时间空闲的示例。示例中预取设置为 1,这意味着 RabbitMQ 将在消息交付、处理和确认完成后才会发送下一条消息。

在示例中,处理时间仅为 5 毫秒,往返时间为 125 毫秒(60 毫秒 + 60 毫秒 + 5 毫秒):

图片

图 3.1:往返时间为 125 毫秒,处理时间仅为 5 毫秒

较大的预取计数会使 RabbitMQ 从单个队列向单个消费者发送许多消息。如果所有消息都发送给单个消费者,它可能会超负荷工作,并使其他消费者处于空闲状态。以下图表显示了消费者在接收大量消息的同时,其他消费者处于空闲状态:

图片

图 3.2:消费者处于空闲状态

第二章创建出租车应用程序中,使用 Ruby 创建了一个连接、通道和消费者。以下代码块展示了如何在 Ruby 中配置预取值:

require "bunny"
connection = Bunny.new ENV["RABBITMQ_URI"]

connection.start
channel = connection.create_channel 
channel.prefetch(1)

注意,示例显示了单个 (1) 位置的预取值。这意味着只有一条消息会被发送给消费者,直到消费者对其进行确认/拒绝确认。默认的 RabbitMQ 预取设置提供了无限缓冲区,以便尽可能多地发送给准备接受消息的消费者。在消费者端,客户端库会缓存消息直到处理完毕。预取设置限制了客户端在确认之前能够接收的消息数量,使它们对其他消费者不可见,并从队列中移除。

RabbitMQ 支持基于通道级别、基于消息的预取计数,而不是基于连接或字节大小的预取。

接下来,我们将探讨如何设置正确的预取值。

设置正确的预取值

在一个或几个快速处理消息的消费者场景中,建议一次性预取许多消息,以使客户端尽可能忙碌。可以通过将总往返时间除以每条消息的处理时间来得到估计的预取值——如果处理时间保持不变且网络行为稳定。

在有多个消费者且处理时间较短的情况下,建议使用较低的预取值。如果预取值设置得太低,消费者大部分时间都会处于空闲状态,等待消息的到来。另一方面,如果预取值过高,一个消费者可能会非常忙碌,而其他消费者则处于空闲状态。

一个典型的错误是允许无限制的预取,其中一个客户端接收所有消息,导致高内存消耗和崩溃,这会导致所有消息重新投递。

在有多个消费者和/或较长的消息处理时间的情况下,建议将预取值设置为 1(1),以均匀地将消息分配给所有消费者。

如果客户端设置为自动确认消息,预取设置将没有效果。

第二章中所述,创建出租车应用程序,消费者可以向代理确认消息的投递。现在是时候探讨如何实现这一点了。

确认消息

在代理和消费者之间传输的消息可能会在连接失败的情况下丢失,并且重要消息可能需要重新传输。确认让服务器和客户端知道何时重新传输消息。

确认消息投递有两种可能的方式——一旦消费者收到消息(自动确认,auto-ack),以及当消费者发送回确认(显式/手动确认)。在需要高消息速度、连接可靠且丢失消息不是问题的情况下,最好使用自动确认。

与自动确认相比,在消息上使用手动确认可能会对系统性能产生影响。如果目标是快速吞吐量,应禁用手动确认,并改用自动确认。

在 CC 的情况下,消息丢失的风险是不可接受的,因此前面的代码已被修改为将确认设置为手动,从而可以确定何时确认消息:

queue.subscribe(block: true, manual_ack: true) 

消息在完全处理完毕后也必须进行确认:

channel.acknowledge(delivery_info.delivery_tag, false)

如所示,手动确认的方法需要两个参数——第一个是投递标签,第二个在需要同时确认多个消息时是必需的。投递标签是服务器用于标识投递的通道特定数字。消费者必须在接收消息的同一通道上确认消息至关重要,因为如果不这样做,将会引发错误。

修改代码并运行应用程序后,ack 列的比率显示为非零。这是因为现在使用了手动确认,并且 RabbitMQ 客户端现在通过有线向代理发送 ack 消息。这会在带宽使用和总体性能方面产生成本;然而,如果优先考虑的是确保消息处理成功而不是速度,这是完全可以接受的。

如果存在消息处理可能失败且代理最终需要重新投递的风险,请使用手动确认。除非消息被拒绝或通道关闭,否则未确认消息的重传不会立即发生。

CC 与 RabbitMQ 的旅程变得越来越激动人心,这使得团队请求一个新功能,可以直接向所有出租车司机发送重要信息。让我们看看如何实现这个新功能!

发布到所有队列

拿到新的功能请求后,CC 编程团队提出了以下图中所示的新整体消息架构。后台应用将连接到 RabbitMQ,以便向所有出租车司机发布信息消息:

图片

图 3.3:架构中的后台应用

为了推广这个功能,一种方法可能是使用已经存在的主题消息并创建一个所有司机都会订阅的特殊主题。然而,AMQP 协议提供了一个更干净、更简单的方法——fanout 交换。

Fanout 交换

Fanout 交换接收所有传入的消息并将它们发送到所有绑定到它的队列。一个易于理解的例子是当消息需要在许多参与者之间传播时,例如在聊天中(然而,对于纯聊天应用可能还有更好的选择)。

其他例子包括以下内容:

  • 从体育新闻到移动客户端的得分板或排行榜更新,或其他全球事件

  • 在分布式系统中广播各种状态和配置更新

如以下图所示,fanout 交换将接收到的每个消息的副本路由到所有绑定到它的队列。这种模式与 CC 在新功能中追求的公共广播行为完美匹配,即向所有司机发送单个消息的选项:

图片

图 3.4:fanout 交换路由到所有绑定队列

是时候将 fanout 交换添加到 CC 的应用中了。

绑定到 fanout

后台应该能够向所有出租车司机发布单个消息。这条消息可以包括当前的交通信息或晚上将发生的派对信息。因为这种新的广播系统将很少使用,CC 团队对高效连接管理的关注不如对主应用那么大。事实上,对于 fanout 交换的每次交互,连接和断开连接都是可以的,因为如果 RabbitMQ 代理出现暂时性问题,后台应用的重试最终会成功。

要开始在后台使用新的 fanout 交换,必须执行两个步骤:首先,在应用启动时声明 fanout 交换,然后当用户登录时将队列绑定到它。

以下示例中的第五个数字显示了添加到后台服务以在此新交换机上发布消息的代码:

# 1\. Require client library
require "bunny"

# 2\. Read RABBITMQ_URI from ENV
connection = Bunny.new ENV["RABBITMQ_URI"]

# 3\. Communication session with RabbitMQ
connection.start
channel = connection.create_channel

# 4\. Declare queues for taxis
queue1 = channel.queue("taxi-inbox.1", durable: true)

queue2 = channel.queue("taxi-inbox.2", durable: true)

# 5\. Declare a fanout exchange
exchange = channel.fanout("taxi-fanout")

# 6\. Bind the queue 
queue1.bind(exchange, routing_key: "")
queue2.bind(exchange, routing_key: "")

# 7\. Publish a message
exchange.publish("Hello everybody! This is an information message from the crew!", key: "")

# 8\. Close the connection
connection.close 

这段代码中的逻辑应该感觉熟悉。在绑定队列时,使用空字符串作为路由键。这个值并不真正重要,因为扇出交换机不关心路由键。

注意,exchange 在使用之前被声明。这避免了依赖于交换机的隐式存在。如果不这样做,就意味着主应用程序必须运行一次来创建交换机,然后后台服务才能使用它。由于交换机声明是幂等的,它可以在任何时候声明。

特别注意可能使用不同默认值的 AMQP 客户端库;最好是明确指定所有值。

由于出租车收件箱队列仅包括信息消息,因此不使用与直接和主题交换示例中相同的队列。相反,声明了两个新的队列(taxi-inbox.1taxi-inbox.2),并将它们绑定到交换机上。

除非有强有力的保证交换机或队列将预先存在,否则假设它不存在并声明它。安全总是比后悔好,尤其是在 AMQP 鼓励这样做并提供必要的手段时。

在此代码到位后,后台应用程序现在可以向所有司机发送公共信息消息。这是一个巨大的成功,再次强化了 CC 部署 RabbitMQ 并在此基础上构建的决定。现在,让我们运行应用程序。

运行应用程序

在运行应用程序时没有值得注意的特别之处;来自后台的消息成功流向司机的收件箱队列,唯一可见的变化是新建的司机扇出交换机。

这在以下屏幕截图所示的管理控制台中是可见的:

图片

图 3.5:用户队列的扇出交换机在管理控制台中可见

在这一点上,查看任何特定队列的绑定是有趣的。为此,点击“队列”选项卡,然后向下滚动并点击“绑定”以显示隐藏的窗口。

这将显示以下截图中的内容,其中每个队列都有多个绑定——一个用于用户到出租车消息功能,一个用于主题消息,最后一个用于公共广播功能:

图片

图 3.6:每个出租车队列有多个绑定

在结束之前,让我们暂停一下,享受一下现在有一个成功跨平台出租车请求集成的现实。对于在消息系统方面有一点经验的人来说,这可能并不重要;然而,这简直是一个小小的奇迹。多亏了 AMQP 和 RabbitMQ,消息代理可以被任何其他基于 AMQP 的消息代理所取代,并且可以在任何选择的编程语言中添加更多服务。

摘要

本章讨论了消费者的预取和手动确认。介绍了 fanout 交换,以便能够向所有活跃队列广播单个消息。接下来,CC 对其 RabbitMQ 系统有新的计划——他们希望能够以平稳的方式清理旧消息并调整消息投递。他们还希望能够从后台服务向个别司机发送消息。

继续阅读下一章,了解 CC 正在做什么!

第四章:调整消息投递

消息最终陷入队列中会发生什么?它们会消失吗?防止消息无声无息地被丢弃的最佳方法是什么?在本章中,我们将彻底回答这些问题,详细探讨消息存活时间TTL)和死信交换机及队列。本章还将涵盖如果使用强制标志无法将消息路由到特定队列时,代理应该如何反应。此外,本章将解释策略和默认交换机。

期待了解以下主题的重要信息:

  • 处理死信

  • 强制投递

第五章:技术要求

本章的代码文件可以在 GitHub 上找到:github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition/tree/master/Chapter04

处理死信

Complete CarCC)公司,一切进行得非常顺利。随着越来越多的司机加入公司,驾驶员信息消息功能越来越受欢迎。经过几个月的活动,一个事实变得明显:并非所有出租车司机每天都登录应用程序,这导致消息在出租车收件箱队列中堆积。

尽管数据量对系统没有害处,但消息在队列中滞留,可能永远无法处理的想法并不令人满意。想象一下,一名出租车司机在休假几周后登录,被大量过时的消息淹没——这是 CC 公司竭力避免的负面用户体验。

CC 决定通过指定新规则来解决这个问题:一周后,任何未送达的消息将以以下两种方式之一进行处理:

  • 如果是重要信息消息,将会通过电子邮件发送给用户。

  • 如果是关于当前交通状况或其他重要信息的消息,将会被丢弃:

图片

图 4.1:发送重要信息消息,丢弃其他消息

开发者查看 RabbitMQ 提供的消息过期选项,并列出以下可以实施的可能选项:

  • 标准 AMQP 消息过期属性,用于已发布消息

  • 允许用户为每个队列定义消息 TTL 的定制 RabbitMQ 扩展

  • 允许用户为队列本身定义 TTL 的定制 RabbitMQ 扩展

第一个选项很有趣,因为它是一个标准的 AMQP 选项;然而,在阅读更多关于它在 RabbitMQ 中如何支持的内容后,我们发现这些消息只有在达到队列头部或开始位置时才会被丢弃。即使它们已经过期,消息仍然会留在队列中,这会违背他们试图实现的目的。CC 也排除了最后一个选项,因为他们不希望删除队列。这让我们只剩下了第二个选项,即为每个出租车收件箱队列配置一个由 RabbitMQ 强制执行的 TTL,无论队列是否正在被消费。

这一切看起来都很顺利,但消息过期后实际上会发生什么呢?CC 希望消费这些重要的消息以便通过电子邮件发送。那么我们如何实现这一点呢?这正是 RabbitMQ 的死信交换DLX)发挥作用的地方。

死信是指无法投递的消息,这可能是因为目标无法访问,或者因为消息已过期。在 CC 的情况下,达到其 TTL 的消息将变成死信。

RabbitMQ 提供了自动将这些死信路由到特定交换机的选项,这被称为 DLX。由于 CC 希望接收发送到该交换机的消息,他们必须将其绑定到一个队列上,消费它,并处理接收到的消息。这个队列充当死信队列DLQ),死信的最终目的地。

以下图表说明了 CC 打算推出的整体 DLX 架构:

图片

图 4.2:死信处理架构

达到其 TTL 的消息将通过死信交换机重路由到 taxi-****dlx 队列,并由消费者最终处理。

注意,当消息过期时,它们会使用在它们被投递到出租车收件箱队列时的原始路由键发布到 DLX。此行为可以被修改,因为 RabbitMQ 允许定义在消息发布到 DLX 时使用的特定路由键。默认设置对 CC 来说很合适,因为原始路由键是他们用来找出出租车 ID 的有趣信息。因此,DLX 交换机被构建为一个扇出,以便将所有消息路由到 DLQ,无论它们的原始路由键可能是什么。

战斗计划已经准备好了。现在是时候实施它了!

重构队列

实施此架构的第一步是配置出租车队列,使其具有一周的期望 TTL 和等于 taxi-dlx 的死信交换机。

通过使用 RabbitMQ 扩展 AMQP,这可以通过在声明队列时分别定义 'x-message-ttl'"x-dead-letter-exchange" 参数来实现。在 TTL 过期后发布的消息将被重路由到具有给定 x-dead-letter-routing-key 的交换机。

很有诱惑直接跳到代码编辑器,并通过以下参数修改第三章,向多个出租车司机发送消息中编写的 Ruby 代码:

# Declare a queue for a taxi inbox 1
queue1 = channel.queue('taxi-inbox.1', 
  durable: true, 
  arguments:{
    'x-message-ttl'=> 604800000, 
    'x-dead-letter-exchange'=> 'taxi-dlx', 
    'x-dead-letter-routing-key'=> 'taxi-inbox.1'
  }
)

然而,这在几个层面上都是错误的。主要问题是声明将从没有参数的队列更改为具有三个参数的队列。记住,在第二章,创建出租车应用程序中,队列(或交换)的声明只有在所有使用的参数都相同的情况下才是幂等的。声明中的任何差异都会产生异常,并将立即终止通道。

做一个习惯,确保在声明现有队列和交换时始终使用相同的属性/参数。任何差异都会导致错误并终止通道。

另一个问题是在出租车司机登录时才会应用这个更改。这是出租车队列被声明的时候;然而,它不会满足将过期规则应用于所有现有队列,而不依赖于用户操作的要求。最后,还需要考虑的是,如果这些属性在队列声明级别进行配置,则对其中任何一个的任何更改都需要我们删除并重新创建所有队列。显然,TTL 和 DLX 配置是跨切面关注点,应该以更全局的方式配置。这是否可能呢?

答案是肯定的!RabbitMQ 在策略的概念中提供了一个简单而优雅的解决方案来解决这个问题。RabbitMQ 支持定义特定行为的策略,并且可以将这些策略应用于队列或交换。策略不仅在与队列或交换声明时应用,还可以应用于现有的队列或交换。

队列消息的 TTL 和死信交换都可以通过策略进行配置,但只能有一个策略应用于一个队列或交换。因此,CC 将创建一个结合了 TTL 和 DLX 设置的策略,并将其应用于所有出租车收件箱队列。这不能通过 AMQP 协议或使用 RabbitMQ 客户端来实现。相反,RabbitMQ 提供的强大命令行工具是实现所需策略的最佳方式。

通过以下单个命令行操作实现了对现有队列的重构策略:

$ sudo rabbitmqctl set_policy -p cc-dev-vhost Q_TTL_DLX "taxi\.\d+" '{"message-ttl":604800000, "dead-letter-exchange":"taxi-dlx"}' 
--apply-to queues

让我们花点时间来分析前面的命令:

  • sudo rabbitmqctl set_policy: 命令的这一部分使用了set_policy控制命令。

  • -p cc-dev-vhost: 命令的这一部分将消息应用于开发虚拟主机。

  • Q_TTL_DLX: 命令的这一部分命名了策略,使其明显与队列 TTL 和死信交换相关。

  • "taxi\.\d+": 命令的这一部分使用了一些正则表达式,通过选择名称来仅将整个命令应用于出租车队列。

  • '{"message-ttl":604800000, "dead-letter-exchange":"taxi-dlx"}':此命令部分使用了一个由七天毫秒 TTL 和 DLX 名称组成的策略定义。

  • --apply-to queues:此命令部分确保此策略仅应用于队列,这与正则表达式有些重复,但作为安全网的作用是通过对类型而不是名称选择 RabbitMQ 实体。

准备运行此命令?别急——必须创建taxi-dlx交换机并将其绑定到taxi-dlq队列。立即应用此策略意味着将有七天时间来部署缺失的交换机和队列。当然,这足够多了,但聪明的开发者不喜欢在无法避免的情况下与时间赛跑。

不要立即运行此命令,而是花时间创建处理死信的基础设施,并在应用"Q_TTL_DLX"策略之前将其部署到应用中。

策略现在已经设置好了,现在是时候添加一些代码来创建缺失的交换机和队列。

承担消息

必须创建必要的基础设施来处理过期消息。需要声明死信队列以及新的死信扇出交换机。这些需要相互绑定。

需要执行以下操作:

  • 声明taxi-dlq队列。

  • 声明taxi-dlx扇出交换机。

  • taxi-dlq绑定到taxi-dlx扇出交换机。

  • 创建一个订阅taxi-dlq队列的订阅者,该订阅者消费并发送死信邮件。

要实现此行为,只需添加以下代码中的交换机和队列以创建交换机并将队列绑定到它:

  1. 首先声明两个队列,x-message-ttl设置为604800000
queue1 = channel.queue('taxi-inbox.1', durable: true,
  arguments: {'x-message-ttl'=> 604800000, 'x-dead-letter-exchange'=> 'taxi-dlx'})

queue2 = channel.queue('taxi-inbox.2', durable: true,
  arguments: {'x-message-ttl'=> 604800000, 'x-dead-letter-exchange'=> 'taxi-dlx'})
  1. 声明一个扇出交换机taxi-fanout
exchange = channel.fanout('taxi-fanout')
  1. 将两个队列绑定到交换机:
queue1.bind(exchange, routing_key: "")
queue2.bind(exchange, routing_key: "")
  1. 声明一个死信队列,taxi-dlq
taxi_dlq = channel.queue('taxi-dlq', durable: true)
  1. 声明一个死信扇出交换机,taxi-dlx
dlx_exchange = channel.fanout('taxi-dlx')
  1. 现在taxi-dlx需要绑定到taxi-dlq
taxi_dlq.bind(dlx_exchange, routing_key: "")
  1. 最后,发布一条消息:
exchange.publish("Hello! This is an information message!",   key: "")

如您所见,这只是一个标准的扇出交换机声明,以及相关的队列声明和绑定。在第三章实现公共地址系统时使用了相同的逻辑,向多个司机发送消息

为了进一步简化问题,确保在发生异常时记录足够的环境数据。始终考虑在必要时执行特定异常的取证分析所需的信息。

在将此代码部署到应用服务器后,请注意死信交换机和队列已正确创建。现在,是时候设置"Q_TTL_DLX"策略,如下代码所示:

$ sudo rabbitmqctl set_policy 
-p cc-dev-vhost Q_TTL_DLX "taxi-inbox\.\d+ " '{"message-ttl":604800000, "dead-letter-exchange":"taxi-dlx"}' --apply-to queues

Setting policy "Q_TTL_DLX" for pattern "taxi-inbox\.\d+ " to "{\"message-ttl\":604800000, \"dead-letter-exchange\":\"taxi-dlx\"}" with priority "0" ...
...done.

运行此脚本后,使用管理控制台查看用户收件箱队列定义中发生了什么变化。

以下截图显示了这些队列中的几个:

图 4.3:Q_TTL_DLX 策略应用于所有出租车队列

以下屏幕截图演示了 Q_TTL_DLX 策略已应用于出租车队列,而其他队列,如taxi-dlq,则未受到影响:

图 4.4:Q_TTL_DLX 策略应用于 taxi-inbox.1 队列

在管理界面中,点击“管理员”标签页,然后点击“策略”标签页(在右侧)。注意以下屏幕截图中的自定义策略:

图 4.5:Q_TTL_DLX 策略在管理员视图中

在这个阶段,任何将在出租车队列中停留超过 7 天的消息都将被无情地移动到taxi_dlq,被消费,可能被发送电子邮件,并真正地被埋葬!但是,对于在政策实施之前创建的现有消息应该怎么办呢?

很遗憾,对于这个问题没有现成的解决方案,因此必须采取一种相对激进的措施,清除所有非空且没有活跃订阅者的队列。这很粗糙,但这是摆脱当前状况的唯一方法。此外,这是一个可以通过简单的脚本轻松实现的解决方案。

到目前为止,rabbitmqctl脚本已被用于管理 RabbitMQ 代理。下一步需要安装一个新脚本,该脚本包含在第一章中安装的管理控制台,即“A Rabbit Springs to Life”。这个名为rabbitmqadmin的脚本可以通过简单地浏览管理界面中的特定 URL 来下载,即http://localhost:15672/cli/。在遵循显示的下载说明后,将脚本安装在一个对所有用户都可用的地方(在 Linux 机器上通常是/usr/local/bin)。

关于rabbitmqadmin脚本的更多信息可以在www.rabbitmq.com/management-cli.html找到。

以下代码展示了如何创建一个脚本,该脚本将删除所有无消费者的非空队列:

#!/bin/bash

queues_to_purge=`rabbitmqctl list_queues -p cc-dev-vhost name messages_ready consumers | grep "taxi\.[[:digit:]]\+[[:space:]]\+[1-9][[:digit:]]*[[:space:]]\+0" | awk '{ print $1}'`

for queue in $queues_to_purge ; do
    echo -n "Purging $queue ... "
    rabbitmqadmin -V cc-dev-vhost -u cc-admin -p taxi123 purge queue name=$queue
done

注意,rabbitmqctlrabbitmqadmin都被用来达到目标,前者具有以易于解析的方式列出队列特定属性的能力,而后者具有清除队列的能力。在以超级用户身份执行此脚本后,RabbitMQ 代理的状态适合使用,TTL 和 DLX 策略将长期保持这种状态!

CC 现在希望在完成行程后的几分钟内向所有完成出租车行程的客户发送调查问卷。让我们看看如何使用死信交换和 TTL 在 RabbitMQ 中延迟消息投递。

使用 RabbitMQ 的延迟消息

在使用此功能完成工作后,后台意识到他们可以发布具有固定延迟的消息,这样消费者就不会立即看到它们。这对于他们的调查来说是一个完美的功能,因为调查应该在行程完成后 5 分钟发送给客户。AMQP 协议没有原生的延迟队列功能,但可以通过组合消息 TTL 功能和死信功能轻松模拟。

延迟消息插件适用于 RabbitMQ 3.5.3 及更高版本的 RabbitMQ。延迟消息插件为 RabbitMQ 添加了一个新的交换机类型。通过向消息添加延迟头,可以延迟通过该交换机路由的消息。您可以在github.com/rabbitmq/rabbitmq-delayed-message-exchange上了解更多关于插件的信息。

当司机标记行程已完成后,CC 决定将调查请求消息发布到一个延迟队列。所有调查请求消息都被设置为在 TTL(生存时间)为 5 分钟后过期。然后,消息的路由键被更改为与目标队列名称相同。这意味着调查请求消息将最终进入应该发送调查请求的队列。

以下是一个 CC 会使用的代码示例。消息首先被发送到名为work.laterDELAYED_QUEUE。在 300,000 毫秒后,消息被标记为死信并路由到名为work.nowDESTINATION_QUEUE

  1. 我们首先分配变量:
DELAYED_QUEUE='work.later'
DESTINATION_QUEUE='work.now'
  1. 之后,我们定义发布方法。这里发生了很多事情:
  • 首先,声明延迟队列DELAYED_QUEUE,并将x-dead-letter-exchange设置为默认队列。

  • 通过x-dead-letter-routing-key参数设置为DESTINATION_QUEUE来设置用于死信消息的路由键。

  • 消息延迟的毫秒数在消息 TTL 的x-message-ttl参数中指定。

  1. 最后,将消息发布到默认交换机,其中DELAYED_QUEUE用作路由键:
def publish
  channel = connection.create_channel

  channel.queue(DELAYED_QUEUE, arguments: {
    'x-dead-letter-exchange' => '', 
    'x-dead-letter-routing-key' => DESTINATION_QUEUE,
    'x-message-ttl' => 300000
  })

  channel.default_exchange.publish 'message content', routing_key: DELAYED_QUEUE
    puts "#{Time.now}: Published the message"
    channel.close
end
  1. 然后我们定义订阅方法并处理消息:
def subscribe
  channel = connection.create_channel
  q = channel.queue DESTINATION_QUEUE, durable: true
  q.subscribe do |delivery, headers, body|
    puts "#{Time.now}: Got the message"
  end
end
  1. 最后,我们调用这两个方法:
subscribe()
publish()

就这样!调查请求功能已实现。但,当然,立即提出了新的功能请求。后台希望能够向单个司机发送消息,并确保所有司机,包括没有 RabbitMQ 出租车收件箱的司机,都能收到消息。让我们看看 RabbitMQ 中的消息强制投递。

强制投递

到目前为止,CC 的后台团队一直只依赖电子邮件与个人司机互动。CC 最近添加了第三章“向多个出租车司机发送消息”中讨论的由 RabbitMQ 驱动的系统,允许后台向所有司机发送信息消息。他们现在想探索从后台服务向个别司机发送消息的可能性。此外,如果可能的话,CC 希望没有在 RabbitMQ 上设置收件箱队列的司机能够立即收到电子邮件消息。

在消息架构方面,这是一个已知领域——在第二章“创建出租车应用程序”中,为客户端到出租车消息实现了完全相同的模型,如下所示:

图片

图 4.6:后台团队将使用出租车直接交换来发送给司机的直接消息

使用了直接交换。唯一的不同之处在于,与主应用程序不同,后台在发送消息之前不会创建和绑定出租车队列。相反,后台必须以某种方式检测到不存在此类队列,并将消息的投递回退到电子邮件。

不清楚的是如何实现这些要求的第二部分:后台如何检查队列的存在?AMQP 规范没有指定直接的方法来做这件事。RabbitMQ 管理插件公开了一个 REST API,可以用来检查队列的存在,这是一个诱人的方法,但不是 AMQP 默认提供的,这是首选的方法。此外,这可能会使过程暴露于检查-行动类型的竞争条件。

事实上,队列可以在确认它不存在后由另一个进程创建。深入研究 AMQP 规范可以发现一个更优雅处理此问题的功能,即强制投递。mandatory字段是 AMQP 规范的一部分,它告诉 RabbitMQ 如果消息无法路由到队列时如何反应。

考虑到当 AMQP 规范没有提供支持所需功能的方法时,可以使用 RabbitMQ 的管理 REST API。您可以在 RabbitMQ 代理上访问 REST API 文档,网址为http://localhost:15672/api/

当消息在一个设置了mandatory标志为true的交换上发布时,如果消息无法投递到队列,它将被 RabbitMQ 返回。消息无法投递到队列的原因可能是没有队列绑定到交换,或者没有绑定队列具有与交换的路由规则匹配的路由键。在当前情况下,这意味着没有出租车收件箱队列绑定到与出租车 ID 匹配的路由键。

返回消息的技巧是 RabbitMQ 不会同步地将它们作为发布操作的响应返回:它是异步返回的。这意味着对于开发者来说,必须将特定的消息处理器注册到 RabbitMQ,以便接收返回的消息。

这导致以下图中展示的整体架构:

图片

图 4.7:一个专门的处理器负责处理返回的消息

发布到不存在队列的消息会被返回到返回处理器。现在,这个处理器负责确保信息消息以某种方式到达司机——例如,通过电子邮件。

在实现新的后台发送器之前,将先描述默认交换的类型。

RabbitMQ 的默认交换

每次创建队列时,它都会自动绑定到默认交换,其队列名称作为路由键。通过使用队列名称作为路由键向默认交换发布消息,消息最终会到达指定的队列。这也是将在以下代码示例的“实现后台发送器”部分中添加的内容。

这神秘的默认交换是什么?它是一个名为""(一个空字符串)的直接且持久的交换,由 RabbitMQ 为每个虚拟主机自动创建。

为了在管理控制台中使默认交换可见,其空字符串名称被渲染为 AMQP 默认值,如下面的截图所示:

图片

图 4.8:默认交换是几个内置交换之一

如您所见,对于每个虚拟主机,都会自动创建许多其他预定义的交换。它们很容易识别,因为它们的名称以 amq 开头。这些交换仅用于测试和原型设计目的,因此在生产环境中无需使用它们。

向默认交换发送消息是到达特定队列的便捷方式;然而,不要过度使用这种模式。因为它会在生产者和消费者之间创建紧密耦合,因为生产者会知道特定的队列名称。

解释到这里,现在是时候添加必要的代码来构建后台请求的这个功能了,该功能是在没有现有收件箱队列的驱动程序中实现的。

实现后台发送器

CC 的后台现在正在添加对发送给没有出租车收件箱队列的司机的消息的支持,以及返回的消息。Ruby 客户端库等库优雅地支持此功能。以下是需要支持强制将消息发送到出租车收件箱并处理可能返回的消息的代码。

首先,需要 bunny 客户端库,然后设置一个连接和一个通道到 RabbitMQ,如 第二章 所述,创建出租车应用程序

require "bunny"
 connection = Bunny.new ENV["RABBITMQ_URI"]

 connection.start
 channel = connection.create_channel

然后,声明一个默认交换:

exchange = channel.default_exchange

创建了一个返回处理程序,用于处理返回的消息:

exchange.on_return do |return_info, properties, content|
  puts "A returned message!"
end

接下来,声明一个持久的邮箱队列——在这个例子中,命名为 taxi-inbox.100

queue = channel.queue("taxi-inbox.100", durable: true)

订阅来自 RabbitMQ 的消息并向开发者发送简单通知。在此阶段,会发送一封电子邮件,但请注意,这个例子故意保持简短,不包括实际发送电子邮件的方法:

queue.subscribe do |delivery_info, properties, content|
  puts "A message is consumed."
end

消息通过 routing_key 发布以针对具有 mandatory 标志设置为 true 的特定出租车。由于这个队列已被创建并存在,这条消息不应该被返回:

exchange.publish("A message published to a queue that does exist, it should NOT be returned", :mandatory => true, :routing_key => queue.name)

另一条强制消息被发布,但这次是到一个随机队列。这条消息将被返回并由返回处理程序处理:

exchange.publish("A message published to a queue that does not exist, it should be returned", :mandatory => true, :routing_key => "random-key")

最后,关闭连接:

connection.close

之前的代码示例包括一条发布到已存在的队列的消息,而另一条消息是发布到一个具有随机键名的队列,一个不存在的队列。更多代码示例可以在 rubybunny.info/articles/exchanges.html 找到。

就这些!功能已准备好上线。消息异步返回,无需正确处理。

摘要

本章包含了有关消息 TTL 的信息,探讨了在查看其他关于调整消息投递的重要主题时如何使用消息属性名称过期值。信息还描述了使用死信交换和队列的使用。然后,本章探讨了如何使用默认交换以及如何发送强制消息。

CC 正在成长为一个正规公司,其平台也在不断推出新功能以满足司机、客户和后台工作人员的需求。

到目前为止,只讨论了与 RabbitMQ 的异步交互,这是有意义的,因为它是消息传递的核心前提。话虽如此,也可以执行同步操作,下一章将演示这一点。下一章将包括有关出租车与客户之间直接交互的信息。下一个功能发布将包括哪些内容?唯一找到答案的方法是继续阅读!

第五章:消息路由

到目前为止,本书中所有的消息交互都是单向的,从消息发布者流向消费者。如果消费者想要通知发布者处理已完成并发送响应,或者出租车司机想要确认出租车预订请求,会怎样呢?

本章涵盖了出租车应用系统架构中的步骤 5 到 10,其中出租车司机响应客户并确认预订请求。出租车将其当前位置发布到一个队列中。客户的应用程序通过 WebSockets 连接到代理,并订阅位置更新,这些更新直接来自出租车。

将介绍远程过程调用(RPC)请求-响应概念,以及如何将响应路由回消费者。自高级消息队列协议(AMQP)0-9-1以来,代理提供了四种交换类型。本章还将展示如何实现最后一种,即头部交换。

让我们深入以下主题:

  • 向发布者发送响应

  • 回复队列和 RPC

  • 创建数据分析服务

技术要求

本章的代码文件可以在 GitHub 上找到,地址为github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition/tree/master/Chapter05

向发布者发送响应

诚然,到目前为止,我们与 RabbitMQ 的所有交互都是单向的异步交互。同样,与服务交互的客户端通常期望收到响应。在响应阶段反转发布者和消费者的角色需要客户端充当发布者,而服务充当消费者。

如第2章中所示,创建出租车应用,并如图所示,使用不同的队列来处理请求和响应:

图 5.1:使用消息队列执行请求-响应交互

在以下图中,图 5.2,我们可以看到以下内容:

  • 当出租车司机确认预订请求时,会向消息代理发送包含司机信息的信息(5)。

  • 应用程序服务接收消息(6),将信息存储在数据库中(7),并通过移动应用程序确认预订,最终显示给客户(8)。

  • 在这一点上,出租车需要持续与客户分享其当前位置。这是通过每分钟向位置队列发送汽车的纬度和经度来实现的(9)。应用程序的客户端使用通过 RabbitMQ 的 WebSocket 连接订阅当前位置队列(10)。

完整汽车CC)的架构如下所示,以供参考:

图 5.2:CC 的主要应用架构

让我们看看 WebSockets 是如何实现的。

RabbitMQ 中的 WebSockets

RabbitMQ 是一个多协议消息代理。本节探讨了 单文本导向消息协议STOMP)以及如何使用 RabbitMQ 来构建交互式 Web 应用程序。Web STOMP RabbitMQ 插件使得通过互联网使用 STOMP 成为可能,通过使用 WebSocket 在客户端(如网页浏览器)和通过 Web 服务器代理之间发送实时数据。该插件允许用户在服务器上存储或处理数据时获得高度交互的用户体验。

首先启用 Web STOMP 插件。

启用 Web STOMP 插件

与 RabbitMQ 管理插件一样,RabbitMQ 默认不嵌入 Web STOMP 插件,而是将其作为选项提供。必须启用并安装适当的 RabbitMQ 插件,并且必须创建一个具有适当权限的 虚拟主机vhost)。

运行以下 Debian 软件包脚本以安装 Web STOMP 插件:

rabbitmq-plugins enable rabbitmq_web_stomp

为了安全起见,在公开暴露的虚拟主机上至少创建一个具有有限权限的用户。运行以下代码以创建新的虚拟主机:

$ sudo rabbitmqctl add_vhost cc-dev-ws
Adding vhost "cc-dev-ws" ...

接下来,为 cc-dev 用户和 cc-dev-ws 虚拟主机添加用户权限:

$ sudo rabbitmqctl set_permissions -p cc-dev-ws cc-dev ".*" ".*" ".*"
Setting permissions for user "cc-dev" in vhost "cc-dev-ws" ..

新的虚拟主机已创建,并且对 cc-dev 用户可访问。在为出租车设置新队列以发布当前位置之前,应配置一些基本的安全选项。

使用 SSL 保护 Web STOMP

Web STOMP 使用互联网,在 CC 的应用中,除非得到适当的安全保护,否则信息容易受到窃听。由于大多数客户端发送代理 统一资源定位符URL)、用户名和密码信息,因此需要额外的安全层。

幸运的是,可以通过配置文件告诉 RabbitMQ 使用 安全套接字层SSL)。为了安全起见,CC 团队将在配置文件中添加以下行以设置证书:

ssl_options.cacertfile = /path/to/tls/ca_certificate.pem

ssl_options.certfile = /path/to/tls/server_certificate.pem

ssl_options.keyfile = /path/to/tls/server_key.pem

ssl_options.verify = verify_peer

ssl_options.fail_if_no_peer_cert = true 

stomp.default_user = guest

stomp.default_pass = guest

stomp.implicit_connect = true

为了使设置生效,必须重新启动代理并更改默认用户名和密码。脚本包含代理 URL,这可能会给服务器带来不希望的简单访问。

创建并发布 GPS 数据到队列

现在,CC 团队将创建一个队列,出租车将发送当前位置,这次使用 rabbitmqadmin 并运行以下命令来创建名为 taxi_information 的队列:

rabbitmqadmin declare queue name=taxi_information durable=true vhost=cc-dev-ws

添加一个名为 taxi_exchange 的交换机,如下所示:

rabbitmqadmin declare exchange name=taxi_exchange type=direct vhost=cc-dev-ws

由于命令行工具不允许将队列绑定到交换,请使用 RabbitMQ 管理界面使用 taxi_information 路由键将 taxi_information 队列绑定到 taxi_exchange 交换。

CC 团队将登录,转到队列部分,并将此信息添加到绑定部分,如图所示:

图片

图 5.3:通过 RabbitMQ 管理界面将绑定添加到队列

建立队列后,出租车应用程序可以与代理通信。此代码未提供,因为它几乎与第二章中的代码相同,创建出租车应用程序。相反,以下图表显示了如何通过管理控制台发布消息,这通常用于测试目的:

图片

图 5.4:将 GPS 坐标发送到 RabbitMQ

消费者现在可以订阅来自taxi_information队列的全球定位系统GPS)数据。

通过 WebSockets 订阅 GPS 和司机信息

客户端可以使用移动客户端通过 WebSockets 接收位置数据,如图 5.2 所示。

客户端移动应用程序使用 JavaScript 和 HTML,这得益于 React Native 或 Angular NativeScript 等工具,它们是两个持续获得关注的跨平台框架。

CC 团队使用内容分发网络将 StompJs 库(stomp.umd.min.js)导入到应用程序中,如下所示:

<script src=”https://cdn.jsdelivr.net/npm/@stomp/stompjs@5.0.0/bundles/stomp.umd.min.js”></script>

然后,CC 包含一些代码,以便从队列中接收更新。

首先,声明并配置stompClient变量。代理 URL 应以ws://wss://开头。示例中的reconnectDelay变量设置为200毫秒,这意味着在断开连接后 200 毫秒将进行重试,如下所示:

let stompClient;

const stompConfig = {
  connectHeaders: {
   login: username,
    passcode: password,
    host: 'cc-dev-ws' 
  },
  brokerURL: brokerURL,
  debug: function (str) {
    console.log('STOMP: ' + str);
  },
  reconnectDelay: 200,
  onConnect: function (frame) {
    const subscription =
stompClient.subscribe('/queue/taxi_information',       
    function (message) {
      const body = JSON.parse(message.body);
      const latitude = body.latitude;
      const longitude = body.longitude;
    });
  }
};

然后,创建实例并将其连接,如下所示:

stompClient = new StompJs.Client(stompConfig);
stompClient.activate();

CC 团队将创建一个回调来处理传入的消息,并直接订阅到taxi_information队列。用户名、密码和代理 URL 必须更改。

代理 URL 必须包含 Web STOMP 端口,默认为15674

快乐时光!客户现在将知道出租车的大致位置,无论是乘车前还是乘车过程中。

现在,让我们看看另一种从消费者那里接收回复的方法。

回复队列和 RPC

CC 应用程序现在可以在发布者和消费者之间良好地通信,但如果一个函数需要在远程计算机上运行并等待结果呢?在服务中硬编码交换和路由键以发布响应是不可能的,因为这会过于不灵活。解决方案是让请求消息携带响应应发送的位置坐标,这是一种常见的称为 RPC 的模式。

应用程序服务调用出租车应用程序中驻留的特定函数,出租车将结果发送给最终用户。请求消息携带应发送响应的队列名称。AMQP 协议支持这种机制。客户端可以存储必须发送响应的位置的队列名称。

当 RabbitMQ 将消息传递给消费者时,它将更改reply-to属性。服务器可以通过向默认交换发送带有reply-to属性路由键的消息来从发布者回复消息。

对于reply-to机制,可以使用任何类型的队列,但在实践中,以下两种方法被使用:

  • 为每个请求-响应交互创建一个短暂队列。这种方法使用客户端创建的专用、自动删除、非持久的、服务器端命名的队列,具有以下优点:

  • 由于它是专用的,没有其他消费者可以从中获取消息。

  • 它可以自动删除;一旦回复被消费,就不再需要它。

  • 不需要它是持久的;请求-响应交互并不打算长期存在。

  • 服务器生成一个唯一的名称,从而减轻了客户端需要想出唯一命名方案的负担。

  • 使用针对客户端的特定永久reply-to队列。这种方法使用非专用、非自动删除、非持久的客户端端命名的队列,具有以下优点:

  • 同样不需要它是持久的,原因如前所述。

  • 不需要它是专用的——每个请求-响应交互将使用不同的消费者。

使用永久队列的困难在于关联响应与请求。这是通过CorrelationId消息属性完成的,该属性从请求消息携带到响应消息。该属性允许客户端识别要处理的正确请求。

永久队列比使用每个请求的短暂队列更有效,因为创建和删除是昂贵的操作。

RabbitMQ 客户端库提供了简化与请求关联的响应的原始操作。

这就完成了通过reply-to将路由回选项信息到响应队列的信息。为了继续,CC 团队将通过连接数据分析服务来发现 RabbitMQ 提供的第四种交换类型。

创建数据分析服务

CC 希望能够分析传入的数据。该系统分析不同地区的出租车请求,发现重要模式,并找出高峰请求时间。项目经理指派团队构建一个能够并行运行同一服务的多个版本的系统,以便在更新期间优雅地演进服务。

团队成员表示,可以使用主题交换并将路由键结构化为{service_name}{version}。他们的想法在当前系统中是可行的;然而,RabbitMQ 通过头部交换提供了对这个问题的更优雅的解决方案。

头部交换机允许根据消息的头部路由消息,这些头部是存储在消息属性中的自定义键值对。自定义键值对引导消息到正确的队列或队列。采用这种方法,消息及其路由信息都是自包含的,保持一致,因此更容易作为一个整体进行检查。

这个添加在 CC 的架构中工作得非常完美,只需团队将队列绑定到头部交换机并带有适当头部的消息发送即可。首先打开命令行 shell 并执行以下命令:

  1. 创建一个名为 taxi_headers_exchange 的新头部交换机,如下所示:
rabbitmqadmin declare exchange name=taxi_header_exchange type=headers --vhost cc-dev

CC 团队将设置一个队列以接收来自出租车的信息。

  1. 创建一个名为 taxi_information_with_headers 的新队列,如下所示:
rabbitmqadmin declare queue --name=taxi_information_with_headers durable=true --vhost cc-dev

在管理控制台中,将新队列绑定到 taxi_header_exchange 头部交换机,如图下所示:

图片

图 5.5:在管理控制台中将队列绑定到交换机

通过设置 x-match 为 all,这意味着只有当 system = taxi 且 version = 0.1b 时,RabbitMQ 才会将发送到 taxi_header_exchange 的消息路由到 taxi_information_with_headers 队列。否则,系统将丢弃该消息。要匹配的头部值可能是 String、Number、Boolean 或 List 类型。由于键值对充当键,因此不需要路由键。

x-match 参数指定是否所有头部都必须匹配或只需一个。该属性可以有两个不同的值——any 或 all,如下所述:

  • all 是默认值,这意味着所有头部对(键,值)都必须匹配。

  • any 表示至少有一个头部对必须匹配。

由于数据分析服务是用 Python 编写的,我们将暂时放弃 Ruby。幸运的是,在 Python 中连接和发布消息与在 Ruby 中非常相似,因此没有很大的学习曲线需要克服。

注意,RabbitMQ 推荐的 Python 库是 pika (pypi.org/project/pika/)。信息可以按照以下方式发送到新队列:

  1. 开始导入客户端库 pikajson
import pika
import json
  1. 设置连接到 RabbitMQ 的凭据:
credentials = pika.PlainCredentials("cc-dev", "taxi123")
parameters = pika.ConnectionParameters(
  host="127.0.0.1",
  port=5672,
  virtual_host="cc-dev-ws", 
  credentials=credentials)
  1. 断言连接已建立,并尝试在连接上打开一个通道。将头部版本值设置为 0.1b,系统值设置为 taxi。发布一个带有给定 GPS 位置的消息到 taxi_header_exchange
conn = pika.BlockingConnection(parameters)
assert conn.is_open
try:
  ch = conn.channel()
  assert ch.is_open
  headers = {"version": "0.1b", "system": "taxi"}
  properties = pika.BasicProperties(content_type='application/json',
headers=headers)
  message = {"latitude": 0.0, "longitude": -1.0}
  message = json.dumps(message)
  ch.basic_publish(
    exchange="taxi_header_exchange",  
    body=message,
    properties=properties, routing_key="")
finally:
  conn.close()

由于 x-match=all,两个头部值都必须嵌入到消息属性中。交换机确保在将消息路由到 taxi_information_with_headers 队列之前,system 和 version 与管理控制台中指定的值匹配。

摘要

随着 CC 的用户和客户对系统越来越熟悉,他们开始要求更多的功能。现在 CC 应用程序能够通过 WebSockets 连接到代理,并订阅直接从出租车发送的位置更新。位置信息正在流动,CC 的应用程序运行良好,并提供了更复杂的功能。

本章进一步演示了如何通过回复队列在 RabbitMQ 中使用 RPC。引入了头部交换,以构建能够并行运行同一服务多个版本的系统,允许在更新期间优雅地演进。CC 系统在这一章中还有一项令人兴奋的添加,那就是将数据分析集成到系统中,以发现重要的用户模式和其它洞察。因此,头部交换得到了解释。

下一章涵盖了 CC 必须了解的前瞻性生产现实。重要的话题,如联盟功能和集群,以及健康检查和警报,即将出现。

第六章:将 RabbitMQ 投入生产

到目前为止,完整汽车CC)在生产中运行单个 RabbitMQ 实例。现在 CC 还需要确保服务具有高可用性。创建节点集群确保即使系统出现故障,信息仍然可访问。本章将介绍如何设置 RabbitMQ 集群,包括代理集群、经典镜像队列和法定多数队列的介绍。CC 还在寻找一个新的优雅的日志聚合解决方案,其中所有日志都通过联邦插件发布到集中的 RabbitMQ 节点,因此本章也将涵盖这一主题。

为了实现 CC 几乎不间断运行的目标,本章将包括以下内容:

  • 向集群添加节点

  • 发现 RabbitMQ 队列的类型

  • 使用联邦代理和日志聚合

技术要求

本章的代码文件可以在 GitHub 上找到,地址为github.com/PacktPublishing/RabbitMQ-Essentials-Second-Edition/tree/master/Chapter06

向集群添加节点

对于 CC 来说,一切运行顺利,但开发者们希望确保系统能够承受崩溃。即使在使用 RabbitMQ 的情况下,崩溃总是可能的。停电可能发生,突然的数据包丢失可能会损坏更新,管理员可能由于意外错误而错误地配置系统。由于故障或错误,整个实例可能会丢失。必须采取措施解决可能导致数据丢失、负面客户体验,甚至可怕的凌晨 2 点给团队打电话的问题。

好消息是,RabbitMQ 提供了处理潜在崩溃和其他灾难所需的特性,无需额外配置。RabbitMQ 可以配置为在活动-活动部署环境中运行,这意味着两个或多个节点可以同时运行相同类型的服务。几个代理可以参与集群,作为单个高可用的高级消息队列协议AMQP)服务。

在使用活动-活动部署时,无需手动故障转移。如果代理发生故障,无需任何操作,从而避免了凌晨 2 点的电话。根据高可用性集群中活动节点的数量,集群可以承受多次故障。

为了避免因无法访问代理而引起的复杂情况,CC 决定首先启动第二个 RabbitMQ 实例(命名为 rmq-prod-2),并将其与生产中已使用的实例进行集群化。

RabbitMQ 集群是由一个或多个节点组成的逻辑分组,每个节点共享用户、虚拟主机、队列、交换机等。系统架构仅在集群内部发生变化,如下所示:

图 6.1:由多个 RabbitMQ 代理组成的高可用性集群

更多的节点被添加到 RabbitMQ 集群中。当第二个 RabbitMQ 实例准备与现有的一个进行集群时,CC 会通知团队。为了实现这一点,将使用 RabbitMQ 的 Erlang 集群功能,以允许多个 Erlang 节点之间的本地或远程通信。Erlang 集群使用安全 cookie 作为跨节点认证的机制。为了避免错误,开发人员已确保 /var/lib/rabbitmq/.erlang.cookie 文件在每个实例中的内容相同。

注意,如果防火墙阻止 RabbitMQ 实例相互通信,则集群将无法工作。如果发生这种情况,请打开 AMQP(默认为 5672)所使用的特定端口,以便集群可以工作。更多信息请参阅www.rabbitmq.com/clustering.html#firewall

在第二节点上无需配置任何用户或虚拟主机,如第一章“Rabbit 春天苏醒”中所述。只需加入集群,配置将自动与现有的 RabbitMQ 实例同步,包括用户、虚拟主机、交换机、队列和策略。

请记住,节点在加入集群时会完全重置。RabbitMQ 在与其他节点同步之前会删除所有配置和数据。

要将节点加入集群,首先停止 RabbitMQ,然后加入集群,最后重新启动 RabbitMQ 应用程序:

$ sudo rabbitmqctl stop_app
# => Stopping node rabbit@rmq-prod-2 ...
# => ...done.
$ sudo rabbitmqctl join_cluster rabbit@rmq-prod-1
# => Clustering node rabbit@rmq-prod-2 with rabbit@rmq-prod-1 ...
# => ...done.
$ sudo rabbitmqctl start_app
# => Starting node rabbit@rmq-prod-2 ...
# => ...done.

确保所有 RabbitMQ 节点使用相同的 Erlang 主版本,否则 join_cluster 命令可能会失败。虽然可以运行包含不同 Erlang 版本的集群,但可能会出现不兼容性,从而影响集群的稳定性。

RabbitMQ 还要求节点之间使用相同的 3.7.x 及以下主/次版本。大多数情况下,可以运行不同的补丁版本(例如,3.7.X 和 3.7.Y),除非发布说明中另有说明。

功能标志是 RabbitMQ 3.8 版本引入的新机制。这些标志定义了 RabbitMQ 节点成为集群一部分的能力。功能标志控制哪些功能在所有集群节点上被视为启用或可用,因此使用子系统的节点必须具有相同的依赖项。更多信息请参阅www.rabbitmq.com/feature-flags.html

在运行前面的命令后,通过在任何节点上运行 cluster_status 命令来检查集群是否活跃:

$ sudo rabbitmqctl cluster_status 
# => Cluster status of node rabbit@rmq-prod-1 # -> 
# => [{nodes,[{disc,[rabbit@rmq-prod-2,rabbit@rmq-prod-1]}]}, {running_nodes,[rabbit@rmq-prod-2,rabbit@rmq-prod-1]}, {partitions,[]}]
# => ...done.

注意状态消息中给出了两个节点列表。在这种情况下,节点是集群中配置的节点列表。名为 running_nodes 的列表包含那些实际活跃的节点。配置的节点是持久的,这意味着它们将能够在代理重启后存活,因为每个代理都会自动重新加入集群。

通过连接到另一个节点(rmq-prod-2)上的管理控制台来确认新节点将同步到集群。使用 cc-admin 用户登录并转到队列视图。

配置应同步,如下截图所示:

图片

图 6.2:加入集群后,所有配置都同步

要添加更多节点,让每个新节点加入集群中的另一个节点。第一个节点管理控制台中的概览选项卡显示了集群中的所有节点,这些节点是自动发现的,如下截图所示:

图片

图 6.3:管理控制台概览显示了所有集群成员

如所示,集群的所有成员都列出了,包括基本统计信息和端口。信息列中显示的不同值如下:

  • basic:描述rates_mode,它告诉队列如何报告统计信息。这可以是basic(默认)、detailednone之一。

  • disc: 表示节点将数据持久化到文件系统,这是默认行为。也可以以RAM模式启动节点,其中所有消息数据都存储在内存中,如果系统有足够的内存,这可以加快系统速度。

  • 7:显示已启用的插件数量。

  • allocated: 描述内存计算策略。

可以通过rabbitmqctl(管理 RabbitMQ 服务器节点的命令行工具)从集群中移除节点(www.rabbitmq.com/clustering.html#breakup)。

目前所有 CC 应用程序都连接到单个 RabbitMQ 节点。这需要修改。应用程序应首先尝试连接到一个节点,如果原始尝试失败,则切换到另一个节点。继续阅读以了解如何实现。

连接到集群

目前所有 CC 应用程序都连接到单个 RabbitMQ 节点,需要修改以利用集群的优势。所有连接到 RabbitMQ 的应用程序都需要修改。应用程序应首先尝试连接到一个节点,如果原始尝试失败,则切换到另一个节点。这是唯一需要更改的地方;应用程序将与代理交互,就像以前一样。

首先,修改主应用程序连接 Ruby 代码如下:

begin
  connection = Bunny.new(
    hosts: ['rmq-prod-01', 'rmq-prod-02'])
  connection.start
  rescue Bunny::TCPConnectionFailed => e
    puts "Connection to server failed"
end

基本上,传递了代理地址列表。有了这个,RabbitMQ Ruby 客户端将连接到地址列表中的第一个响应节点,并尝试提供的每个代理地址,直到建立连接或最终失败。在失败的情况下,已经存在的整体重连机制将启动,并将再次尝试连接地址。

可以使用rabbitmqctl sync_queue <queue_name>命令手动同步镜像队列。使用rabbitmqctl cancel_sync_queue <queue_name>取消同步。

到目前为止,还有一步要执行以确保队列数据的高可用性:启用一种将数据传播到其他节点的方式。可用的选项是经典镜像队列仲裁队列。但首先,需要了解一些分区处理策略。

分区处理策略

当然,向集群中添加更多节点是可能的。然而,这会带来一个新的挑战,即网络连接问题。当使用多个节点时,脑裂和早期消息确认是常见问题。在分布式系统中,当网络的一部分无法从另一部分访问时,就会发生脑裂,这会创建网络分区(称为netsplit)。为了避免这种情况,设置分区处理策略。在 RabbitMQ 中,这通过配置文件中的cluster_partition_handling参数来设置——www.rabbitmq.com/partitions.html#automatic-handling

pause-minority策略终止少数分区中的节点。这是许多分布式网络解决脑裂问题的默认方式。pause-if-all-down功能仅在没有任何节点可访问时暂停节点。这不可取,因为它会在每个分区的数据之间造成很大的差异。

一旦在pause-if-all-down设置中可用节点,还有两个选项可以指定如何重新连接网络。简单地忽略另一个分区或自动修复集群。系统无法暂停的节点也必须指定。在pause-minority策略中,分区在可用时重新连接。

RabbitMQ 确保跨集群的同步。客户端可以通过任何节点访问其交换机和队列;然而,消息本身并不会在节点间传递。下一节将介绍如何实现这一点。

发现 RabbitMQ 队列的类型

RabbitMQ 中的队列可以是持久的或临时的。对于临时消息处理,建议使用经典镜像队列,而仲裁队列是持久队列的良好替代方案。

持久队列元数据存储在磁盘上,而临时队列在可能的情况下存储在内存中。另一种队列类型,懒队列,尽可能早地将持久和临时消息的内容写入磁盘。

由于经典镜像队列的技术限制,很难对如何处理故障做出保证。RabbitMQ 文档(www.rabbitmq.com/ha.html)建议用户熟悉仲裁队列,并在可能的情况下考虑它们而不是经典镜像队列。

队列镜像

在 CC 的情况下,队列中的数据需要高度可用。镜像队列提供了这种安全性。队列镜像使用主镜像设计模式。所有消息入队和出队操作都在主节点上进行,镜像节点定期从主节点接收更新。如果主节点不可用,RabbitMQ 会将一个镜像节点提升为主节点;通常,最老的镜像节点成为新的主节点,只要它保持同步。

还可以通过将数据发送到原始集群之外的不同集群来设置主主系统。这为硬件更新和极端故障情况提供了有用的备份。它还可以帮助加快不同地理区域之间的交互。

在我们的情况下,必须通过Q_TTL_DLX策略来告诉集群如何镜像队列,因为在一个队列或交换中一次只能允许一个策略。第一步是清除在第四章,调整消息投递中创建的策略,然后应用一个新的策略,该策略结合了Q_TTL_DLX策略和为队列镜像创建的策略。

运行以下命令以更改Q_TTL_DLX策略并告诉 RabbitMQ 如何镜像队列。首先清除策略:

$ sudo rabbitmqctl clear_policy -p cc-prod-vhost Q_TTL_DLX
# => Clearing policy "Q_TTL_DLX"
# => ......done.
"Specify the new HA_Q_TTL_DLX policy:"
$ sudo rabbitmqctl set_policy -p cc-prod-vhost HA_Q_TTL_DLX "taxi\.\d+" '{"message-ttl":604800000, "dead-letter-exchange":"taxi-dlx", "ha-mode":"all", "ha-sync-mode":"automatic"}' --apply-to queues 
# => Setting policy "HA_Q_TTL_DLX" for pattern "taxi\.\d+" to "{\"ha-mode\":\"all\", \"message-ttl\":604800000, \"dead-letter-exchange\":\"taxi-dlx\"}" with priority "0" 
# => ......done.

或者,可以从管理控制台添加策略,如下面的截图所示:

图 6.4:通过 RabbitMQ 管理控制台添加的策略

已将高可用性模式添加到现有的 TTL 和 DLX 策略规则中。ha-mode 的 all 值告诉 RabbitMQ 在集群的所有节点上镜像队列,这正是 CC 在他们的双节点集群中想要的。其他选项是 exact 和 nodes,允许开发者在使用 exact 选项时指定节点数,在使用 nodes 选项时通过 ha-params 参数指定节点名称列表。

ha-sync-mode 参数用于指定镜像队列的同步模式。此参数可以设置为手动或自动。在手动模式下,新镜像队列不会接收任何现有消息,但最终会随着消费者检索消息而与主队列保持一致。这减少了开销,但以丢失信息为代价。自动模式将消息发送到每个队列,意味着对系统性能有轻微的影响。

CC 决定使用即时队列同步,以便任何现有消息几乎瞬间在所有节点上可见。CC 对由此产生的初始无响应性表示可以接受,因为性能对于用户消息不是关键。

在运行上述命令后,导航到管理控制台中的队列标签页。观察到的 HA_Q_TTL_DLX 策略已应用于目标队列:

图 6.5:应用了高可用性策略的镜像队列

注意镜像队列旁边有一个+1。这表示队列已镜像到集群中的另一个节点。在管理控制台的每个队列的详细信息部分也清楚地定义了主节点(rabbit@rmq-prod-1)和镜像节点(rabbit@rmq-prod-2),如下面的截图所示:

图片

图 6.6:主节点和镜像节点细节

在这一点上,RabbitMQ 代理已集群化,出租车订单请求队列已镜像。客户端应用程序可以从中受益于这种高可用性部署并连接到不同的节点。

设置主队列位置:每个队列都有一个主副本,称为队列主。在同步之前,该队列是第一个接收消息的。可以通过在管理控制台的队列选项卡中使用 x-queue-master-locator 参数或在程序创建队列时来影响其设置。

仲裁队列是一种新型队列,通常比经典镜像队列更受推荐。

仲裁队列

作为持久镜像队列的替代方案,仲裁队列通过就队列内容达成一致来确保集群是最新的。在此过程中,仲裁队列避免了数据丢失,这在镜像队列中可能会在消息过早确认时发生。仲裁队列自 RabbitMQ 3.8.0 版本起可用。如 RabbitMQ 文档(www.rabbitmq.com/quorum-queues.html)中详细说明,当使用仲裁队列时,某些临时功能不可用。

队列仲裁队列有一个领导者,其作用大致与经典镜像队列主相同。所有通信都路由到队列领导者,这意味着队列领导者的本地性会影响消息的延迟和带宽需求;然而,这种影响应该低于经典镜像队列。

在仲裁队列中,领导者和复制是由共识驱动的,这意味着它们就队列的状态及其内容达成一致。虽然镜像队列可能会过早确认消息并丢失数据,但仲裁队列只有在大多数节点可用时才会确认,从而避免了数据丢失。

使用以下命令声明仲裁队列:

rabbitmqadmin declare queue name=<name> durable=true arguments='{“x-queue-type”: “quorum”}'

这些队列必须是持久的,并且通过将x-queue-type报头设置为quorum来实例化。如果大多数节点就队列内容达成一致,则数据有效。否则,系统会尝试将所有队列更新到最新状态。

仲裁队列支持处理毒消息,这些消息永远不会被完全消费或确认。

可以跟踪并显示在x-delivery-count报头中的未成功投递尝试次数。当消息被返回的次数超过配置值时,可以将其作为毒消息进行死信处理。

懒队列是另一种值得探索的队列类型,所以请继续阅读。

懒队列

由于消费者维护或大量消息批次的到达等原因,队列可能会变得很长。虽然 RabbitMQ 可以支持数百万条消息,但大多数专家建议尽可能保持队列尽可能短。默认情况下,消息存储在内存中。当队列变得太长以至于底层实例无法处理时,RabbitMQ 会将消息(页面输出)刷新以释放 RAM 使用。在 RAM 中存储消息比将它们存储到磁盘上能更快地将消息传递给消费者。

页面输出功能通常需要时间,并且经常停止队列处理消息,这会降低队列速度。因此,包含大量消息的队列可能会对经纪人的性能产生负面影响。此外,在集群重启后重建索引需要花费大量时间,以及在不同节点之间同步消息也需要花费大量时间。

从 RabbitMQ 版本 3.6 开始,添加了一个名为 lazy queues 的策略,以自动将消息存储到磁盘,从而最小化 RAM 使用。可以通过设置queue.declare参数或应用策略到所有队列来启用 lazy queues。

持久消息可以在进入经纪人时写入磁盘,并同时保存在 RAM 中。

已经展示了不同的队列类型,现在是时候看看 CC 应该如何处理来自所有集群的日志聚合了。

使用联邦经纪人和日志聚合

创建两个 RabbitMQ 经纪人集群的方式实际上与在创建一个高度可用的关系数据库时通常所做的是非常相似的。数据库仍然是一个集中的资源,提供高可用性保证。然而,当涉及到高可用性时,RabbitMQ 并非只有一项绝技。

要形成一个 RabbitMQ 系统的图像,以下两个插件允许经纪人连接:

  • Shovel: 在不同经纪人之间连接队列和交易所

  • 联邦: 为队列到队列或交易所到交易所形成跨经纪人连接

这两个插件通过按指令路由消息或提供一个安全的地方让它们在可以处理之前保持不变,确保了消息在经纪人之间可靠地传递。它们都不需要经纪人集群,这简化了设置和管理。此外,这两个插件在 WAN 连接上也能正常工作,这在集群场景中是不常见的。

手动配置联邦中的目标节点。上游节点会自动配置。另一方面,铲子必须手动配置每个源节点以发送到目标节点,而目标节点本身不需要任何配置。

CC 团队正在寻求一种处理日志的好方法,他们很快意识到联邦插件非常适合这个过程。

处理日志处理

CC 的系统正在不断增长,出租车司机和开发者的团队也在不断扩大。负责分析的那个团队一直在寻找一个优雅的解决方案来聚合来自不同应用的日志,以便推出新的统计数据,供内部和最终用户使用。幸运的是,由于 RabbitMQ 的高性能,它可以用于应用日志处理。

在这个拓扑中,所有应用都将写入本地 RabbitMQ 节点,该节点将充当存储和转发代理,将所有日志推送到中心 RabbitMQ 节点,如下面的图所示:

图片

图 6.7:将日志消息联邦到一个中心代理的拓扑

如果这个中心节点宕机,日志条目将保留在本地累积,直到它恢复。消息通过一个位置(称为上游)的交换流动,以复制到其他位置的交换(下游),如下面的图所示:

图片

图 6.8:交换联邦消息流

显然,这里的假设是本地 RabbitMQ 节点非常稳定。过去几个月运行 RabbitMQ 的经验将有助于这种方法。此外,日志被认为对 CC 来说很重要,但不是关键数据,因此尽力而为的方法是可以接受的。了解这一点后,团队选择使用联邦插件,因为它支持联邦到队列连接(如果使用铲子插件,消息将需要在每个节点的本地队列中累积)。

记住,上一节中镜像的所有队列都是匹配taxi-inbox\.\d+正则表达式的队列。现在提到的所有日志队列都被排除在外。这就是 CC 团队想要的,因为他们不希望镜像这样高流量的队列。为了使 CC 能够享受相同的日志聚合保证,可以做什么呢?引入消息拓扑的概念。

更多关于铲子插件的信息可以在www.rabbitmq.com/shovel.html找到。

需要在所有将参与拓扑的 RabbitMQ 节点上安装联邦插件,通过在每个节点上运行以下命令:

 $ sudo rabbitmq-plugins enable rabbitmq_federation 
Applying plugin configuration to rabbit@app-prod-1...

$ sudo rabbitmq-plugins enable rabbitmq_federation_management
Applying plugin configuration to rabbit@app-prod-1...

此外,与集群不同,每个节点都需要手动设置以配置所需的用户和虚拟主机。如第一章“Rabbit Springs to Life”中讨论的那样,现在是运行必要命令的时候了。接下来,必须配置apps-log交换联邦本身。这涉及到多个步骤(稍后详细介绍),所有这些步骤都在中心代理上运行,即所有日志将汇聚的下游代理。

首先,配置上游,这些是向中心代理发送数据的 RabbitMQ 节点。需要三个上游,因为有三个服务器将发送日志,app-prod-1app-prod-2app-prod-3;然而,为了简洁起见,以下示例中只显示两个节点。

可以通过rabbitmqctl添加上游:

# Adds a federation upstream named "app-prod-logs"
rabbitmqctl -p logs-prod set_parameter federation-upstream app-prod-logs '{"uri":"amqp://cc-prod:******@app-prod-1:5672/cc-prod-vhost"}'

或者,可以通过管理控制台添加策略:

图片

图 6.9:将名为 app-prod-logs 的联盟上游添加到下游代理

一旦在下游指定了上游,就可以将控制联盟的策略添加到下游服务器。app-prod-logs 联盟的添加就像任何其他策略一样(www.rabbitmq.com/parameters.html#policies),通过使用终端:

rabbitmqctl set_policy -p logs-prod --apply-to exchanges log-exchange-federation "^app-logs*" '{"federation-upstream-set":"all"}' --apply-to exchanges

策略也可以通过管理控制台添加:

图片

图 6.10:将联盟策略添加到下游服务器

CC 团队通过应用匹配交换名称的策略来完成此操作。模式参数是一个用于匹配队列(或交换)名称的正则表达式。在 CC 的情况下,联盟策略应用于所有以app-prod开头的交换。

策略可以应用于上游集、单个交换或队列上游。在这个例子中,federation-upstream-set应用于所有上游。

如果确定永远不会存在超过一个逻辑上游组,则可以跳过创建上游集,转而使用名为all的隐式集,该集自动包含虚拟主机中的所有上游。

在这种情况下,确保联盟插件将在中心代理中用于与联盟交换进行交互的用户也已配置。

浏览到管理控制台管理部分中的联盟上游选项卡,它将显示上游已正确配置,如下面的屏幕截图所示:

图片

图 6.11:在联盟中配置上游节点

切换到联盟状态会显示一个空屏幕,因为它尚未激活。为什么会这样?毕竟,拓扑刚刚创建。原因是还没有任何交换或队列在拓扑中活跃。由于其动态特性,联盟处于非活动状态。在返回到联盟状态选项卡之前,下一步是在上游和下游服务器上创建 app-logs 交换并将其绑定到队列。这里需要注意的是,联盟现在正在从配置集的两个上游节点为 app-logs 交换运行链接。请参阅以下屏幕截图:

图片

图 6.12:运行联盟交换的上游链接

可以通过在下游节点上运行 sudo rabbitmqctl eval rabbit_federation_status:status() 命令从命令行获取联盟的状态。

管理控制台中的连接和通道选项卡现在显示下游节点通过 AMQP 协议连接到上游节点,如下面的屏幕截图所示:

图片

图 6.13:连接选项卡中的联盟链接

除了拓扑结构的设置本身,联盟没有神秘之处。它是建立在 AMQP 之上的,因此能够从该协议提供的相同优势中受益。因此,如果 RabbitMQ 实例被防火墙隔离,除了默认情况下 AMQP 使用的端口(5672)之外,不需要打开任何特殊端口。

更多关于联盟插件的详细信息,请参阅www.rabbitmq.com/federation.htmlwww.rabbitmq.com/federation-reference.html

摘要

CC 示例提供了有关如何创建基本消息队列架构、添加满足用户需求的有价值功能以及使系统无故障运行的信息。本章介绍了 RabbitMQ 如何通过集群和联盟提供强大的功能,以及这些功能如何提高消息基础设施的可用性和整体弹性。还探讨了法定人数、经典镜像和懒队列。

在此过程中,提供了有关构建可靠、弹性系统的最佳实践的信息和指导。下一章强调了这些建议,并从 CC 通过 RabbitMQ 的旅程中总结了关键要点。它还探讨了 RabbitMQ 的监控。

第七章:最佳实践和代理监控

本书的前几章专注于使用 RabbitMQ 在示例公司Complete CarCC)中设置成功的微服务架构。虽然包括了众多 RabbitMQ 功能,但任何系统如果没有对其实施的最佳实践的理解都是不完整的。与所有生产系统一样,适当的监控和警报也是必要的,以便保持对事物的掌控。

CC 的集群稳定,没有性能问题。本章总结了从 CC 的系统中学到的关键要点,包括队列、路由、交换、消息处理等方面的最佳实践和建议。

本章探讨了以下主题:

  • 如何避免丢失消息

  • 保持队列和代理清洁

  • 路由最佳实践

  • 通过连接和通道进行网络通信

  • 探索关键要点

  • 监控 - 查询 REST API

当使用 RabbitMQ 设置基础设施时,本章是一个理想的参考指南。在将 RabbitMQ 投入生产时,请回顾本章中的关键要点、最佳实践和监控技巧,以获得宝贵的见解。

如何避免丢失消息

通过遵循本节中的最佳实践可以避免消息丢失。大部分情况下,CC 遵循了最佳实践,即保持队列短小高效。包含太多消息的队列会对代理的性能产生负面影响。已识别的高 RAM 使用可能表明队列中的消息数量迅速增加。

以下是关于如何在 RabbitMQ 中不丢失消息的最佳实践建议:

  • 在 RabbitMQ 集群中使用至少三个节点,并使用法定多数队列类型将消息传播到不同的节点。

  • 如果绝对必要确保所有消息都被处理,则将队列声明为持久的,并将消息投递模式设置为持久,如第二章中所述,创建出租车应用程序。队列、交换和消息需要能够处理可能发生的任何重启、崩溃或硬件故障。

以下是关于 RabbitMQ 中消息处理的澄清:

  • 理解持久性带来的权衡是设计耐用系统架构时的关键。懒队列虽然使用临时消息,但对性能的影响类似。

  • 使用临时消息与持久队列结合可以创造速度,而不会丢失配置,但可能会导致消息丢失。

如果遵循了所有这些最佳实践,消息仍然有丢失的风险,那么下一节将介绍死信交换,这样那些可能永远消失的消息就有地方等待,直到它们可以被处理。

使用死信交换

即使使用持久队列和持久消息,也可能出现导致未处理消息的问题。可以设置 TTL,队列长度可能已超过,或者消息可能被消费者否定确认。作为最佳实践,消息的路由键应指定x-dead-letter-routing-key,以确保消息永远不会丢失。将队列附加到交换机并按程序管理消息。尽量避免将消息发送到同一交换机,因为这可能导致无限递归。有些消息可能难以处理,并不断进入交换机。确保在编程逻辑中处理这些错误。

在队列的声明中设置x-dead-letter-routing-key属性。这有助于提高性能,并使架构中的组件能够分别处理错误,如第四章中所述,调整消息传递。

对于经常受到消息峰值冲击的应用程序,建议设置队列最大长度。队列最大长度通过丢弃队列头部的消息来保持队列较短。最大长度可以设置为消息数量,或固定字节数。

处理确认和确认

在连接失败的情况下,传输中的消息可能会丢失。如果需要重新传输消息,确认会向服务器和客户端提供警报。客户端可以在收到消息时或处理完消息后确认消息。然而,重要的是要记住,消费重要消息的应用程序应在处理后再确认,这样未处理的来自崩溃或异常的消息就不会丢失。发布者确认需要服务器确认已从发布者接收消息。

确认也可能影响系统性能,但如果发布者必须至少处理一次消息,则它们是必需的。

消息处理最佳实践

队列和客户端处理它们的负载——消息。为了进一步提高性能,微调消息和消息处理。

限制消息大小

每秒发送的消息数量比消息本身的大小更令人担忧。然而,发送大消息不是最佳实践,发送过小的消息也不是,因为 AMQP 会给所有发送的消息添加一个小数据包开销。

检查消息,看是否可以将其拆分并发送到不同的队列,如下所示:

  • 将可迭代数据拆分为块,每条消息发送一个小块。

  • 将大文件存储在分布式存储中,如 Hadoop 或网络附加存储。

  • 将架构拆分为更多模块化组件,每个组件一个队列。

  • 将适当的元数据卸载到键值存储中。

尽管可以避免发送大消息,但带宽、架构和故障转移限制是需要考虑的因素。消息的大小取决于应用程序,但应尽可能小。

使用消费者和预取

设置预取值可以在系统内均匀地分配工作负载。RabbitMQ 允许预取,但重要的是要记住,只有在所有消费者都忙碌时,预取才是有效的。

RabbitMQ 必须管理跨队列和消费者的消费。预取值过低会使消费者空闲,等待消息到达,这反过来会减慢代理处理请求的能力。设置预取值过高会使一个消费者忙碌,而其余的消费者保持空闲,如第三章,向多个出租车司机发送消息中所述。

如果处理时间低且网络稳定,则可以增加预取值。在这种情况下,可以通过将总往返时间除以处理时间来确定预取值。

如果有多个消费者和较长的处理时间,预取值会趋于较低。如果处理时间足够长,可以将预取限制设置为 1。

随着需求增加,队列变得更加繁忙,系统资源消耗也更多。保持队列和代理的清洁对于良好的性能至关重要,这将在下一节中介绍。

保持队列和代理的清洁

清洁的代理是高效的代理。为了保持功率和空间在最佳水平,确保队列和代理清洁是很容易的。RabbitMQ 提供了自动删除消息和队列以保持空间空闲的机制。这包括设置存活时间TTL)和自动删除未使用的队列,这些将在以下章节中详细介绍。

为消息设置 TTL 或队列的最大长度

为长时间运行的过程提供消息支持的队列可能会变得非常大。过大的队列可能会影响代理的性能。设置 TTL 允许在一段时间后从队列中删除消息。如果指定,这些消息将进入死信交换。这可以保存更多消息,甚至处理潜在问题而不会丢失数据。

在声明队列时,使用 x-message-ttl 属性设置合理的 TTL。请确保提供 x-dead-letter-exchangex-dead-letter-routing-key 以避免完全丢失消息。

对于经常受到消息高峰冲击的应用程序,建议设置队列最大长度。队列最大长度通过丢弃队列头部的消息来保持队列短小。最大长度可以设置为消息数量,或字节数量。

自动删除未使用的队列

除了防止队列变得过大之外,还可以根据使用情况删除队列。

自动删除未使用队列有三种方法,如下所示:

  1. 使用声明中的x-expires属性为队列设置过期策略,当未使用时,只保持队列活跃数毫秒。

  2. 在声明中将auto-delete队列属性设置为true。这意味着在以下情况下队列将被删除:

  • 建立了初始连接。

  • 最后一个消费者关闭。

  • 通道/连接被关闭或队列失去了与服务器之间的传输控制协议TCP)连接。

  1. 在队列声明中将独占属性设置为true,这样结构就属于声明连接,并在连接关闭时被删除。

有时,消息队列中的旅程本身就会造成低效。为了确保消息走的是最佳路径,请遵循下一节中找到的路由最佳实践。

路由最佳实践

作为最佳实践,直接交换是使用最快的。即使在使用直接交换时,具有多个绑定也需要更多时间来计算消息必须发送的位置。在路由方面还有一些额外的最佳实践需要考虑。

考虑路由设计系统

每个端点都是一个服务或应用程序。与 CC 不同,CC 在汽车和大部分单一应用层之间运行,而许多微服务架构通过数十个服务传递消息。

CC 围绕小型服务设计了他们的系统架构。他们结合了有意义的操作。在设计了较小的系统之后,他们考虑了额外的交换或队列可能有益的地方。这保持了整体设计足够小,同时不会限制处理能力。

通过连接和通道进行网络通信

数千个连接对 RabbitMQ 服务器来说是一个沉重的负担,导致其内存耗尽并崩溃。大量连接和通道也可能由于处理大量性能指标而负面影响 RabbitMQ 管理界面。为了避免这种情况,请为每个应用程序配置创建极少数量的连接——如果可能的话,1 个。而不是使用多个连接,为每个线程建立一个通道。每个连接应该是长久的,并且应根据应用程序结构考虑以下最佳实践。

记住,即使新的硬件提供了数百个线程,也只能建立设置的通道数,并且这个数字不应过大。由于一些客户端没有使通道线程安全,最好不要在线程之间共享通道。这样做可能会创建竞争条件,这可能导致应用程序完全崩溃。

重复打开和关闭连接和通道也会损害系统性能。这样做会增加延迟,因为更多的 TCP 数据包通过网络发送。

使用 TLS 和 AMQPS 进行安全

RabbitMQ 可以通过 AMQPS 连接,即 TLS 包装的 AMQP 协议。通过网络传输的数据被加密,但需要考虑性能影响。为了最大化性能,请使用 VPC 或 VPN 对等连接,因为它们为流量提供了一个私有、隔离的环境,并且不直接涉及 AMQP 客户端和服务器。

不要在前端暴露后端。CC 示例被简化了。在现实中,很可能在未知用户和代理之间添加一个应用层。

分离的连接

默认情况下,RabbitMQ 会降低发布速度过快的连接速度,以便队列能够跟上。RabbitMQ 简单地对 TCP 连接应用反向压力,将其置于流量控制状态。流量控制的连接在管理 UI 和 HTTP API 响应中显示流量状态。这意味着连接每秒经历多次阻塞和解阻塞,以保持消息流率在一个服务器和队列都能处理的水准。

当发布者使用与消费者相同的 TCP 连接时,在回复代理时可能会阻塞消息。服务器可能无法从客户端收到确认,这将对其速度产生负面影响,并可能导致其过载。因此,通过为发布者和消费者使用单独的连接来实现更高的吞吐量是最佳选择。

在不同的核心上分割队列

CC 基础设施运行在多个核心上。为了获得更好的性能,队列被分散在不同的核心和节点之间。RabbitMQ 中的队列绑定到它们首次声明的节点。即使对于集群代理也是如此,因为所有路由到特定队列的消息都发送到队列所在的节点。RabbitMQ 中的一个队列每秒可以处理高达 50,000 条消息。因此,当队列在不同核心和节点之间分割,并在多个队列之间分散时,可以获得更好的性能。

可以手动在节点之间均匀分割队列,但这可能难以记住。或者,有两个插件可以帮助组织多个节点或单个节点集群的多核。这些是一致性哈希交换RabbitMQ 分片插件。

RabbitMQ 分片

分片使得在不同节点上的队列之间分配消息变得容易。一个队列可以分散到多个实际队列中。一旦交换被定义为分片,支持队列将自动在每个集群节点上启动,消息相应地分散,如下所示:

图片

图 7.1:队列间的分片

路由键确保消息在队列之间均匀分布。插件期望你为每个分片运行一个消费者,新节点会自动纳入。请注意,从所有队列中消费是很重要的。插件提供了一个集中位置来发送消息,并通过在集群中添加队列来跨节点负载均衡消息。更多关于 RabbitMQ 分片插件的详细信息请参阅:github.com/rabbitmq/rabbitmq-sharding

一致性哈希交换

RabbitMQ 提供了一个插件,可以帮助通过一致性哈希交换来负载均衡消息。基于路由键,此交换中绑定的队列会均匀地发送消息。这优化了具有多个核心的集群的使用,因为插件会创建路由键的哈希,并确保在交换绑定的队列之间均匀分配消息,确保在集群的多个核心上优化使用。

更多关于一致性哈希交换插件的详细信息请参阅:github.com/rabbitmq/rabbitmq-consistent-hash-exchange

探索关键要点

为了简化,优化可以分为两种形式,鉴于本书中汽车是一个热门话题,让我们继续这个主题。

法拉利 快速平稳:如果系统必须具有快速性能和高吞吐量,则使用单个节点。尽可能保持队列最短,如果可能的话,设置最大长度或 TTL。不要设置懒队列策略以保持检索时间短。出于相同的原因,使用临时消息而不是持久消息。利用多个队列和消费者的使用,提供最大吞吐量。为了实现最快的吞吐量,应禁用手动确认。始终努力使用最新的稳定 RabbitMQ 版本。

沃尔沃 稳定可靠:一个必须高度可用且不能丢失消息的系统应该有耐用的队列并发送持久消息。队列仍然应该保持较短。

建议通过法定队列来设置集群。如果已经使用镜像队列,添加懒队列策略以获得更稳定的设置。确保系统中使用三个或五个节点以实现高可用性。在设置 RabbitMQ 集群时,使用一致性哈希交换或分片插件在不同核心和不同节点之间分割队列,以保持一切运行顺畅和高效。始终努力使用最新的稳定 RabbitMQ 版本。

既然最佳实践提示已经介绍完毕,现在是时候考虑如果出现问题应该发生什么了。监控集群和设置警报策略是任何生产环境的重要收尾工作。

监控 – 查询 REST API

监控 RabbitMQ 代理时,检索实时信息有两种主要方式:一种是通过rabbitmqctl命令行工具,另一种是通过管理控制台通过 HTTP 公开的REST API

任何监控系统都可以使用这些工具来收集指标并将它们报告给日志、分析、报告和警报框架。例如,可以将信息推送到外部日志服务进行进一步分析。

自从 CC 安装了管理控制台,如第一章《Rabbit Springs to Life》中所述,团队选择使用丰富、文档齐全的 API 而不是命令行。RabbitMQ 在任何安装了管理插件的节点上提供http://localhost:15672/ API 的文档。虽然如此,也可以通过命令行检索相同的原始指标,但缺乏图形界面。

请记住,管理控制台由 API 支持,因此浏览器内看到的任何操作都可以通过 API 完成。

RabbitMQ 公开了各种不同的指标类型以供收集,如前文所述。这些包括但不限于以下内容:

  • 节点状态:测试 RabbitMQ 的性能涉及执行一系列命令来声明一个存活性测试队列,然后发布和消费它。如果命令返回0(没有消费消息),则通过适当的请求设置一个警报:
curl -s http://cc-admin:******@localhost:15672/api/aliveness-test/cc-prod-vhost | grep -c "ok"
  • 集群大小:测试集群大小对于发现网络分区很有用。如果集群大小低于预期,则设置一个警报:
curl -s http://cc-admin:******@localhost:15672/api/nodes | grep -o "contexts" | wc -l

CC 使用bash脚本和 Python 发送错误,当节点数量低于预期时。

  • 联邦状态:由于重启或其他问题,联邦队列可能会解耦。请检查中央日志聚合代理上的活动上游链接,如果其数量小于最佳大小(在 CC 的情况下为3),则发出警报,如下所示:
curl -s http://cc-admin:******@localhost:15672/api/federation-links/cc-prod-vhost | grep -o "running" | wc -l
  • 队列的高水位:基于云的代理有时以低成本提供扩展,但有限制消息。在其他情况下,消息延迟是一个问题。确保队列中可用的消息数量低于某个阈值:
curl -s -f http://cc-admin:******@localhost:15672/api/queues/cc-prod-vhost/taxi-dlq | jq '.messages_ready'

在 CC 的情况下,他们想验证taxi-dlq队列中的消息少于 25 条。否则,他们发出警报,表明存在瓶颈。如果队列不存在,脚本需要处理优雅的失败。

  • 整体消息吞吐量:监控特定代理上的消息流量强度可以使资源根据需要增加或减少。使用以下命令收集消息速率:
curl -s http://cc-admin:******@localhost:15672/api/vhosts/cc-prod-vhost | jq '.messages_details.rate'

当 CC 的吞吐量阈值超过其代理可以承受的上限时,CC 会添加一个警报。

一些指标具有严格的最高限制,其值也通过 API 提供。建议在达到上限 80%的阈值时发出警报。以下脚本在必须发出警报时返回 false。这些指标包括以下内容:

  • 文件描述符:许多操作系统都有文件描述符限制。如果可用的描述符不足,磁盘上消息持久化的性能可能会受到影响。使用的文件描述符数量可以与可用文件描述符的数量进行比较:
curl -s http://cc-admin:******@localhost:15672/api/nodes/rabbit@${host} | jq '.fd_used<.fd_total*.8'

在 macOS X 和 Linux 上,可以增加可用的文件描述符数量。文件描述符用于访问其他文件。如果超过这个限制,检查吞吐量也是一个好主意。

  • 套接字描述符:套接字描述符维护对单个套接字的连接句柄。如果这些描述符耗尽,RabbitMQ 将停止接受新的连接,这是大型集群中常见的问题:
curl -s http://cc-admin:******@localhost:15672/api/nodes/rabbit@${host} | jq '.sockets_used<.sockets_total*.8'

Linux 使用文件描述符来处理套接字,可以通过ulimit命令调整计数。遵循最佳实践,使用更多通道和更少的连接可以帮助处理这个问题。

  • Erlang 进程:Erlang 虚拟机创建的进程数量有一个上限。尽管通常接近 100 万进程,但每个进程都需要资源来运行。使用的 Erlang 进程数量可以与 Erlang 进程限制进行比较:
curl -s http://cc-admin:******@localhost:15672/api/nodes/rabbit@${host} | jq '.proc_used<.proc_total*.8

操作系统不会为每个进程创建线程。尽管如此,每个进程都使用轻量级栈,并且需要时间来调度和维护。

  • 内存和磁盘空间:如果内存或磁盘空间耗尽,RabbitMQ 将无法正常工作——例如,可能会触发流控制。请确保有足够的资源,并相应地调整硬件。

    应该使用的总内存量应小于内存使用量高水位线的 80%:

curl -s http://cc-admin:******@localhost:15672/api/nodes/rabbit@${host} | jq '.mem_used<.mem_limit*.8'curl -s

磁盘可用空间限制应与当前可用磁盘空间进行比较:

http://cc-admin:******@localhost:15672/api/nodes/rabbit@${host} | jq '.disk_free_limit<.disk_free*.8'

除了指标外,一个运行实例还会运行以下程序:

  • rabbitmq-server:这是显而易见的,但不应被遗忘!

  • epmd:Erlang 端口映射器守护进程epmd在集群和网络中起着关键作用。建议设置脚本以检查这些服务是否正在运行。在 Linux 或 macOS X 上使用ps列出程序,在 Windows 上使用Get-Process

主日志文件中的ERROR REPORT条目揭示了系统中的问题。在 Linux 中,RabbitMQ 将日志文件存储在/var/log/rabbitmq/rabbit@<hostname>.log。有关更多信息,请检查配置文件www.rabbitmq.com/logging.html#log-file-location

摘要

本章通过考察最佳实践和监控,总结了基于 RabbitMQ 构建的微服务的研究。本书经历了 CC 应用的应用流程,从基本服务到扩展。新功能和流程轻松添加,且没有中断 CC 应用。随着时间的推移,CC 的开发团队创建了一个全面、实用、可靠、长期运行的应用。为了避免可能导致不良用户体验或甚至数据丢失的故障,CC 团队实施了一个监控策略。在 CC 形成警报计划的过程中,概述了收集、记录、分析和报告指标。最后,通过 RabbitMQ 管理控制台设置了警报参数。

恭喜您完成了这本书的阅读之旅!拥有了足够的 RabbitMQ 驯服技能后,下一步是为您自己创建一个实例。开始使用 RabbitMQ 的一个简单方法是通过世界上最大的 RabbitMQ 集群托管服务提供商 CloudAMQP。

posted @ 2025-09-11 09:46  绝不原创的飞龙  阅读(9)  评论(0)    收藏  举报