服务到服务通信

提示

此内容摘自电子书《为 Azure 构建云原生 .NET 应用程序》,可在 .NET 文档上获取,也可作为免费可下载的 PDF 脱机阅读。

Cloud Native .NET apps for Azure eBook cover thumbnail.

从前端客户端开始,我们现在解决后端微服务之间的通信。

构造云原生应用程序时,需要注意后端服务之间的通信方式。 理想情况下,服务间通信越少,性能越好。 不过,由于后端服务通常相互依赖于另一个服务器来完成操作,因此不一定能避免这种情况。

有几种广泛接受的方法来实现跨服务通信。 通信交互的类型往往会决定最佳方法。

请考虑下列交互类型:

  • 查询 – 调用微服务需要来自被调用微服务的响应,如“你好,为给定客户 Id 提供买方信息。”

  • 命令 – 调用微服务需要另一个微服务来执行操作,但不需要响应,如“你好,仅发送此订单。”

  • 事件 – 当名为“发布者”的微服务引发状态已更改或操作已发生的事件时。 其他感兴趣的微服务(称为订阅者)可以相应地对事件做出反应。 发布者和订阅者彼此之间并不知道。

当执行需要跨服务交互的操作时,微服务系统通常会结合使用这些交互类型。 我们来看一看每个类型及其实现方式。

查询

很多时候,一个微服务需要查询另一个微服务,需要立即响应来完成操作。 购物篮微服务可能需要产品信息和价格才能将物品添加到购物篮中。 可以通过多种方法实现查询操作。

请求/响应消息传送

实现此方案的一种方法是,调用后端微服务向其需要查询的微服务直接发出 HTTP 请求,如图 4-8 所示。

Direct HTTP communication

图 4-8。 直接 HTTP 通信

尽管微服务之间的直接 HTTP 调用相对简单,但应注意尽量减少这种做法。 首先,这些调用始终同步,并将阻止操作,直到返回结果或请求超时。 曾经是自包含的独立服务,可以独立发展并经常部署,现在变成了相互耦合。 随着微服务之间的耦合度增加,它们在体系结构上优势就会降低。

如果执行的请求很少,则在某些系统中可进行对另一个微服务的单个直接 HTTP 调用。 但是,调用多个微服务的直接 HTTP 调用的高容量调用并不可取。 它们可能会增加延迟,并对系统的性能、可伸缩性和可用性产生负面影响。 更糟的是,一长串直接 HTTP 通信可能导致深度复杂的同步微服务调用链,如图 4-9 所示:

Chaining HTTP queries

图 4-9. 链接 HTTP 查询

当然,可以想像一下上图设计中所示的风险。 如果步骤 3 失败会发生什么情况? 或步骤 8 失败会发生什么情况? 如何进行恢复? 如果由于基础服务繁忙而导致步骤 6 缓慢,会怎么样? 如何继续操作? 即使全部操作运行正常,也可以考虑此调用会产生的延迟,这是每个步骤的延迟总和。

上图中的大量耦合表明服务未进行最佳建模。 该团队应重新考虑其设计。

具体化视图模式

用于删除微服务耦合的常用选项为具体化视图模式。 使用此模式,微服务存储其自己的本地非规范化数据副本(这些数据由其他服务拥有)。 “购物篮”微服务不是查询“产品目录”和“定价”微服务,而是保留其自己的本地数据副本。 此模式消除了不必要的耦合,并改进了可靠性和响应时间。 整个操作在单个进程内执行。 我们将在第 5 章探讨此模式和其他数据问题。

服务聚合器模式

消除微服务到微服务耦合的另一个选项是聚合器微服务,如图 4-10 中所示。

Aggregator service

图 4-10。 聚合器微服务

此模式隔离了对多个后端微服务进行调用的操作,将其逻辑集中到专门的微服务中。 上图中的紫色签出聚合器微服务协调签出操作的工作流。 它包括按顺序调用多个后端微服务。 工作流中的数据将聚合并返回给调用方。 尽管它仍实现直接 HTTP 调用,但聚合器微服务减少了后端微服务之间的直接依赖关系。

请求/答复模式

分离同步 HTTP 消息的另一种方法是请求-答复模式,这种模式使用队列通信。 使用队列的通信始终是单向通道,生成者发送消息,使用者接收消息。 使用此模式时,将实现请求队列和响应队列,如图 4-11 所示。

Request-reply pattern

图 4-11。 请求-答复模式

此处,消息生成者创建包含唯一相关 ID 的基于查询的消息,并将其放入请求队列。 使用服务取消排队该消息,处理消息,并将响应放入具有相同相关 ID 的响应队列。 生成者服务取消排队该消息,将其与相关 ID 匹配并继续处理。 我们将在下一节中详细介绍队列。

命令

通信交互的另一种类型为命令。 微服务可能需要另一个微服务才能执行操作。 “订单”微服务可能需要“发货”微服务才能创建已批准订单的发货信息。 在图 4-12 中,一个名为“生成者”的微服务将一条消息发送给另一个名为“使用者”的微服务,命令其执行操作。

Command interaction with a queue

图 4-12。 与队列的命令交互

通常“生成者”不需要相应,可对信息进行即发即弃操作。 如果需要答复,则“使用者”将单独的消息发送回另一个通道上的“生成者”。 命令消息最好通过消息队列以异步方式发送。 由轻型消息中转站支持。 在前面的关系图中,请注意队列如何分隔并分离这两个服务。

消息队列是一个中介构造,使用者和使用者通过它传递消息。 队列实现了异步的点到点消息传送模式。 “生成者”知道需要将命令发送到何处并进行正确路由。 队列保证消息正好由从通道读取的某个使用者实例进行处理。 在此方案中,生成者或使用者服务可以横向扩展,而不会影响另一个。 而且,每一方的技术都可能完全不同,这意味着我们可能会有一个 Java 微服务调用 Golang 微服务。

在第 1 章中,我们谈到了支持服务。 支持服务是云本机系统所依赖的辅助资源。 消息队列是支持服务。 Azure 云支持使用两种类型的消息队列,云原生系统将其用来实现命令消息传送:Azure 存储队列和 Azure 服务总线队列。

Azure 存储队列

Azure 存储队列提供了一个简单的队列基础结构,该基础结构速度快、经济实惠,并由 Azure 存储帐户提供支持。

Azure 存储队列采用一种基于 REST 的排队机制,提供可靠且持久的消息传送。 它们提供了最小的功能集,但价格较低,并可存储数百万条消息。 其容量范围高达 500 TB。 单个消息的大小最大可为 64 KB。

可以通过使用 HTTP 或 HTTPS 的经过身份验证调用,从世界上的任何地方访问消息。 存储队列可以横向扩展至大量并发客户端,以处理流量高峰。

也就是说,服务存在一些限制:

  • 不保证消息顺序。

  • 在自动删除消息之前,消息只能保留七天。

  • 不支持状态管理、重复检测或事务。

图 4-13 显示了Azure 存储队列的层次结构。

Storage queue hierarchy

图 4-13。 存储队列层次结构

在上图中,请注意存储队列如何将消息存储在基础 Azure 存储帐户中。

对于开发人员,Microsoft 提供了多个用于存储队列处理的客户端和服务器端库。 支持大多数主要平台,包括 .NET、Java、JavaScript、Ruby、Python 和 Go。 开发人员不应直接与这些库通信。 这样做会将微服务代码紧密耦合到 Azure 存储 队列服务。 最好隔离 API 的实现细节。 引入一个中间层或中间 API,用于公开泛型操作并封装具体库。 这种松散耦合使你能够将一个队列服务换为另一个队列服务,而无需更改主线服务代码。

Azure 存储队列是一个经济实惠的选项,用于实现云原生应用程序中的命令消息传送。 尤其是在队列大小超过 80 GB 或可接受简单功能集时。 只需为消息的存储付费;没有固定的每小时费用。

Azure 服务总线队列

有关更复杂的消息传送要求,请考虑使用 Azure 服务总线队列。

Azure 服务总线基于强大的消息基础设施,支持中转消息传送模型。 消息可靠地存储在中转站(队列)中直到使用者收到。 队列保证 First-In/First-Out (FIFO) 消息传送,同时遵循消息添加到队列的顺序。

消息的大小可以更大,最多 256 KB。 消息将无限期地持久保留在队列中。 服务总线不仅支持基于 HTTP 的调用,而且还对 AMQP 协议提供完全支持。 AMQP 是跨供应商开放标准,支持二进制协议和更高级别的可靠性。

服务总线提供了一套丰富的功能,包括交易支持重复检测功能。 队列保证每条消息“最多传递一次”。 它会自动放弃已发送的消息。 如果不能确定生成者,它可以重新发送相同的消息,服务总线保证只处理一个副本。 重复检测无需构建其他基础结构管道。

另外两个企业功能是分区和会话。 传统的服务总线队列由单个消息中转站处理,并存储在单个消息存储中。 但是,服务总线分区将队列分散在多个消息中转站和消息存储中。 整体吞吐量不再受单个消息中转站或消息存储的性能限制。 消息存储的暂时中断不会导致分区的队列不可用。

会话服务总线提供对消息进行分组相关的方法。 想象一下在一个工作流方案中,消息必须一起处理,且必须在最后完成操作。 若要利用这一优势,必须为队列显式启用会话,并且每个相关消息必须包含相同的会话 ID。

但是,有一些重要的注意事项:服务总线队列大小限制为 80 GB,这比存储队列提供的大小小得多。 此外,服务总线队列会产生基本成本和每个操作的费用。

图 4-14 概述了服务总线队列的高级体系结构。

Service Bus queue

图 4-14。 服务总线队列

在上图中,请注意点到点关系。 同一提供程序的两个实例将消息排入单个服务总线队列。 每条消息仅由右侧三个使用者实例中的一个使用。 接下来,我们将讨论如何实现消息传送,其中不同的使用者可能都对同一条消息感兴趣。

事件

消息队列是实现通信的有效方法,其中生成者可以异步向使用者发送消息。 然而,当许多不同的使用者对同一条消息感兴趣时,会发生什么呢? 每个使用者的专用消息队列无法很好地缩放,并且难以管理。

为了应对这种情况,我们转向第三种类型的消息交互,即事件。 一个微服务宣布进行了操作。 其他微服务(如果感兴趣)对操作或事件做出反应。 这也称为事件驱动的体系结构样式

事件处理是一个两步过程。 对于给定的状态更改,微服务将事件发布到消息中转站,使其可供任何其他感兴趣的微服务使用。 订阅消息中转站中的事件来通知感兴趣的微服务。 使用发布/订阅模式来实现基于事件的通信

图 4-15 显示了购物篮微服务,该微服务发布一个事件,其他两个微服务订阅了该事件。

Event-Driven messaging

图 4-15。 事件驱动的消息传送

注意位于通信通道中间的事件总线组件。 它是一个自定义类,用于封装消息中转站,并将其与基础应用程序分离。 订单和库存微服务独立运行事件,彼此不了解,购物篮微服务也不知情。 当注册的事件发布到事件总线时,它们会处理。

通过事件处理,我们从队列技术转向主题。 主题类似于队列,但支持一对多消息传送模式。 一个微服务发布一条消息。 多个订阅微服务可以选择接收该消息并处理。 图 4-16 显示了主题体系结构。

Topic architecture

图 4-16。 主题体系结构

在上图中,发布者向主题发送消息。 最后,订阅者从订阅接收消息。 在中间,该主题根据一组规则将消息转发到订阅,如图中深蓝色框显示。 规则充当将特定消息转发到订阅的筛选器。 此处,“GetPrice”事件会发送到价格和日志记录订阅,因为已选择日志记录订阅接收所有消息。 “GetInformation”事件会发送到信息和日志记录订阅。

Azure 云支持两种不同的主题服务:Azure 服务总线主题和 Azure 事件网格。

Azure 服务总线主题

基于 Azure 服务总线队列的同一可靠中转消息模型的是 Azure 服务总线主题。 一个主题可以接收来自多个独立发布者的消息,并将消息发送到最多 2,000 个订阅者。 可以在运行时动态添加或删除订阅,而无需停止系统或重新创建主题。

Azure 服务总线队列的许多高级功能也可用于主题,包括重复检测事务支持。 默认情况下,服务总线主题由单个消息中转站处理,并存储在单个消息存储中。 但是,服务总线分区通过将主题分散到多个消息中转站和消息存储中来对其进行缩放。

计划消息传递对消息标记特定处理时间。 在该时间之前,此消息不会出现在主题中。 消息延迟使你能够将消息的检索延迟到稍后的时间。 这两者通常用于工作流处理方案,其中操作按特定顺序进行处理。 可以推迟处理收到的消息,直到完成之前的工作。

服务总线主题是一种可靠且经过验证的技术,用于在云原生系统中启用发布/订阅通信。

Azure 事件网格

虽然 Azure 服务总线是一个经过全面测试的消息传送中转站,具有一整套企业功能,而Azure 事件网格是新生事物。

一开始,事件网格可能看起来只是另一个基于主题的消息传送系统。 但是,它在许多方面有所不同。 它侧重于事件驱动的工作负载,支持实时事件处理、深度 Azure 集成和开放平台 - 所有这些都在无服务器基础结构上实现。 它专为现代的云原生和无服务器应用程序而设计

事件网格作为集中式事件底板或管道,对 Azure 资源内部和你自己的服务中的事件做出反应。

事件通知发布到事件网格主题,而事件网格主题又将每个事件路由到订阅。 订阅者映射到订阅并使用该事件。 与服务总线一样,事件网格支持筛选的订阅者模型,其中订阅为它希望接收的事件设置规则。 事件网格提供快速吞吐量,保证每秒 1000 万个事件,实现准实时传递,这远高于 Azure 服务总线所能生成的。

事件网格的一个亮点是,它能与 Azure 基础设施的结构深度集成。 Azure 资源(例如 Cosmos DB)可以直接将内置事件发布到其他感兴趣的 Azure 资源,而无需自定义代码。 事件网格可以从 Azure 订阅、资源组或服务发布事件,使开发人员能够精细地控制云资源的生命周期。 但是,事件网格不限于 Azure。 它是一个开放平台,可以使用从应用程序或第三方服务发布的自定义 HTTP 事件,以及将事件路由到外部订阅者。

从 Azure 资源发布和订阅本机事件时,无需编码。 使用简单的配置,可以使用主题和订阅的内置管道将事件从一个 Azure 资源集成到另一个 Azure 资源。 图 4-17 显示了事件网格的解析。

Event Grid anatomy

图 4-17。 事件网格解析

事件网格和服务总线的主要区别在于基础消息交换模式。

服务总线实现较旧的拉取模型,其中下游订阅者主动轮询主题订阅以寻找新消息。 从好的方面来看,订阅者可通过此方法完全控制其处理消息的速度。 它控制在任意给定时间要处理消息的时间和要处理的消息数。 未读消息保留在订阅中,直到处理完毕。 一个明显的缺点是生成事件的时间与将该消息拉取到订阅者进行处理的轮询操作之间的延迟。 此外,持续轮询下一个事件的开销会消耗资源和资金。

但是,事件网格不同。 它实现推送模型,在该模型中,事件在接收时发送到 EventHandlers,从而提供准实时事件传递。 它还降低了成本,因为服务仅在需要使用事件时触发,而不是像轮询一样持续触发。 也就是说,事件处理程序必须处理传入的负载并提供限制机制,以保护自身免于过载。 许多使用这些事件的 Azure 服务(例如 Azure Functions 和逻辑应用)都提供自动化自动缩放功能来处理增加的负载。

事件网格是完全托管的无服务器云服务。 它根据流量动态缩放,且仅根据实际使用情况(而不是预购容量)收费。 每月前 100,000 个操作免费 - 定义为事件流入(传入事件通知)、订阅传递尝试、管理调用和按主题筛选的操作。 凭借 99.99% 的可用性,事件网格保证在 24 小时内传递事件,并为不成功的传递提供内置重试功能。 未传送的消息可以移动到“死信”队列进行解析。 与 Azure 服务总线不同,事件网格经过优化以提高性能,不支持有序消息传送、事务和会话等功能。

在 Azure 云中流式传输消息

Azure 服务总线和事件网格为公开单个离散事件(如已将新文档插入到 Cosmos DB 中)的应用程序提供了很好的支持。 但是,如果云原生系统需要处理相关事件流,怎么样? 事件流比较复杂。 它们通常按时间排序、相互关联,而且必须作为一个组进行处理。

Azure 事件中心是一个数据流式处理平台和事件引入服务,用于收集、转换和存储事件。 经过调整优化,它可以捕获流式处理的数据,例如从遥测上下文发出的连续事件通知。 该服务高度可缩放,每秒可以存储和处理数百万个事件。 如图 4-18 所示,它通常是事件管道的前端,将引入流与事件使用分离。

Azure Event Hub

图 4-18. Azure 事件中心

事件中心支持低延迟和可配置的时间保留。 与队列和主题不同,事件中心在使用者读取事件数据后保留事件数据。 此功能允许内部和外部的其他数据分析服务重播数据以进一步分析。 只有在保持期到期(默认为一天,但可配置)后,才删除存储在事件中心中的事件。

事件中心支持常见的事件发布协议,包括 HTTPS 和 AMQP。 它还支持 Kafka 1.0。 现有的 Kafka 应用程序可以使用 Kafka 协议与事件中心通信,从而提供管理大型 Kafka 群集的替代方法。 许多开放源代码云原生系统都采用 Kafka。

事件中心通过分区使用者模型实现消息流式处理,在此模型中,每个使用者只读取消息流的特定子集或分区。 此模式允许以极大的水平缩放规模进行事件处理,并提供队列和主题所不能提供的其他面向流的功能。 分区是事件中心内保留的有序事件。 当较新的事件到达时,它们将添加到此序列的末尾。 图 4-19 显示了事件中心中的分区。

Event Hub partitioning

图 4-19. 事件中心分区

每个使用者组读取消息流的子集或分区,而不是从同一资源读取。

对于必须流式传输大量事件的云原生应用程序,Azure 事件中心可作为可靠且经济实惠的解决方案。