微服务架构设计模式笔记--第三章 微服务架构中的进程间通信

当前有多种进程间通信机制供开发者选择。比较流行的是REST(使用JSON)。但是,需要牢记“没有银弹”这个大原则。你必须仔细考虑这些选择。本章将探讨各种进程间通信机制,包括REST和消息传递,并讨论如何进行权衡。
选择合适的进程间通信机制是一个重要的架构决策。它会影响应用程序可用性。更重要的是,一个理想的微服务架构应该是在内部由松散耦合的若干服务组成,这些服务使用异步消息相互通信。REST等同步协议主要用于服务与外部其他应用程序的通信。
在这里插入图片描述

1. 微服务架构中的进程间通信概述

在这里插入图片描述

1.1 交互方式

有多种客户端与服务的交互方式。可以分为两个维度。
第一个维度关注的是一对一和一对多。

  • 一对一:每个客户端请求由一个服务实例来处理。
  • 一对多:每个客户端请求由多个服务实例来处理。

交互方式的第二个维度关注的是同步和异步。

  • 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞。
  • 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的。
一对一的交互方式
  • 请求/响应:一个客户端向服务端发起请求,等待响应;客户端期望服务端很快就会发送响应。在一个基于线程的应用中,等待过程可能造成线程阻塞。这样的方式会导致服务的紧耦合。
  • 异步请求/响应:客户端发送请求到服务端,服务端异步响应请求。客户端在等待响应时不会阻塞线程,因为服务端的响应不会马上就返回。
  • 单向通知:客户端的请求发送到服务端,但是并不期望服务端做出任何响应。
一对多的交互方式
  • 发布/订阅方式:客户端发布通知消息,被零个或者多个感兴趣的服务订阅。
  • 发布/异步响应方式:客户端发布请求消息,然后等待从感兴趣的服务发回的响应。

1.2 在微服务架构中定义API

API或接口是软件开发的中心。应用是由模块构成的,每个模块都有接口,这些接口定义了模块的客户端可以调用若干操作。一个设计良好的接口会在暴露有用功能同时隐藏实现的细节。因此,这些实现的细节可以被修改,而接口保持不变,这样就不会对客户端产生影响。
相比单体架构,我们面临的挑战在于:并没有一个简单的编程语言结构可以用来构造和定义服务的API。根据定义,服务和它的客户端并不会一起编译。如果使用不兼容的API部署新版本的服务,虽然在编译阶段不会出现错误,但是会出现运行时故障。

API优先设计
即使在那些最简单的项目中,组件和API之间也常常发生冲突。例如,负责后端的Java程序员和负责前端的AngularJS程序员都声称他们完成了开发,然而这个应用程序却无法工作。前端的REST和 Web Socket API无法与后端的API一起工作。最终导致这 个应用程序的前端和后端无法正常通信!

服务的API很少一成不变,它可能会随着时间的推移而发展。让我们来看看如何做到这点,并讨论你将面临的问题。

1.3 API的演化

语义化版本控制规范为API版本控制提供了有用的指导。它是一组规则,用于指定如何使用版本号,并且以正确的方式递增版本号。语义化版本控制最初的目的是用软件包的版本控制,但你可以将其用在分布式系统中对API进行版本控制。
语义化版本控制规范要求版本号由三部分组成: MAJOR. MINOR. PATCH。必须按如下方式递增版本号:

  • MAJOR:当你对API进行不兼容的更改时。
  • MINOR:当你对API进行向后兼容的增强时。
  • PATCH:当你进行向后兼容的错误修复时。

1.4 消息的格式

  • 基于文本的消息格式(JSON、XML):主要应用HTTP协议
  • 二进制消息格式(Protocol Buffers、Avro):应用gRPC

2. 基于同步远程过程调用模式的通信

在这里插入图片描述
当使用基于远程过程调用的进程间通信机制时,客户端向服务发送请求,服务处理该请求并发回响应。有些客户端可能会处在堵塞状态并等待响应,而其他客户端可能会有一个响应式的非阻塞架构。但与使用消息机制时不同,客户端假定响应将及时到达。
在这里插入图片描述

2.1 使用REST

好处:

  • 它非常简单,并且大家都很熟悉。
  • 可以使用浏览器扩展(比如 Postman插件)或者curl之类的命令行(假设使用的是JSON或其他文本格式)来测试Http api。
  • 直接支持请求/响应方式的通信。
  • HTTP对防火墙友好。
  • 不需要中间代理,简化了系统架构。

弊端:

  • 它只支持请求/响应方式的通信。
  • 可能导致可用性降低。由于客户端和服务直接通信而没有代理来缓冲消息,因此它们必须在 REST API调用期间都保持在线。
  • 客户端必须知道服务实例的位置(URL)。这是现代应用程序中的一个重要问题。客户端必须使用所谓的服务发现机制来定位服务实例在单个请求中获取多个资源具有挑战性。
  • 有时很难将多个更新操作映射到HTTP动词。

虽然存在这些缺点,但REST似乎是API的事实标准,尽管有几个有趣的替代方案。例如,通过 GraphQL实现灵活、高效的数据提取。

2.2 使用gRPC

好处:

  • 设计具有复杂更新操作的API非常简单。
  • 它具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时。
  • 支持在远程过程调用和消息传递过程中使用双向流式消息方式。
  • 它实现了客户端和用各种语言编写的服务端之间的互操作性。
    弊端:
  • 与基于 RESTASON的API机制相比,JavaScript客户端使用基于gRPC的API需要做更多的工作。
  • 旧式防火墙可能不支持HTTP/2。

gRPC是REST的一个引人注目的替代品,但与REST一样,它是一种同步通信机制,因此它也存在局部故障的问题。

2.3 使用断路器模式处理局部故障

分布式系统中,当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险。因为客户端和服务端是独立的进程,服务端很有可能无法在有限的时间内对客户端的请求做出响应。服务端可能因为故障或维护的原因而暂停。或者,服务端也可能因为过载而对请求的响应变得极其缓慢。
客户端等待响应被阻塞,这可能带来的麻烦就是在其他客户端甚至使用服务的第三方应用之间传导,并导致服务中断。
在这里插入图片描述
要通过合理地设计服务来防止在整个应用程序中故障的传导和扩散,这是至关重要的。
解决这个问题分为两部分:

  • 必须让远程过程调用代理有正确处理无响应服务的能力。(Netflix Hystrix)
  • 需要决定如何从失败的远程服务中恢复。(API Gateway返回备用值)
    在这里插入图片描述

2.4 使用服务发现

假设你正在编写一些调用具有REST API的服务的代码。为了发出请求,你的代码需要知道服务实例的网络位置(IP地址和端口)。在物理硬件上运行的传统应用程序中,服务实例的网络位置通常是静态的。例如,你的代码可以从偶尔更新的配置文件中读取网络位置。但在现代的基于云的微服务应用程序中,通常不那么简单。现代应用程序更具动态性。
在这里插入图片描述
服务实例具有动态分配的网络位置。此外,由于自动扩展、故障和升级,服务实例集会动态更改。因此,你的客户端代码必须使用服务发现。

应用层服务发现模式

在这里插入图片描述
这种服务发现方法是两种模式的组合:

  • 自注册模式:服务实例调用服务注册表的注册API来注册其网络位置。它还可以提供运行状况检查URL,运行状况检查URL是一个API端点,服务注册表会定期调用该端点来验证服务实例是否正常且可用于处理请求。服务注册表还可能要求服务实例定期调用“心跳”API以防止其注册过期。
  • 客户端发现模式:当客户端想要调用服务时,它会査询服务注册表以获取服务实例的列表。为了提高性能,客户端可能会缓存服务实例。然后,服客户端使用负载平衡算法(例如循环或随机)来选择服务实例。然后它向选择的服务实例发出请求。

应用层服务发现的一个好处是它可以处理多平台部署的问题(服务发现机制与具体的部署平台无关)其余服务在遗留环境中运行。在这种情况下,使用 Eureka的应用层服务发现同时适用于两种环境,而基于Kubernetes的服务发现仅能用于部署在 Kubernetes平台之上的部分服务。
应用层服务发现的一个弊端是:你需要为你使用的每种编程语言(可能还有框架)提供服务发现库。Node. js或GoLang则必须找到其他一些服务发现框架。应用层服务发现的另一个弊端是开发者负责设置和管理服务注册表,这会分散一定的精力。因此,最好使用部署基础设施提供的服务发现机制。

平台层服务发现模式

许多现代部署平台(如Docker和Kubernetes)都具有内置的服务注册表和服务发现机制。部署平台为每个服务提供DNS名称、虚拟IP(VIP)地址和解析为VIP地址的DNS名称。客户端向DNS名称和VIP发出请求,部署平台自动将请求路由到其中一个可用服务实例。因此,服务注册、服务发现和请求路由完全由部署平台处理。
在这里插入图片描述
这种方法是以下两种模式的组合:

  • 第三方注册模式:由第三方负责(称为注册服务器,通常是部署平台的一部分)处理注册,而不是服务本身向服务注册表注册自己。
  • 服务端发现模式:客户端不再需要查询服务注册表,而是向DNS名称发出请求对该DNS名称的请求被解析到路由器,路由器查询服务注册表并对请求进行负载均衡。

由平台提供服务发现机制的主要好处是服务发现的所有方面都完全由部署平台处理。服务和客户端都不包含任何服务发现代码。因此,无论使用哪种语言或框架,服务发现机制都可供所有服务和客户使用。
平台提供服务发现机制的一个弊端是它仅限于支持使用该平台部署的服务。例如,如前所述,在描述应用程序级别发现时,基于 Kubernetes的发现仅适用于在 Kubernetes上运行的服务。尽管存在此限制,我建议尽可能使用平台提供的服务发现。

3. 基于异步消息模式的通信

在这里插入图片描述
使用消息机制时,服务之间的通信采用异步交换消息的方式完成。基于消息机制的应用程序通常使用消息代理,它充当服务之间的中介。另一种选择是使用无代理架构,通过直接向服务发送消息来执行服务请求。服务客户端通过向服务发送消息来发出请求。如果希望服务实例回复,服务将通过向客户端发送单独的消息的方式来实现。由于通信是异步的,因此客户端不会堵塞和等待回复。相反,客户端都假定回复不会马上就收到。

3.1 什么是消息传递

在这里插入图片描述
消息的不同类型:

  • 文档:仅包含数据的通用消息。接收者决定如何解释它。对命令式消息的回复是文档消息的一种使用场景。
  • 命令:一条等同于RPC请求的消息。它指定要调用的操作及其参数。
  • 事件:表示发送方这一端发生了重要的事件。事件通常是领域事件,表示领域对象的状态更改。

两种类型的消息通道:

  • 点对点通道:例如,命令式消息通常通过点对点通道发送。
  • 发布-订阅通道:例如,事件式消息通常通过发布一订阅通道发送。

3.2 使用消息机制实现交互方式

  • 实现请求/响应和异步请求/响应
    在这里插入图片描述
  • 实现单向通知
  • 实现发布/订阅
  • 实现发布/异步响应

3.3 为基于消息机制的服务API创建API规范

在这里插入图片描述

如图所示,服务的异步API规范必须指定消息通道的名称、通过每个通道交换的消息类型及其格式。你还必须使用诸如JSON、XML或 Protobuf之类的标准来描述消息的格式。但与REST和 Open API不同,并没有广泛采用的标准来记录通道和消息类型,你需要自己编写这样的文档。

记录异步操作
  • 请求/异步响应式API:包括服务的命令消息通道、服务接受的命令式消息的具体类型和格式,以及服务发送的回复消息的类型和格式。
  • 单向通知式API:包括服务的命令消息通道,以及服务接受的命令式消息的具体类型和格式。
记录事件发布

服务还可以使用发布/订阅的方式对外发布事件。此API风格的规范包括事件通道以及服务发布到通道的事件式消息的类型和格式。

3.4 使用消息代理

在这里插入图片描述

基于消息传递的应用程序通常使用消息代理,即服务通信的基础设施服务。但基于消息代理的架构并不是唯一的消息架构。你还可以使用基于无代理的消息传递架构,其中服务直接相互通信。这两种方法具有不同的利弊,但通常基于代理的架构是一种更好的方法。
无代理的架构好处:

  • 允许更轻的网络流量和更低的延迟,因为消息直接从发送方发送到接收方,而不必从发送方到消息代理,再从代理转发到接收方。
  • 消除了消息代理可能成为性能瓶颈或单点故障的可能性。
  • 具有较低的操作复杂性,因为不需要设置和维护消息代理。

弊端

  • 服务需要了解彼此的位置,因此必须使用服务发现机制。
  • 会导致可用性降低,因为在交换消息时,消息的发送方和接收方都必须同时在线。
  • 在实现例如确保消息能够成功投递这些复杂功能时的挑战性更大。

实际上,这些弊端中的一些(例如可用性降低和需要使用服务发现),与使用同步请求/响应交互方式所导致的弊端相同。

有许多消息代理可供选择。流行的开源消息代理包括:

  • Apache ActiveMQ
  • RabbitMQ
  • Apache Kafka

基于代理的消息的好处和弊端
好处:

  • 松耦合:客户端发起请求时只要发送给特定的通道即可,客户端完全不需要感知服务实例的情况,客户端不需要使用服务发现机制去获得服务实例的网络位置。
  • 消息缓存:消息代理可以在消息被处理之前一直缓存消息。像HTTP这样的同步请求/响应协议,在交换数据时,发送方和接收方必须同时在线。然而,在使用消息机制的情况下,消息会在队列中缓存,直到它们被接收方处理。这就意味着,例如,即使订单处理系统暂时离线或不可用,在线商店仍旧能够接受客户的订单。订单消息将会在队列中缓存(并不会丢失)。
  • 灵活的通信:消息机制支持前面提到的所有交互方式。
  • 明确的进程间通信:基于RPC的机制总是企图让远程服务调用跟本地调用看上去没什么区别(在客户端和服务端同时使用远程调用代理)。然而,因为物理定律(如服务器不可预计的硬件失效)和可能的局部故障,远程和本地调用还是大相径庭的。消息机制让这些差异变得很明确,这样程序员不会陷入一种“太平盛世”的错觉。

弊端:

  • 潜在的性能瓶颈:消息代理可能存在性能瓶颈。幸运的是,许多现代消息代理都支持高度的横向扩展。
  • 潜在的单点故障:消息代理的高可用性至关重要,否则系统整体的可靠性将受到影响。幸运的是,大多数现代消息代理都是高可用的。
  • 额外的操作复杂性:消息系统是一个必须独立安装、配置和运维的系统组件。

3.5 处理并发和消息顺序

挑战之一是如何在保留消息顺序的同时,横向扩展多个接收方的实例。为了同时处理消息,拥有多个实例是一个常见的要求。而且,即使单个服务实例也可能使用线程来同时处理多个消息。使用多个线程和服务实例来并发处理消息可以提高应用程序的吞吐量。但同时处理消息的挑战是确保每个消息只被处理一次,并且是按照它们发送的顺序来处理的。
在这里插入图片描述
现代消息代理(Apache Kafka)使用的常见解决方案是使用分片(分区)通道。

  • 并发:分片处理并发。
  • 顺序:特定订单的每个事件都放到同一个分片。

3.6 处理重复消息

使用消息机制时必须解决的另一个挑战是处理重复消息。理想情况下,消息代理应该只传递一次消息,但保证有且仅有一次的消息传递通常成本很高。相反,大多数消息代理承诺至少成功传递一次消息。
处理重复消息有以下两种不同的方法。

编写幂等消息处理程序

如果应用程序处理消息的逻辑是满足幂等的,那么重复的消息就是无害的。不幸的是,应用程序逻辑通常不是幂等的。或者你可能正在使用消息代理,该消息代理在重新传递消息时不会保留排序。重复或无序消息可能会导致错误。

跟踪消息并丢弃重复项

例如,考虑一个授权消费者信用卡的消息处理程序。它必须为每个订单仅执行一次信用卡授权操作。这段应用程序逻辑在每次调用时都会产生不同的效果。如果重复消息导致消息处理程序多次执行该逻辑,则应用程序的行为将不正确。执行此类应用程序逻辑的消息处理程序必须通过检测和丢弃重复消息而成为幂等的。
一个简单的解决方案是消息接收方使用 message id跟踪它已处理的消息并丢弃任何重复项。例如,它可以存储它在数据库表中使用的每条消息的 message id。
在这里插入图片描述
当接收方处理消息时,它将消息的 message id作为创建和更新业务实体的事务的部分记录在数据库表中。在此示例中,接收方将包含 message id的行插入 PROCESSED MESSAGES表。如果消息是重复的,则 INSER将失败,接收方可以选择丢弃该消息。
另一个选项是消息处理程序在应用程序表,而不是专用表中记录 message id。当使用具有受限事务模型的 NoSQL数据库时,此方法特别有用,因为 NOSQL数据库通常不支持将针对两个表的更新作为数据库的事务。

3.7 事务性消息

服务通常需要在更新数据库的事务中发布消息。如果服务不以原子方式执行这两个操作则类似的故障可能使系统处于不一致状态。
传统的解决办法是在数据库和消息代理之间使用分布式事务。然而,分布式事务对现今的应用程序而言并不是一个很好的选择。而且,很多新的消息代理不支持分布式事务。
解决方案:

  1. 使用数据库表作为消息队列
    在这里插入图片描述
    我们假设你的应用程序正在使用关系型数据库。可靠地发布消息的直接方法是应用事务性发件箱模式。此模式使用数据库表作为临时消息队列。如图所示,发送消息的服务有一个OUTBOX数据库表。作为创建、更新和删除业务对象的数据库事务的一部分,服务通过将消息插入到OUTBOX表中来发送消息。这样可以保证原子性,因为这是本地的ACID事务。通过轮询数据库中的发件箱来发布消息。
  2. 消息相关的类库和框架(Eventuate Tram)
    实现了两个重要机制:
    1.事务性消息机制:它将消息作为数据库事务的一部分发布。
    2.重复消息检测机制:支持消息的接收方检测并丢弃重复消息,这对于确保接收方只准确处理消息一次至关重要。

4. 使用异步消息提高可用性

在这里插入图片描述

4.1 同步消息会降低可用性

REST是一种非常流行的进程间通信机制。你可能很想将它用于服务间通信。但是,REST的问题在于它是一个同步协议:HTTP客户端必须等待服务端返回响应。只要服务使用同步协议进行通信,就可能降低应用程序的可用性。
从统计意义上讲,一个系统操作的可用性,由其所涉及的所有服务共同决定。如果Order service服务和它所调用的两个服务的可用性都是99.5%3,那么这个系统操作的整体可用性就是99.5%3,大约是98.5%3,这其实是个非常低的数值了。每一个额外增加的服务参与到其中,都会更进一步降低整体系统操作的可用性。

4.2 消除同步交互

  1. 使用异步交互模式 在这里插入图片描述
  2. 复制数据 在这里插入图片描述
  3. 先返回响应,再完成处理
    在这里插入图片描述
    1.仅使用本地的数据来完成请求的验证。
    2.更新数据库,包括向 OUTBOⅩ表插入消息。
    3.向客户端返回响应。

猜你喜欢

转载自blog.csdn.net/ylnlp5602260/article/details/113792339
今日推荐