前言

这是一本关于微服务架构设计方面的书,这是本人阅读的学习笔记。首先对一些符号做些说明:

()为补充,一般是书本里的内容;
[]符号为笔者笔注;

微服务架构将应用程序构建为一组服务,这些服务必须经常协作才能处理各种外部请求。而服务的实例通常是在多台机器上运行的进程,所以它们必须使用进程间通信进行交互。

当前有多种进程间通信机制,比较流行的是REST(使用JSON)。选择合适的进程间通信机制是一个重要的架构决策,它影响应用程序的可用性。


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

进程间通信技术有:基于同步请求/响应、异步的基于消息的通信机制等。

1.1 交互方式的两个维度

  • 第一个维度

    • 一对一:每个客户端请求由一个服务实例来处理;
    • 一对多:每个客户端请求由多个服务实例来处理;
  • 第二个维度

    • 同步模式:客户端请求需要服务端实时响应,客户端等待响应时可能导致堵塞;
    • 异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的;

交互方式的两个维度

1.2 交互方式的类型

  • 一对一交互

    • 请求/响应:一个客户端向服务端发起请求,等待响应;客户端期望服务端很快就会发送响应。在一个基于线程的应用中,等待过程可能造成线程阻塞。这样的方式会导致服务的紧耦合;
    • 异步请求/响应:客户端发送请求到服务端,服务端异步响应请求。客户端在等待时不会阻塞线程,因为服务端响应不会马上返回;
    • 单向通知:客户端的请求发送到服务端,但是并不期望服务端做出任何响应;
  • 一对多交互

    • 发布/订阅方式:客户端发布通知消息,被零个或多个感兴趣的服务订阅;
    • 发布/异步响应方式:客户端发布请求消息,然后等待从感兴趣的服务发回的响应;

1.3 API的演化

  • 语义化版本控制:用于指定如何使用版本号,并且以正确的方式递增版本。其由3部分组成:

    • MAJOR:对API进行不兼容的修改时;
    • MINOR:对API进行向后兼容的增强时;
    • PATCH:进行向后兼容的错误修复时;
    • 规范:MAJOR.MINOR.PATCH
  • 进行次要并且向后兼容的改变:对ADP的附加修改更换或功能增强。其包括:

    • 添加可选属性
    • 向响应添加属性
    • 添加新操作
  • 进行主要并且不向后兼容的版本:需要服务在一段时间内同时支持新旧版本的API时;

1.4 消息的格式

消息格式会影响进程间通信的效率、API的可用性和可演化新。使用跨语言的消息格式尤为重要;

  • 基于文本的消息格式

    • 举例:JSON、XML;
    • 好处:可读性高、自描述性,有良好的向后兼容性 [消息接收方只需挑选他们感兴趣的值,忽略其他];
    • 弊端:信息冗长,解析文本需要额外的性能效率开销;
  • 二进制消息格式

    • 举例:Tars、Protocol Buffers、Avro;
    • 好处:提供强类型定义的IDL(接口描述文件),用于定义消息;编译器会根据这些格式生成序列化和反序列化代码;
    • 弊端:不得不采用API优先的方法进行服务设计

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

2.1 远程过程调用RPI

指客户端使用同步的远程过程调用协议(如REST)来调用服务。

远程过程调用工作原理

图解:客户端业务逻辑调用代理接口,这个接口由远程过程调用代理适配器类实现。远程过程调用代理向服务器发送请求,该请求由远程过程调用服务器适配器类处理,该类通过接口调用服务的业务逻辑。然后它将恢复发送回远程过程调用代理,该代理将结果返回给客户端的业务逻辑。

  • 代理接口:通常是封装底层通信协议,如下面介绍的REST与gRPC。

2.2 REST通信协议的特点及优缺点

REST是一种(总是)使用HTTP协议的进程间通信机制。

特点

  • REST使用HTTP动词来操作资源,使用URL引用这些资源;
  • 资源通常使用XML文档或JSON对象的形式,也可以使用其他格式(二进制等);
  • REST的成熟模型有:有4个层次(P71);
  • REST API:最流行的REST IDL是Open API规范,它是从Swagger开源项目发展而来的;
  • REST API的挑战

    • 在一个请求中获取多个资源的挑战:指如何在单个请求中检索多个相关对象;
    • 吧操作映射为HTTP动词的挑战:指一个HTTP动词可能对应多种方法,如PUT请求更新订单可能包括取消订单、修改订单等;

好处

  • 非常简单,大家比较熟悉;
  • 可以使用浏览器扩展(如Postman插件)或者curl之类的命令行测试HTTP API;
  • 直接支持请求/响应方式的通信;
  • HTTP对防火墙友好;
  • 不需要中间代理,简化系统架构;

弊端

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

2.3 gRPC通信协议的特点及优缺点

gRPC是一个用于编写跨语言客户端和服务端的框架,是一种二进制协议。

特点

  • gRPC API由一个或多个服务和请求/响应消息定义组成;
  • 服务定义类似Java接口,是强类型方法的集合;
  • 使用Protocol Buffers作为消息格式,是一种高效且紧凑的二进制格式,是一种标记格式;
    • 因此gRPC使API能够在保持向后兼容的同时进行变更;

好处

  • 设计具有复杂更新操作的API非常简单;
  • 具有高效、紧凑的进程间通信机制,尤其是在交换大量消息时;
  • 支持在远程过程调用和消息传递过程中使用双向流式消息方式;
  • 实现了客户端和用各种语言编写的服务端之间的互操作性;

弊端

  • 与基于REST/JSON的API机制相比,JavaScript客户端使用基于gRPC的API需要做更多的工作;
  • 旧式防火墙可能不支持HTTP/2;

2.4 同步通信下的局部故障风险

客户端和服务端是独立的进程,服务端很可能无法在有限的时间内对客户端的请求作出响应。

同步通信下的局部故障风险
图解:当Order Service无响应时,OrderServiceProxy将无限期地阻塞,等待响应。会消耗时间、浪费线程等资源。最终API Gateway将资源消耗,无法处理请求,整个API不可用。

解决方法是

  • 必须让远程过程调用代理(如OrderServiceProxy)有正确处理无响应服务的能力;
  • 需要决定如何从失败的远程服务中恢复;

2.5 解决局部故障的思路与方法

  • 开发可靠地远程过程调用代理:使用Netflix描述的方法,可以包括以下机制的组合;

    • 网络超时:在等待针对请求的响应时,不要做成无限阻塞,而是设定一个超时,用来保证不会一直在无响应的请求上浪费资源;
    • 限制客户端向服务器发出请求的数量:把客户端能够向特定服务发起的请求设置一个上限,如果请求达到上限,就让请求立刻失败;
    • 断路器模式:监控客户端发出请求的成功和失败数量,如果失败的比例超过一定的阈值,就启动断路器,让后续调用立即失败。如果大量请求都以失败告终,说明被调服务不可用。经过一定时间后,客户端继续尝试,如果调用成功,则移除断路器;
  • 从服务失效故障中恢复

    • 可以只是服务向其客户端返回错误;
    • 返回备用值(如默认值或缓存响应);

2.6 应用层服务发现模式

服务及其客户直接与服务注册表交互;

应用层服务发现模式工作原理

  • 服务实例使用服务注册表注册其网络位置。客户端首先通过查询服务注册表获取服务实例列表来调用服务,然后它向其中一个实例发送请求;
  • 这种服务发现是以下两种模式的组合:
    • 自注册模式:服务实例向服务注册表注册自己;

      • 可以提供运行状态检查URL(“心跳”功能,服务注册表定期调用该端点验证服务实例是否正常且可用于处理请求);
    • 客户端发现模式:客户端从服务注册表检索可用服务实例的列表,并在它们之间进行负载均衡;

      • 为了提高性能,客户端可能会缓存服务实例;
  • 业界有Netflix开发的Eureka组件,一个高可用的服务注册表;Pivotal开发的SpringCloud
    使相关组件使用非常简单;

2.7 平台层服务发现模式

通过部署基础设施来处理服务发现;

平台层服务发现模式工作原理

  • 部署平台包括一个服务注册表,用于跟踪已部署服务的IP地址;
  • 部署平台为每个服务提供DNS名称、虚拟IP(VIP)地址和解析为VIP地址的DNS名称;
  • 这种服务发现是以下两种模式的组合:
    • 第三方注册模式:由第三方负责(称为注册服务器)处理注册,而不是服务本身先服务注册表注册自己;
    • 服务端发现模式:客户端向DNS名称发出请求,对该DNS名称的请求被解析到路由器,路由器查询服务注册表并对请求进行负载均衡;
  • 业界有Docker与Kubernetes,都内置有服务注册表与服务发现机制;

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

使用消息机制时,服务之间的通信采用异步交换消息的方式完成。

基于消息机制的应用程序通常采用消息代理;另一种选择是使用无代理架构。

3.1 关于消息

消息由消息头部和消息主体组成;

  • 消息头部

    • 标题:名称与值对;
    • 消息ID:消息传递基础唯一ID;
    • 返回地址:指定发送回复的消息通道;
  • 消息主体:以文本或二进制格式发送的数据;

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

3.2 关于消息通道

消息通道工作原理
有以下两种类型的消息通道:

  • 点对点通道

    • 向正在从通道读取的一个消费者传递消息;
    • 如:命令式消息通常通过点对点通道发送;
  • 发布 – 订阅通道

    • 将一条消息发送给所有订阅的接收方;
    • 如:事件式消息通常通过发布 – 订阅通道发送;

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

介绍下面四种交互方式的消息机制:

  • 实现单向通知

    • 客户端将消息(通常是命令式消息)发送到服务所拥有的点对点通道;
    • 服务订阅该通道并处理该消息,但服务不会发回回复;
  • 实现发布/订阅

    • 客户端将消息发布到由多个接收方读取的发布/订阅通道;
    • 发布领域事件的服务拥有自己的发布/订阅通道,通道名称往往派生自领域类;
    • 如:Order Service将Order事件发布到Order通道;Delivery Service将Delivery事件发布到Delivery通道;
  • 实现发布/异步响应

    • 一种更高级的交互方式,将发布/订阅与请求/响应这两种方式的元素组合实现;
    • 客户端发布一条消息,在消息的头部中指定回复通道。这个通道同时也是一个发布 – 订阅通道;
    • 消费者将包含相关性ID的回复消息写入回复通道;
    • 客户端通过使用相关性ID来收集响应,以此将回复消息与请求进行匹配;
  • 实现请求/响应和异步请求/响应

    • 客户端发送请求,服务会发回回复;
    • 客户端必须告知服务发送回复消息的位置,并且必须将回复消息与请求匹配;
      • 即:客户端发送具有回复通道头部的命令式消息。服务器将回复消息写入回复通道,该回复消息包含与消息标识符具有相同的相关性ID。客户端使用相关性ID将回复消息与请求匹配;
    • 由于客户端和服务端使用消息机制进行通信,因此交互本质上是异步的;
    • 工作原理图如下:

实现请求/响应交互方式工作原理图

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

服务的异步API规范必须制定消息通道的名称、通过每个通道交换的消息类型及其格式。

服务的异步API

  • 服务的异步API包含供客户端调用的操作和由服务对外发布的事件;
  • (记录异步操作)可以使用以下两种不同交互方式之一调用服务的操作:
    • 请求/异步响应式API:包括服务端命令消息通道、服务接受的命令式消息的具体类型和格式,以及服务发送的回复消息的类型和格式;
    • 单向通知式API:包括服务的命令消息通道,以及服务接受的命令式消息的具体类型和格式;
  • (记录事件发布)服务还可以使用发布/订阅的方式对外发布事件;
    • 此API风格等规范包括事件通道以及服务发布到通道的事件式消息的类型和格式;

3.5 无代理消息的利弊

在无代理的架构中,服务可以直接交换信息。

好处

  • 允许更轻的网络流量和更低的延迟,因为没有中间代理过程;
  • 消除了消息代理可能成为性能瓶颈或单点故障的可能性;
  • 具有较低的操作复杂性,因为不需要设置和维护消息代理;

弊端

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

举例

  • ZeroMQ:一种流行的无代理消息技术;

无代理与基于代理的架构

3.6 基于代理消息的利弊

消息代理是所有消息的中介节点;发送方将消息写入消息代理,消息代理将消息发送给接收方。

好处

  • 松耦合;
  • 消息缓存:消息代理可以在消息被处理之前一直缓存消息;
  • 灵活的通信:消息代理支持前面提到的所有交互方式;
  • 明确的进程间通信

弊端

  • 潜在的性能瓶颈:解决方法 – 横向扩展;
  • 潜在的单点故障:解决办法 – 大多数现代消息代理是高可用的;
  • 额外的操作复杂性:消息系统必须是一个独立安装、配置和运维的系统组件;

举例

  • 流行的开源消息代理:Apache ActiveMQ(JMS)、RabbitMQ(AMQP)、Apache Kafka;
  • 基于云的消息服务:AWS Kinesis、AWS SQS;
  • 上述除了AWS SQS外都支持点对点和发布 – 订阅通道;AWS SQS只支持点对点通道;

每个消息代理都有自己的特色

3.7 选择消息代理需要考虑的因素

  • 支持的编程语言
  • 支持的信息标准
  • 消息排序:消息代理是否能够保留消息的排序;
  • 投递保证:消息代理提供怎样的消息投递保证;
  • 持久性
  • 耐久性:如果接收方重新连接到消息代理,它是否会收到断开连接时发送的消息;
  • 可扩展性
  • 延迟
  • 竞争性(并发)接收方:消息代理是否支持竞争性接收方;

3.8 处理并发和消息顺序

问题描述:在横向扩展多个消息接收方的实例的情况下,消息的顺序可能会错位。

解决方法:使用分片消息通道扩展接收方;

使用分片消息通道扩展接收方工作原理图
图解

  • 分片通道由两个或多个分片组成,每个分片的行为类似于一个通道;
  • 发送方在消息头部指定分片键,通常是任意字符串或字节序列。消息代理使用分片键将消息分配给特定的分片;
    • 如:通过计算分片键的散列来选择分片;
  • 消息代理将接收方的多个实例组合在一起,并将他们视为相同的逻辑接收方;
    • 如:Apache Kafka使用术语消费者组;消息代理将每个分片分配给单个接收器;它在接收方启动和关闭时重新分配分片;

3.9 处理重复消息

问题描述:客户端、网络或消息代理的故障可能导致消息被多次传递。

有以下两种解决办法:

  • 编写幂等消息处理器

    • 幂等操作特点:任意多次执行所产生的影响均与一次执行的影响相同;
  • 跟踪消息并丢弃重复消息

    • 将消息处理程序注册进应用程序表(NoSQL)【第七章介绍】;
    • 使用message id跟踪消息并丢弃重复消息,如下图:

使用message id跟踪消息并丢弃重复消息

3.10 事务性消息

  • 使用数据库表作为消息队列

    • 事务性发件箱:通过将事件或消息保存在数据库OUTBOX表中,将其作为数据库事务是一部分发布;
      使用数据库表作为消息队列
  • 通过轮询模式发布事件

    • 轮询发布数据:通过轮询数据库中的发件箱发布消息;
    • 小规模下运行良好,弊端在于经常轮询数据库会造成较大开销;
  • 使用事务日志拖尾模式发布事件

    • 事务日志拖尾:通过拖尾数据日志发布对数据库所做的修改;
    • 一些行业案例:Debezium、Linkedln Databus、DynamoDB streams、Eventuate Tram;
    • 下图解:每次应用程序提交到数据库的更新都对应着数据库事务日志中的一个条目;事务日志挖掘器可以读取事务日志,把每条跟消息有关的记录发送给消息代理;

事务日志拖尾模式

3.11 消息相关的类库和框架

服务需要使用库来发送和接收消息。

有两种方法:

  • 使用消息代理的客户端库,问题有:

    • 客户端库将发布消息的业务逻辑耦合到消息代理API;
    • 客户端库通常只提供发送和接收消息的基本机制,不支持更高级别的交互方式;
    • 消息代理的客户端库通常非常底层,需要多行代码才能发送/接收消息;
  • 使用更高级别的库或框架来隐藏底层细节,并直接支持更高级别的交互方式
  • 如Eventuate Tram框架;

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

采用同步通信机制处理请求,会对系统的可用性带来影响。因此,应尽可能选择异步通信机制来处理服务之间的调用。

4.1 同步消息会降低可用性

同步交互方式提交订单流程图

4.2 消除同步交互的方法

  • 使用异步交互模式

    • 下图解:客户的通过Order Service发送一个请求消息交换消息的方式创建订单;这个服务随即采用异步交换消息的方式跟其他服务通信完成订单的创建;
    • 缺点:很多情况下都要采用REST等同步通信协议API,不能替换为异步;
      异步交互方式提交订单流程图
  • 复制数据

    • 下图解:Consumer Service和Restaurant Service在它们的数据发生变化时对外发布事件;Order Service订阅这些事件,并据此更新自己的数据副本;
    • 缺点:当数据量巨大时效率低下;

复制数据提交订单流程图

  • 先返回响应,再完成处理

    • 下图解:Order Service创建一个未检验(Pending)状态的订单,然后通过异步交互方式直接跟其他服务通信来完成验证;
    • 缺点:使客户端更复杂。

先返回响应,再完成处理订单流程

5. 本章小结

  • 微服务架构是一种分布式架构,因此进程间通信起着关键作用;
  • 仔细管理服务API的演化至关重要。向后兼容的更改是最容易进行的,因为它们不会影响客户端。如果对服务的API进行重大更改,通常需要同时支持旧版本和新版本,直到客户端升级为止;
  • 有许多进程间通信技术,每种技术都有不同的利弊。一个关键的设计决策是选择同步远程过程调用模式或异步消息模式。基于同步远程过程调用的协议(如REST)是最容易使用的。但是,理想情况下,服务应使用异步消息进行通信,以提高可用性;
  • 为了防止故障通过系统层层蔓延,使用同步协议服务的客户端必须设计成能够处理局部故障,这些故障是在被调用的服务停机或表现出高延迟时发生的。特别是,它必须在发出请求时使用超时,限制未完成请求的数量,并使用断路器模式来避免调用失败的服务;
  • 使用同步协议的架构必须包含服务发现机制,以便客户端确定服务实例的网络位置。最简单的方法是使用部署平台实现的服务发现机制:服务器端发现和第三方注册模式。但另一种方法是在应用程序级别实现服务发现:客户的发现和自注册模式。它需要的工作量更大,但它确实可以处理服务在多个部署平台上运行的场景;
  • 设计基于消息的架构的一种好方法是使用消息和通道模型,它抽象底层消息系统的细节。然后,你可以将该设计映射到特定的消息基础结构,该基础结构通常基于消息代理;
  • 使用消息机制的一个关键挑战是以原子化的方式同时完成数据库更新和发布消息。一个好的解决方案是使用事务性发件箱模式,并首先将消息作为数据库事务的一部分写入数据库。然后,一个单独的进程使用轮询发布者模式或事务日志拖尾模式从数据库中检索信息,并将其发布给消息代理。


最后

新人制作,如有错误,欢迎指出,感激不尽!

欢迎关注公众号,会分享一些更日常的东西!

如需转载,请标注出处!

版权声明:本文为dlhjw原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/dlhjw/p/15139245.html