首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

微服务架构设计模式-进程间通信

微服务架构进程间通信概述

进程间的通信本质是交换消息

交互方式

第一个维度:一对一和一对多

  • 一对一:一个请求一个服务实例处理
  • 一对多:一个请求多个服务实例处理

第二个维度:同步和异步

  • 同步模式:客户端请求服务端实时响应,客户端等待响应过程中可能会阻塞线程
  • 异步模式:客户端请求不会阻塞线程,服务端响应可以不是实时

交互方式的两个维度具体类型

微服务架构中定义 API

API 优先设计

<font color="red">API 优先设计</font>:首先编写接口定义,然后与客户端开发人员一起查看这些接口定义。只有在反复迭代几轮 API 定义之后,才开始具体的服务实现编程。这种预先设计有助于你构建满足客户端需求的服务(即使在那些最简单的项目中,组件和 API 之间也经常发生冲突)。

API 的演化

API 随着需求变更会随之发生变化

语义化版本控制

它是一组规则,用于指定如何使用版本号,并且以正确的方式递增版本号

语义化版本控制规范由三部分组成:MAJOR.MINOR.PATCH

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

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

向后兼容的更改是对 API 的附加或增强

  • 添加可选请求属性
  • 向响应添加新属性
  • 添加新操作

理想情况下应努力向后兼容

健壮性原则:服务因改为缺少的请求属性提供默认值,客户端应忽略额外的响应属性

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

对 API 进行主要且不向后兼容的修改。无法要求客户端升级,新旧 API 在一段时间内需要同时支持

REST API :可以使用主要版本作为 URL 路径的第一个元素。比如 /v1/xxx

消息机制:可以在发布的消息中包含版本号

消息格式

基于文本的格式:JSON 或 XML

​ 好处:可读性高,同时也是自描述的。这样的格式允许消息的接收方只挑选他们感兴趣的值,而忽略掉其他。因此,对消息结构的修改可以做到很好的后向兼容性。

​ 弊端:消息往往过度冗长,特别是 XML。消息较大时解析文本会引入额外开销。在对效率和性能敏感的场景下需要慎重。

二进制消息格式:Protocol Buffers 或 Avro

​ 好处:传递二进制数据,性能好/效率高。

​ 弊端:开发稍嫌复杂,可读性不高

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

模式:远程过程调用

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

使用 REST

好处

  • 它非常简单,并且大家都很熟悉
  • 可以很方便的用 Postman 之类的工具来测试
  • 直接支持请求/响应方式的通信
  • HTTP 对防火墙友好
  • 不需要中间代理,简化了系统架构

弊端

  • 它只支持请求/响应方式的通信
  • 可能导致可用性降低
  • 客户端必须知道服务实例的位置(URL)
  • 在单个请求中获取多个资源具有挑战性
  • 有时很难将多个更新操作映射到 HTTP 动词

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

分布式系统中,当服务向另一个服务发送同步请求,永远存在局部故障的可能。如:服务端过载过高响应缓慢、服务端打包维护等导致无法正常提供服务。 这种局部故障如何解决?

模式:断路器模式(如:netflix hystrix 组件)

这事一个远程过程调用代理(如:hystrix 远程调用使用 HystrixCommon 模式),在连续失败次数超过指定阀值后的一段时间内,这个代理会立即拒绝其他调用(即:超过阀值后在指定时间内的调用请求会被直接拒绝)

如上图:API Gateway 必须保护自己免受无响应服务的影响。如:途中 Order Service

雪崩/故障传播:某个服务因无法提供正常响应,不断导致客户端越来越多调用被阻塞进而客户端也无法响应它的调用者,使故障不断扩展和传播,最后导致整个系统瘫痪。

要通过合理地设计来防止在整个应用程序中故障的传导和扩散,这是至关重要的。解决这个问题可分为两部分:

  1. 必须让远程过程调用代理(如:SpringClod 里远程调用代理 hystrix)有正确处理无响应服务的能力(不断调用失败的问题需要调用代理解决,如:可拒绝下次调用)
  2. CAP 中的 P 分区容错性
  3. 需要解决如何从失败的远程过程服务中恢复(调用失败后,失败结果如何处理,如:抛异常或返回默认值)

开发可靠的远程过程调用代理

远程调用代理需要有一种自我保护机制,来保证调用在失败情况下能正确被处理

Netflix 保护机制组合:

  • 网络超时(调用超时机制)
  • 设置网络超时时间,可避免调用无限阻塞。使用超时可保证不会一直在无响应的请求上浪费资源,同时避免服务崩溃
  • 限制客户端向服务端发送请求的数量(限流)
  • 客户端向特定服务端请求设置一个上限,若达到请求上限,则后面来的请求对于服务端可能无法处理甚至会造成服务端出现问题,此时可让该请求立刻失败(没有触发调用操作)
  • 断路器模式
  • 监控客户端发出请求的成功和失败数量,若失败的比例超过一定阀值,就启动断路器,后续调用立刻失败(此时可能服务不可用)。在经过一段时间后,客户端应该尝试调用,若调用成功则解除断路器(服务经过一段时间可能变得可用)。

从服务时效故障中恢复

远程调用失败后,这个调用的结果如何处理?

  1. 直接向客户端返回错误
  2. 返回备用值(fallback value,如:默认值或缓存响应)调用失败降级

如上图:API Gateway 调用多个服务并聚合结果返回(适合选择返回备用值)

考虑局部故障至关重要:每个服务的数据对于客户端的重要性不一样,甚至可有可无,不重要数据服务调用失败依然可以正常响应客户端,该部分数据可省略或给个默认值,若重要数据如订单查询失败,则可返回客户端失败。

服务注册发现

为什么需要服务发现?

什么是服务发现

包含服务实例网络位置信息的一个数据库,其关键组件是服务注册表

云服务时代,无法使用静态 IP 配置客户端,应用程序网络地址必须能够动态被其他服务所发现。服务启动或停止,服务发现机制会更新服务注册表,当客户端调用时,服务发现机制查询服务注册表以获取可用的服务实例列表,并将请求路由到其中一个服务

实现服务发现有以下两种主要方式:

  • 服务及其客户直接与服务注册表交互
  • 通过部署基础设施来处理服务发现

应用层服务发现模式

服务端和客户端向服务注册表进行交互。服务端向注册表注册其网络位置,客户端查询服务注册表实例列表,并向其中一个实例发送请求。

这种服务发现模式是两种模式的组合

  1. 模式:自注册
  2. 服务实例向注册表注册自己
  • eureka(客户端 ribbon,远程调用代理)
  • nacos
  • discovery
  1. 模式:客户端发现
  2. 客户端从注册表检索可用的服务实例列表,并在它们之间进行负载均衡

好处:可以处理多平台部署问题(如:分别在 kubernetes 平台及 serverless 平台部署了,它们可以只同一个 eureka 注册中心交互)

弊端:

  • 每种编程语言都需要提供服务发现库
  • 开发者负责设置和管理服务注册表,这会分散一定的精力

平台层服务发现模式

现代部署平台(如 Docker 和 Kubernetes)都具有内置的服务注册表和服务发现机制。部署平台为每个服务提供 DNS 名称、虚拟 IP(VIP)地址和解析为 VIP 地址的 DNS 名称。客户端向 DNS 名称和 VIP 发出请求,部署平台自动将请求路由到其中一个可用实例。服务注册、服务发现和请求路由完全由部署平台处理。

如上图:该平台负责服务注册表、用于跟踪已部署服务的 IP 地址。客户端使用 DNS 名称 order-service 访问 order-service,该服务解析为虚拟 IP 地址 10.1.3.4,该平台会自动在三个实例之间进行负载均衡。

平台层服务发现模式由两种模式组合:

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

好处:服务发现、服务注册等所有方面完全由部署平台处理,服务和客户端都不需要包含任何服务发现代码。不论何种框架和开发语言,服务发现机制可供所有服务和客户使用。

弊端:它仅限于支持使用该平台部署的服务,如部署在 kubernetes 和 docker 上的服务是不能互通的。

服务发现机制建议:尽可能使用平台提供的服务发现。

基于异步消息的通信

Gregor Hohpe 和 Bobby Woolf 在 《Enterprise Integration Patterns》一书中定义了一种有用的消息传递模型。在此模型中,消息通过消息通道进行交换。发送方(应用程序或服务)将消息写入通道,接收方(应用程序或服务)从通道读取消息。

模式:消息。客户端使用异步消息调用服务。

关于消息

消息由消息头和消息主题组成,消息头应包含发送者提供的名称与值、唯一消息ID以及可选的返回地址

消息类型

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

命令:一条等同于 RPC 请求的消息,它指定要调用的操作及其参数

文档:只包含数据,接收方决定如何解释它,一般是对命令的回复

消息通道

两种类型的消息通道

点对点通道:向正从通道读取消息的某个消费者传递消息。服务使用点对对通道实现一对一交互

:命令式消息通常通过点对点消息通道发送

发布订阅通道:通道将一条消息发送给所有的订阅方。服务使用发布订阅通道实现一对多交互

:事件式消息通过发布订阅通道发送

消息代理

无代理消息

无代理架构中,服务可以直接交换消息。如:ZERO、EMQ(待定)

好处

  • 允许更轻的网络流量和更低的延迟,因为消息直接由发送方发送到接收方,无需经过代理中转
  • 消除了代理可能成为性能瓶颈或单点故障的可能性(如 kafka 需要做集群)
  • 具有较低的操作复杂性,无需设置和维护消息代理

弊端

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

无消息代理和同步请求响应交互方式的弊端相同

有代理消息

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

好处

  • 发送方无需知道接收方的网路位置
  • 消息代理可以缓冲消息,直到接收方能够处理他们
  • 松耦合
  • 灵活的通信:支持所有的交互方式,如:一对一、一对多、异步、同步
  • 明确的进程间通信:消息机制和远程调用两种通信方式明显不同

弊端

  • 潜在的性能瓶颈:消息代理可能存在性能瓶颈,许多消息代理支持横向扩展
  • 潜在的单点故障:消息代理的可用性极为重要
  • 额外的操作复杂性:消息代理的安装、运维和配置

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

  • 支持的编程语言
  • 支持的消息标准(消息协议)
  • 消息代理是否支持多种消息标准,如:AMQP、STOMP、MQTT、WS 等,还是它只支持专用标准
  • 消息排序:消息代理是否能保证消息顺序
  • 投递保证:消息代理能提供什么样的消息投递保证,如:至少一次、最多一次、只有一次
  • 持久性:消息是否可以持久化保存到磁盘并且能够在代理崩溃时恢复
  • 耐久性:如果消息接收方重新连接到消息代理,它能否接收到断开链接时发送的消息?
  • 可扩展性:消息代理的扩展性如何
  • 延迟:端到端是否有较大延迟
  • 竞争性(并发)接收方:消息代理是否支持并发性接收方

消息代理按需求选择最合适的。一个延迟非常低的打击可能不会保证消息顺序,不保证消息投递成功,只在内存保存消息。保证投递成功并在次盘持久化存储消息可能具有更高的延迟。不同的需求可采用不同的消息传递方式

开源消息代理Kafka、Pulsar、RocketMQ、RabbitMQ、ActiveMQ

基于云的消息服务AWS Kinesis、AWS SQS、阿里云 RocketMQ

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

<center>每个消息代理都用自己与众不同的 概念来实现消息通道</center>

处理并发和消息顺序

并发指的是单个消息通道多个接收方

保证消息顺序的同时,横向扩展多个接收方实例。同一个消息通道,有多个接收方(如:kafka 一个主题有多个消费实例)

现代消息代理解决并发和消息顺序方案为分片(分区)通道,具体步骤:

  1. 分片通道由两个或多个分片组成,每个分片的功能类似一个通道(如:kafka每个分区就是一个通道
  2. 发送方在消息头部指定分片键(key),通常是任意字符串或字节序列。消息代理根据 key 将消息分配给对应的分片(分区),可以计算 key 的散列来选择分片,如:订单消息根据订单类型 orderType(百货、母婴、酒水等)作为 key,不同的 key 发送的不同分区。
  3. 消息代理将接收方的多个实例组合成一个分组,并将它们视为同一个逻辑接收方(如:同一个微服务的多个实例),例如 kafka 消费者分组。消息代理将每个分片分配给消费者组中的某一个接收方处理,它在接收方启动或关闭时会重新分片。

例:kafka 的 topic 为一个消息通道,分区为分片通道,每个分片通道分配给 kafka 的消费组某一个消费者处理,生产者可以指定分区的 key,kafka同一个分区的消息有序总结处理并发和消息顺序:同一个消息通道(topic)多个消费实例,消息通道(topic)拆分成分片通道(分区,多个分区),每个分片通道分配给多个实例中的某一个,同一个分片通道内消息是有序的。

处理重复消息

重复消息处理办法

  1. 编写幂等消息处理程序
  2. 幂等性:应用程序被相同输入参数多次重复调用时,不会产生不同的效果。

2. 跟踪消息并丢弃重复项

  1. 跟踪消息:给每条消息一个消息 ID,程序记录入库已处理过消息的消息 ID

如图:消息 ID 被单独存在一张表,业务表的更新和消息 ID 的插入在同一个事务中,若消息重复,在整个事务失败。NoSQL 没有多表同时更新的事务,可将消息 ID 放在应用程序的数据表中

事务性消息

如何保证程序在更新数据库和发送消息是一个原子操作,如果不是原子操作,则结果会产生不一致或系统故障

使用数据库作为消息队列事务性发件箱模式:利用数据库作为临时消息队列,将业务创建、更新、删除等操作和消息写入数据库放在同一个事务中,利用本地事务,保证消息一致。

如图:outbox 充当临时队列,使用一个 MessageReplay 读取 outbox 表数据并发送给消息代理。

使用轮询模式发布事件

MessageReplay 相当于轮询器,不断查询 outbox 表数据并发送给消息代理

模式:轮询发布数据

通过轮询数据库中的发件箱来发布消息

弊端:不断轮询数据库会导致服务和数据库开销变大

使用事务日志拖尾模式发布事件

MessageReplay 监听数据库 outbox 表事务提交日志,然后将对应数据发送到消息代理

模式:事务日志拖尾

通过拖尾事务日志发布对数据库所做的更改

开源事务日志拖尾工具

  • 阿里 canel
  • Debezium
  • Linkedin Databus
  • Eventuate Tram
  • DynamoDB streams

使用异步消息提高可用性

同步调用会降低应用程序的可用性,CAP 理论中可用性一般必须保证,异步可提升可用性,使服务之间松耦合。

减少同步通信方法:1. 使用异步交互模式 2. 数据复制 3.先返回响应,再完成处理

使用异步交互模式

使用请求/异步响应方式交互,如:请求后接收方发送一个消息给请求方

复制数据(数据分发)

数据复制指的是服务维护一个数据的副本,从而减少对其他服务的同步调用。

KT 通过数据分发,使得各个服务可自定义接收分发的数据

弊端

  • 复制的数据量巨大,会导致效率低下
  • 复制数据没有从根本上解决其他服务所拥有数据这个问题(数据复制不能更新其他服务数据)

先返回响应,再完成处理

具体步骤如下:

  1. 利用本地数据完成请求验证(没有远程调用,直接服务内部处理后返回)
  2. 更新数据库,包括向 OUTBOX 表插入消息
  3. 向客户端返回响应

总结

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/8eb1cdd743467d0bd3159180f
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券