微服务架构设计模式

 第一章 逃离单体地狱

1.1 单体架构的好处

开始阶段,应用程序相对较小的背景下,单体架构拥有以下好处:

  • 应用的开发相对简单
  • 易于对应用程序做大规模的修改
  • 测试相对简单
  • 部署简单明了
  • 横向扩展简单 

1.2 什么是单体地狱?

 随着时间的推移,当程序不断的向巨无霸式单体应用程序演进的时候,就会暴露出很多问题。

  • 过度的复杂性会吓退开发者。系统过于复杂与庞大,以至于开发者很难理解它的全部。
  • 开发速度缓慢。编辑、构建、部署、自测流程时间拉长。
  • 从代码提交到实际部署的周期很长,而且容易出问题。同一代码库,拉取合并分支的方式。
  • 难以扩展。应用的不同模块对资源的需求是有冲突的。有些需要大内存,而有些需要多CPU。
  • 交付可靠的单体应用是一项挑战。缺乏故障隔离,一个模块的问题可能导致整个程序不可用。如内存泄露
  • 需要长期依赖某个可能过时的技术栈。团队在单体地狱中采用一套相同的技术方案,尝试采用新技术都是高风险和高代价。

1.3 拯救之道:微服务架构

软件架构其实对功能性需求影响不大,架构的重要性在于它影响了应用的非功能性需求,也称为质量需求。

现在针对大型复杂应用的开发,越来越多的共识趋向于考虑使用微服务架构。尝试考虑下怎么定义微服务的概念?

1.3.1 扩展立方体和服务

扩展立方体模型描述了扩展一个应用程序的三种维度:X、Y和Z。

X轴扩展:在多个实例之间实现请求的负载均衡。

Z轴扩展:根据请求的属性路由请求。 也许运行单体程序的多个实例,但不同于X轴扩展,每个实例仅负责数据的一个子集。

Y轴扩展:根据功能把应用拆分为服务。X轴、Z轴扩展有效的提升了应用的吞吐量和可用性,但是这两种方式都没有解决应用复杂性。

Y轴扩展把一个单体应用分成了一组服务。 服务本质上是一个麻雀虽小五脏俱全的应用程序,它实现了一组相关的功能。服务可以在需要的时候借助X轴、Z轴进行扩展。

 

对微服务架构的概括性定义:把应用程序功能性分解为一组服务的架构风格。每一个服务都是一组专注的、内聚的功能职责组成。

1.3.2 微服务架构作为模块化的一种形式

 模块化是开发大型、复杂应用程序的基础。微服务架构使用服务作为模块化的单元,服务的API为它自身构筑了一个不可逾越的边界。

1.3.3 每个服务都有自己的数据库

 微服务架构的一个关键特性是每一个服务之间都是松耦合的,他们仅通过API进行通信。

 

1.4 微服务架构好处与弊端

1.4.1 微服务架构的好处

  • 使大型的复杂的应用程序可以持续交付与部署。拥有持续交付和持续部署所需要的可测试性、可部署性、使开发团队能够自主且松耦合。

  • 每个服务都相对较小并容易维护

  • 服务可以独立部署

  • 服务可以独立扩展

  • 微服务架构可以实现团队的自治

  • 更容易实验和采纳新的技术

  • 更好的容错性。更好的实现故障隔离。

1.4.2 微服务架构的弊端

  •  服务的拆分与定义是一项挑战。

  • 分布式系统带来的各种复杂性,使开发、测试和部署变得更加困难。

  • 当部署跨越多个服务的功能是需要谨慎的协调更多开发团队。

  • 开发者需要思考到底应该在应用的什么阶段开始使用微服务架构。

1.5 微服务架构的模式语言

架构设计的核心是 决策。一个用来表述多种架构设计的选择方案,并且可以用来改进决策的方式,就是使用模式语言。

1.5.1 模式与模式语言

模式 是针对特定上下文中发生的问题的可重用解决方案。

常用的模式结构包括三个重要部分:

a.需求:必须解决的问题

需求部分描述了必须解决的问题和围绕这个问题的特定上下文环境。

b.结果上下文:采用模式后可能带来的后果

也包含三个部分:

好处、弊端、采用模式引入的新的问题。

c.相关模式:5种不同类型的关系

相关模式部分描述了这个模式和其他模式之间的关系。模式之间存在5种关系:

  • 前导:前导模式是催生这个模式的需求的模式。例如微服务架构的模式是除单体架构模式以外整个模式语言中所有模式的前导模式。
  • 后续:后续模式是指用来解决当前模式引入的新问题的模式。例如当采用微服务架构模式,需要一系列后续模式来解决诸如服务发现、断路器等微服务带来的新问题。
  • 替代:当前模式的替代模式。如单体架构模式与微服务架构模式互为替代。
  • 泛化:针对一个问题的一般性解决方案。
  • 特化:针对特定模式的具体解决方案。

 

 第二章 服务的拆分策略

2.1 为应用程序定义微服务架构

定义应用程序架构三步式流程。

第一步,定义系统操作。

将应用程序的需求提炼为各种关键请求。不是根据特定的进程间通信技术来描述请求,而是使用更抽象的系统操作这个概念。 系统操作是应用程序必须处理的请求的一种抽象描述。

第二步,定义服务。

如何分解服务?围绕业务概念而非技术概念分解和设计的服务。

第三步,定义服务API和协作方式。

将第一步中标识的每个系统操作分配给服务。如果服务之间需要协作,还要确定服务的协作方式。

 

2.1.1 识别系统操作

 起点是应用程序的需求,包含用户故事及其相关的用户场景。

两步式流程识别和定义系统操作:

第一步创建由关键类组成的抽象领域模型。

第二步确定系统操作,并根据领域模型描述每个系统操作的行为。

领域模型主要来源用户故事中的名词,系统操作主要来自用户故事中的动词。

 

2.1.2 服务拆分

2.1.2.1 根据业务能力进行服务拆分

业务能力定义了一个组织的工作。 识别业务能力,从业务能力映射到服务。

2.1.2.2 根据子域进行服务拆分

领域驱动为每一个子域单独定义的领域模型。子域是领域的一部分。领域模型的边界称为限界上下文。可以通过DDD的方式定义子域,把子域对应为每一个服务。

2.1.2.3  拆分的指导原则

 单一职责原则:改变一个类应该只有一个原因。

 闭包原则:在包中包含的所有类应该是对同类的变化的一个集合。也就是说,如果要对包做出修改,需要调整的类应该都在这个包中。

2.1.2.4 拆分单体应用为服务的难点

  •  网络延迟:服务间的调用带来了网络传输开销。
  • 同步进程间通信导致可用性降低
  • 在服务间保持数据一致性 :引入分布式事务
  • 获取一致的数据视图:无法跨越多个数据库获取真正一致的数据视图,此问题也很少遇到
  • 上帝类阻碍了拆分:上帝类是在整个应用程序中使用的全局类。几种拆分思路:1.将上帝类打包到入库并创建中央数据库,并且所有服务都使用此库并使用中央数据库。 违背了微服务的思想。2. 将上帝类单独封装为一个服务,但此服务没有任何业务逻辑,仅提供数据操作接口,一个纯粹的数据服务,成为贫血领域模型。 3 DDD的思想,将上帝类拆分到各个服务的领域模型的子域中去。

2.1.3 定义服务API 

目前,已经有了一个系统操作列表和一个潜在服务列表。下一步是定义每个服务的API:也就是服务的操作和事件。 存在服务APi操作有以下两个原因:首先,某些操作对应系统操作。其次,存在一些其他操作用于支持服务之间的协作。

服务通过对外发布事件,使其能够与其他服务协作。

定义服务API的起点是将每个系统操作映射到服务,之后确定服务是否需要与其他服务协作以实现系统操作。如需协作,我们将确定其他服务必须提供哪些API才能支持协作。

 

  • 把系统操作分配给服务:第一步确定哪个服务是请求的初始入口点。下一步是确定在处理每一个系统操作时,服务之间如何交互
  • 确定支持服务协作所需API:出现系统操作跨越多个服务时候。

 

第三章 微服务架构中的进程间通信

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

3.1.1 交互方式

有多种客户端与服务的交互方式,它们分为两个维度。

第一个维度:

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

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

第二个维度:

同步模式:客户端请求要服务端实时响应,客户端等待响应时可能导致阻塞。

异步模式:客户端请求不会阻塞进程,服务端的响应可以是非实时的

 

3.1.2 在微服务架构中定义API

服务的API是服务与其客户端之间的契约。

3.1.3 API的演化

语义化版本控制规范 要求版本号由三部分组成:MAJOR.MINOR.PATCH。必须按如下方式递增版本号。

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

3.1.4 消息的格式

进程间通信的本质是交换消息。消息通常包含数据。消息的格式可以分为两大类:文本和二进制。

基于文本的消息格式

第一类是JSON和XML这样基于文本的格式。好处在于可读性很高,也是自描述的。弊端在于消息往往过度冗长,解析文本引入额外的开销。

 二进制消息格式

 

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

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

代理接口通常封装底层通信协议。

3.2.1 使用REST

REST是一种使用HTTP协议的进程间通信机制。REST中一个关键概念是 资源

REST好处与弊端

好处:简单并熟悉、直接支持请求/响应方式的通信、不需要中间代理,简化了系统架构。

弊端:只支持请求/响应方式的通信、在单个请求中获取多个资源具有挑战性、有时很难将多个更新操作映射到HTTP动词。

3.2.2 使用gRPC

一种基于二进制消息的协议,gRPC API由一个或多个服务和请求/响应消息定义组成。

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

分布式系统中,当服务试图向另一个服务发送同步请求时,永远都面临着局部故障的风险。

客户端等待响应被阻塞,可能带来的麻烦就是在其他客户端甚至使用服务的第三方应用之间传导,并导致服务中断。

模式:断路器。 这是一个远程过程调用代理,在连续失败次数超过指定阈值后的一段时间内,这个代理会立即拒绝其他调用。

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

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

如何编写健壮的远程过程调用代理呢?

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

  网路超时:在等待针对请求的响应时,要设定一个超时时间,可以保证不会一直在无响应的请求上浪费资源。

  限制客户端向服务器发出请求的数量:设置请求上限,如果请求超出上限,很有可能发出请求也不会获得响应,应该让请求立刻失败。

  断路器模式:监控客户端发出的请求的成功和失败数量。如果失败的比例超过一定的阈值,就启动断路器,让后续调用失效。在经过一定的时间后,客户端应该继续尝试,如果成功,则取消断路器。

 从服务失效故障中恢复

  需要根据具体情况决定如何从无响应的远程服务中恢复你的服务。

一种场景是 服务只向其客户端返回错误,其他情况,返回备用值(默认值/缓存)可能会有意义。

 

3.2.4 使用服务发现

为什么要使用服务发现?

为了让一个服务使用远程过程调用调用另一个服务,需要知道服务实例的网络位置。 在物理硬件运行的传统应用中,服务实例位置往往是静态的。在现代基于云的微服务应用程序中,服务实例具有动态分配的网络位置,此外 由于自动扩展、故障和升级,服务实例集会动态更改。所以要使用服务发现。

什么是服务发现?

服务发现在概念上非常简单,其关键组件就是服务注册表,他是包含服务实例网络位置信息的一个数据库。 服务启动和停止时,服务发现机制会更新服务注册表。当客户端调用服务时,服务发现机制会查询服务注册表以获取可用服务实例的列表,并且请求路由到其中一个服务实例。

实现服务发现的方式?

主要有两种方式:服务及其客户直接与服务注册表交互; 通过部署基础设施来处理服务发现。

  应用层服务发现模式

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

第一种模式是自注册模式。服务实例调用服务注册表的API来注册其网络位置,它还提供一个运行状况监测的URL。

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

第二种模式:客户端发现模式。当客户端想要调用服务时,它会查询服务注册表,获取服务实例列表。然后客户端使用负载均衡算法来选择服务实例,发出请求。

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

弊端:要为每种编程语言(或框架)提供服务发现库。

好处:可以解决多平台部署的问题。

  平台层服务发现模式

是以下两种模式的组合:

模式:第三方注册 服务实例由第三方注册到服务注册表

模式:服务端发现 客户端向路由器发出请求,路由器负责服务发现

好处: 服务发现所有方面都由部署平台处理。服务和客户端都不包含任何服务发现代码

弊端:仅限于支持使用该平台部署的服务。如基于k8s的发现仅适用于在k8s上运行的服务

 

3.3 基于异步消息模式的通信

 使用消息机制时,服务之间通信采用异步交换消息的方式完成。 基于消息机制的应用程序通常使用消息代理,它充当服务之间的中介。另一种选择是使用无代理架构,通过直接向服务发送消息来执行服务请求。

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

3.3.1 什么是消息传递?

发送方将消息写入通道,接收方从通道读取消息。

关于消息

关于消息通道

 有两种类型的消息通道:点对点和发布-订阅

 

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

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

实现单向通知

实现发布/订阅

实现发布/异步响应

 

3.3.4 使用消息代理

3.3.5 处理并发和消息顺序

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

 现代消息代理(kafka)使用的常见解决方案是分片通道。 该解决方案分为三个部分:

1.分片通道由两个或多个分片组成,每个分片的行为类似一个通道。

2 发送方在消息头部指定分片键,通常是任意字符串或字节序列。消息代理使用分片键将消息分配给特定的分片。

3.消息代理将接收方多个实例组合在一起,并将它们视为相同的逻辑接收方,例如,Kafka中的消费者组,消息代理将每个分片分配给单个的接收器,它在接收方启动和关闭时重新分配分片。

 

3.3.6 处理重复消息

处理重复消费有两种处理方法

编写幂等消息处理程序

跟踪消息并丢弃重复项

 查询历史记录,不处理已经处理过的消息   

3.3.7 事务性消息

 服务通常需要在更新数据库的事务中发布消息。 传统的解决方式是在数据库跟消息代理之间使用分布式事务。

但很多消息代理不支持分布式事务。

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

 

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

 

将消息从数据库移动到消息代理并对外发布有两种不同方法。

模式:轮训发布数据。 通过轮训数据库中的发件箱来发布消息。

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

 

3.4 使用异步消息提高可用性 

3.4.1 同步消息会降低可用性

当服务必须从另一个服务获取信息后,才能返回它客户端的调用,都会导致可用性问题。包括异步消息的请求/响应方式。

 

 

第四章 使用Saga管理事务

在微服务架构中,单个服务中的事务仍然可以使用ACID事务(原子性、一致性、隔离性、持久性)。跨服务的操作必须使用所谓Saga(一种消息驱动的本地事务序列)来维护数据一致性,而不是ACID事务。 Saga的一个挑战在于只满足了ACD(原子性、一致性、持久性)特性,缺乏传统ACID事务的隔离性。因此,应用程序必须使用所谓的 对策,找到办法来防止或减少由于缺乏隔离而导致的并发异常。

4.1 微服务架构下的事务管理

4.1.1 微服务架构对分布式事务的需求

每个服务都有自己的私有数据库,需要一种机制来保障多数据库环境下的数据一致性。

4.1.2 分布式事务的挑战

在多个服务、数据库、消息代理之间维持数据一致性的传统方式时采用分布式事务。分布式事务管理的事实标准XA。XA采用了两阶段提交(2PC)来保证事务中所有参与发方同时完成提交,或者在失败时同时回滚。 应用程序的整个技术栈需要满足XA标准,包括符合XA标准的数据库、消息代理、数据库驱动、消息API,以及用来传播XA全局事务ID的进程间通信机制。

一个问题在于 许多新技术包括NoSQL数据库、kafka并不支持XA标准的分布式事务.另一个问题在于 分布式事务的本质都是同步进程间通信,会降低分布式系统的可用性。

基于上面的问题,分布式事务对于现代应用程序并不是一个很好的选择。

4.1.3 使用Sage模式维护数据一致性 

Saga是一种在微服务架构中维护数据一致性的机制。它可以避免分布式事务所带来的问题。 一个Saga表示需要更新多个服务中数据的一个系统操作。Saga由一连串本地事务组成。每一个本地事务负责更新它所在服务的私有数据库,这些操作仍依赖我们所熟悉的ACID事务框架。

模式:Saga

通过使用异步消息来协调一系列本地事务,从而维护多个服务之间的数据一致性。

 Saga使用补偿事务来回滚所做出的改变。

假设一个Saga的第n+1个事务失败了,必须撤销前n个事务的影响。从概念上说,每一个 Saga按照正常事务的反向顺序来执行补偿事务:Cn,Cn-1,,,C1;

4.2 Saga的协调模式

Saga的实现包括协调Saga步骤的逻辑。当通过系统命令启动Saga时,协调逻辑必须选择并通知第一个Saga参与方执行本地事务。一旦该事务完成,Saga协调选择并调用下一个Saga参与方。这个过程持续到Saga执行完所有步骤。如果任何本地事务失败,则Saga必须以相反的顺序执行补偿事务。有几种方法可以用来构建Saga的协调逻辑。

 协同式:把Saga的决策和执行顺序逻辑分布在Saga的每一个参与方中,它们通过交换事件的方式进行沟通。

 编排式:把Saga的决策和执行顺序逻辑集中在一个Saga编排器类中。Saga编排器发出命令式消息给各个Saga参与方,指示这些参与方服务完成具体操作(本地事务)。

 

4.2.1 协同式Saga

Saga 参与方订阅彼此的事件并做出相应的响应。需要考虑的问题? 

可靠的事件通信 需要注意的问题

 1.确保Saga的参与方将更新本地数据库和发布事件作为数据库事务的一部分。

 2.确保Saga的参与方必须能够将接收到的每个事件映射到自己的数据上。解决方案是 让Saga参与方发布包含 相关性ID 的事件。

协同式Saga利弊

  利:

  •  简单:服务在创建、更新或删除业务对象时发布事件。
  •  松耦合:参与方订阅事件并且彼此之间不会因此产生耦合。

 弊:

  更难理解:Saga逻辑分布在每个服务的实现中。

  服务之间的循环依赖

  紧耦合的风险:每个Saga参与方需要订阅所有影响它们的事件。订阅方需要与发布方生命周期代码保持一致。

 

4.2.2 编排式Saga

定义一个编排器类,唯一职责就是告诉Saga参与方该做什么事情。Saga编排器使用命令/异步响应方式与Saga参与方服务通信。

为完成Saga中一个环节,编排器对某个参与方发出一个命令式的消息,告诉这个参与方该做什么操作。当参与方服务完成操作后,会给编排器发送一个答复消息。

编排器处理这个消息,并决定Saga的下一步操作是什么。

状态机是建模Saga编排器一个好方法,状态机由一组状态和一组由事件触发的状态之间的转换组成。每个转换都可以有一个动作,对Saga来说动作就是对某个参与方的调用。

 
编排式Saga利弊

  利:

  •  更简单的依赖关系
  •  较少的耦合
  • 改善关注点隔离,简化业务逻辑

 弊:

    在编排器中集中过多业务逻辑的风险,可以通过设计 只负责排序的编排器来解决这个风险。

 

4.3 解决隔离的问题

ACID事务的隔离属性可确保同时执行多个事务的结果与顺序执行它们的结果相同。

使用Saga的挑战在于缺乏ACID事务的隔离性。

 

4.3.1 缺乏隔离导致的问题 

  1.  丢失更新:一个Saga没有读取更新,而是直接覆盖了另一个Saga所做的更改
  2.  脏读:一个事务或一个Saga读取了尚未完成的Saga所做的更新
  3.  模糊或者不可重复读:一个Saga两个不同步骤读取相同的数据却获得不同的结果,因为另一个Saga已经进行了更新。

 

4.3.2 Saga模式下实现隔离的策略 

  •  语义锁
  •  交换式更新
  •  悲观视图
  •  重读值
  • 版本文件
  • 业务风险评级
 

第五章 微服务架构中业务逻辑设计 

 

 

 

posted @ 2023-05-21 11:50  逍的遥  阅读(142)  评论(0编辑  收藏  举报