与我一起学习微服务架构设计模式3—微服务架构中的进程间通信

选择合适的进程间通信机制是一个重要的架构决策,它会影响应用的可用性,甚至与事务管理相互影响。

概述

交互方式

首先考虑交互方式有助于你专注于需求,避免陷入细节。


一对一一对多
同步模式请求/响应
异步模式异步请求/响应 单向通知发布/订阅 发布/异步响应

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

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

单向通知: 客户端的请求发送到服务端,并不期望服务端做出任何响应

发布/订阅方式: 客户端发布通知消息,被零个或多个感兴趣的服务订阅

发布/异步响应方式: 客户端发布请求消息,等待从感兴趣的服务发回的响应

在微服务中定义API

服务的API是服务与其客户端之间的契约,它由客户端结构可以调用的方法、服务发布的事件组成。

挑战:

没有一个简单的编程语言结构来构造和定义服务的API。若使用不兼容的API部署新版本的服务,虽然在编译阶段不会出现错误,但会出现运行时故障。

首先编写接口定义,然后与客户端开发人员一起查看这些接口的定义。只有在反复迭代几轮API定义后,才可以具体服务实现编程。这种预先设计有助于你构建满足客户端需求的服务。

API的演化

挑战: 

1、不能够强行要求客户端和服务端API版本保持一致

2、你一般采用滚动升级的方式更新服务,因此一个服务的旧版本和新版本肯定会共存

语义化版本控制

它是一组规则,用于指定如何使用版本号,并且以正确的方式递增版本号,版本号由三部分组成,必须按如下方式递增版本号:

  • MAJOR:当你对API进行不兼容的更改时

  • MINOR:当你对API进行向后兼容的增强时

  • PATCH:当你进行向后兼容的错误修复时

你可以在实现REST API或消息机制的服务时,包含版本号

进行次要且向后兼容的改变

理性情况下应该只进行向后兼容的更改:

  • 添加可选属性

  • 向响应添加属性

  • 添加新操作

服务应该为缺少的请求属性提供默认值,客户端应忽略任何额外的响应属性,这样老版本的客户端能直接只用更新的服务

进行主要且不向后兼容的改变

此时必须在一段时间内同时支持新旧版本的API

假如使用REST,可以在URL中嵌入主要版本号,或者使用HTTP的内容协商机制,在MIME类型中包含版本号。

实现API的服务适配器将包含在旧版本与新版本之间进行转换的逻辑,如API Gateway几乎会使用版本化的API

消息的格式

考虑到以后会扩展到其他语言,我们不应该使用类似java序列化这样跟语言强相关的消息格式

基于文本的消息格式

如JSON和XML,可读性高,自描述的。但往往过度冗长,解析开销过大。

二进制消息格式

对效率和性能敏感的场景下,比较适用。常见的如Protocol Buffers和Avro,它们提供了强类型定义的IDL,编译器会自动根据其格式生成序列化和反序列化的代码,因此你不得不采用API优先的方法来进行服务设计。

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

客户端的业务逻辑调用由RPI代理适配器类实现的接口,RPI代理类向服务发出请求,RPI服务器适配器类通过调用服务的业务逻辑来处理请求

使用REST

REST是使用HTTP协议的进程间通信机制

其关键概念是资源,它通常表示单个业务对象。REST使用HTTP动词操作资源,使用URL引用这些资源。

REST成熟度模型

  • LEVEL 0:只是向服务端点发起HTTP POST请求,进行服务调用

  • LEVEL 1:引入了资源的概念

  • LEVEL 2:使用HTTP动词执行操作

  • LEVEL 3:基于HATEOAS原则设计,基本思想是由GET请求返回的资源信息中包含链接,这些链接能够执行该资源允许的操作

最流行的REST IDL是Open API的规范,他是从Swagger开源项目发展而来的。

一个请求中获取多个资源的挑战

REST资源通常以业务对象为导向,设计REST API时常见问题是如何使客户端能够在单个请求中检索多个相关对象。纯REST API要求客户端发出多个请求,更复杂的情况时需要更多往返并遭受过多延迟,其中一个解决方案是API允许客户端在获取资源时检索相关其他资源,如果情况更复杂耗时,则使用GraphQL和Falcor等替代技术。

把操作映射为HTTP动词的挑战

如何将在业务对象上执行的操作映射到HTTP动词。但很难将多个更新操作映射到HTTP动词,且更新可能不是幂等的,但这却是使用PUT的要求。

一种解决方案是定义用于更新资源的特定方面的子资源,还有就是将动词指定为URL的查询参数。但这不是很符合RESTful的要求。

REST的好处和弊端

好处:

  • 简单熟悉

  • 可使用浏览器扩展或curl来测试API

  • 直接支持请求/响应方式通信

  • HTTP对防火墙友好

  • 不需要中间代理,简化系统架构

弊端:

  • 只支持请求/响应方式通信

  • 没有代理缓冲消息,可能导致可用性降低

  • 客户端必须知道服务实例的位置

  • 在单个请求中获取多个资源具有挑战性

  • 有时很难将多个更新操作映射到HTTP动词

使用gRPC

由于HTTP仅提供有限数量的动词,设计支持多个更新操作的REST API不总是很容易,gRPC可以避免此问题。它是一种跨语言客户端和服务端的框架,基于二进制消息,你可以基于Protocol Buffer的IDL定义gRPC API,能够保持在向后兼容的同时进行变更。

gRPC除简单的请求/响应RPC外,还支持流式RPC。

好处:

  • 便于设计具有复杂更新操作的API

  • 具有高效紧凑的进程间通信机制,尤其在交换大量信息时

  • 支持双向流式消息方式

  • 实现了客户端和用各种语言编写的服务端间的互操作性

弊端:

  • 需要更多工作

  • 旧式防火墙也许不支持HTTP/2

  • 也是一种同步通信机制,存在局部故障的问题

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

服务端可能因为故障等无法在有限时间内对客户端请求做出响应,客户端等待响应被阻塞,这可能会在其他客户端甚至使用服务的第三方应用之间传导,导致服务中断。

解决方案:

1、开发可靠的远程过程调用代理,包括:

  • 网络超时机制

  • 限制客户端向服务器发出的请求数量

  • 断路器模式:在连续失败次数超过指定阀值后一段时间内,这个代理会立即拒绝其他调用,稍后重试,若成功则解除断路器

2、从服务失效故障中恢复

  • 服务只是向其客户端返回错误

  • 返回备用值

使用服务发现

服务实例具有动态分配的网络位置,由于自动扩展、故障和升级,服务实例会动态更改,因此客户端代码必须使用服务发现

什么是服务发现 

服务发现的关键组件是服务注册表

两种方式实现服务发现:

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

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

应用层服务发现模式 

它是两种模式的组合

  • 自注册模式:服务实例向服务注册表注册自己

  • 客户端发现模式:客户端从服务注册表检索可用服务实例列表,并在它们之间进行负载均衡

例子:如Euraka,高可用的服务注册表;Euraka java客户端;Ribbon,支持Eureka客户端的复杂Http客户端

好处:可以处理多平台部署的问题

弊端:需要为使用的每种编程语言提供服务发现库。开发者需要负责设置、管理服务注册表,分散一定精力。

平台层服务发现模式

它是两种模式的组合:

  • 第三方注册模式:由第三方负责处理注册,而不是服务本身向服务注册表注册自己

  • 服务端发现模式:客户端不需要查询服务注册表,而是向DNS名称发出请求,请求被解析到路由器,路由器查询服务注册表对请求进行负载均衡。

例子:Docker和Kubernetes

好处:服务发现的所有方面完全由部署平台处理

弊端:仅限于支持使用该平台部署的服务

基于异步消息模式的通信

客户端使用异步消息调用服务

消息传递

消息由消息头部和消息主体组成 类型:

  • 文档 仅包含数据的通用消息

  • 命令 一条等同于RPC请求的消息

  • 事件 表示发送方这一端发生了重要事件

关于消息通道

发送方中的业务逻辑调用发送端接口,该接口由消息发送方适配器实现。消息发送方通过消息通道向接收方发送消息。消息通道是消息传递基础设施的抽象。调用接收方的消息处理程序适配器来处理消息。它调用接收方业务逻辑实现的接收端端口。

类型:

  • 点对点通道:向正在从通道读取的一个消费者传递消息

  • 发布-订阅通道:将一条消息发给所有订阅的接收方

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

足够灵活,支持上面描述的所有交互方式

实现请求/响应和异步请求/响应

消息机制本质上是异步的,因此只提供异步请求/响应,但客户端可能会阻塞,直到收到回复。

通过在请求消息中包含回复通道和消息标识符来实现异步请求/响应。接收方处理消息将回复发送到指定的回复通道,回复消息包含与消息标志符具有相同值的相关性ID,用以匹配验证。

实现单向通知

实现发布/订阅

客户端将消息发布到由多个接收方读取的发布/订阅通道,对特定领域对象的事件感兴趣的服务只需订阅相应的通道。

实现发布/异步响应

它把发布/订阅和请求/响应两种方式的元素组合在一起

客户端发布一条消息,在头部指定回复通道,该通道也是发布-订阅通道。消费者将包含相关性ID的回复消息写入回复通道,客户端通过相关性ID来收集响应

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

不像REST,没有广泛采用的标准来记录通道和类型,需要自己定义。服务的异步API一般由消息通道和命令、回复和事件消息类型组成

记录异步操作

  • 请求/异步响应式API

  • 单向通知式API

记录事件发布 

服务可使用发布/订阅的方式对外发布事件

使用消息代理

无代理消息

无代理架构中,服务可以直接交换消息,如ZeroMQ

好处:

  • 允许更轻的网络流量和更低的延迟

  • 消除了消息代理可能会成为性能瓶颈或单点故障的可能性

  • 具有较低的操作复杂性

弊端:

  • 服务需要了解彼此的位置

  • 导致可用性降低,发送方和接收方必须同时在线

  • 实现例如确保消息能够成功投递这些复杂功能时挑战性更大

基于代理的消息

如ActiveMQ,Kafka 好处:

  • 发送方不需要知道接收方的网络位置

  • 消息代理缓冲消息,直到接收方能够处理它们

选择消息代理考虑因素:

  • 支持的编程语言

  • 支持的消息标准

  • 消息排序

  • 投递保证

  • 持久性:保存到磁盘且能在代理崩溃时恢复

  • 耐久性:若接收方重新连接到消息代理,是否会收到断开连接时发送的消息

  • 可扩展性

  • 延迟

  • 竞争性接收方:在多线程多实例同时处理消息的情况下,确保消息仅被处理一次,且按照应有的顺序来处理

使用消息代理实现消息通道:

每个消息代理都用自己与众不同的概念来实现消息通道,如Kafka使用主题实现点对点通道和发布-订阅通道,RabbitMQ使用交换+队列实现点对点通道,使用组播式交换和每客户端队列实现发布-订阅通道

好处:

  • 松耦合:客户端不需要感知服务实例的位置

  • 消息缓存:发送方和接受方不要求一定同时在线

  • 灵活的通信:支持前面所述的所有交互方式

  • 明确的进程间通信:与RPC相比,程序员不会陷入类似“本地调用”的那种“太平盛世”的感觉

弊端:

  • 潜在的性能瓶颈,不过可以横向扩展

  • 潜在的单点故障,不过现代消息代理大部分是高可用的

  • 额外的操作复杂性

处理并发和消息顺序

如何在保留消息顺序的同时,横向扩展多个接收方的实例

采用分片通道方案,如将orderId作为分片键,特定订单的每个事件都发布到同一个分片,该消息也由同一个接收方实例读取

1、分片通道由两个或多个分片组成,分片的行为类似于通道 

2、发送方在消息头部指定分片键,消息代理使用分片键将消息分配给分片 

3、消息代理将接收方的多个实例组合在一起。并将它们视为相同的逻辑接收方,如kafka中的消费者组。消息代理将每个分片分配给单个接收器。

处理重复消息

正常情况下,保证传递的消息代理只会传递一次消息。但故障可能导致消息被多次传递。

两种方法处理重复消息: 

编写幂等消息处理器:

幂等指这个应用被相同输入参数多次重复调用时,也不会产生额外的效果,但要保证消息代理在重新传递消息时保持相同顺序。

跟踪消息并丢弃重复消息:

简单的解决方案是消息接收方使用message id跟踪它已处理的消息并丢弃任何重复项

事务性消息

数据库更新和消息发送都必须在事务中进行,否则系统可能处于不一致状态。

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

通过事务性发件箱模式,即将事件或消息保存在数据库的OUTBOX表中,将其作为数据库事务的一部分发布。

将消息从数据库移动到消息代理的两种方法:

通过轮询模式发布事件 轮询数据库中的发件箱,将消息发送给消息代理,它在小规模下运行良好,但经常轮询数据库可能会导致数据库性能下降

使用事务日志拖尾模式发布事件 应用提交到数据库的更新对应着数据库事务日志中的一个条目。事务日志挖掘器可以读取事务日志,将跟消息有关的记录发送给消息代理。

其挑战在于需要一些开发努力,现有框架有Debezium,Eventuate Tram等。

消息相关的类库和框架

直接使用消息代理客户端库的弊端:

  • 客户端库将发布消息的业务逻辑耦合到消息代理API

  • 客户端库是非常底层的,需要常编写重复类似的代码

  • 不支持更高级别的交互

更好的方法是使用更高级别的库或框架,如Eventuate Tram

使用异步消息提高可用性

同步消息会降低可用性

如REST,当服务必须从另一个服务获取信息后才能返回它客户端的调用,就会导致可用性问题。每增加一个额外的服务,会更进一步降低可用性。

要最大化一个系统的可用性,就应该最小化系统的同步操作量

消除同步交互

方法:

使用异步交互模式:

客户端和服务端使用消息通道发送消息来实现异步通信。这种架构很有弹性,消息代理会一直缓存消息,直到服务端接收并处理消息。但服务很多情况采用同步通信协议的外部API,需要对请求立即作出响应。

复制数据: 

服务维护一个数据副本,这些数据是服务在处理请求时需要使用的,数据的源头会在数据发生变化时发出消息,服务订阅这些消息来确保数据副本的实时更新。

弊端:

  • 数据量巨大时效率低下

  • 没有从根本上解决服务如何更新其他服务所拥有的数据这个问题

先响应,后处理 

如Order Service,它在不调用任何其他服务的情况下创建订单,然后通过与其他服务交换信息来异步验证新创建的Order

优点:即使其他服务中断, Order Service仍然会创建订单响应客户

弊端:为了使客户端知道订单是否已成功创建,需要定期轮询或者向客户端发送通知。

java达人

ID:drjava

(长按或扫码识别)

感谢你的阅读,下面是一个抽奖链接按钮,10月20日晚上开奖,共5个红包,写文不易,感谢大家支持。

感谢大家一直以来的阅读、在看和转发,点我参与抽奖!点我参与抽奖!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值