微服务架构设计模式(二)服务的拆分策略

服务的拆分策略

第一节 微服务架构是什么?

1.1 软件架构是什么?

软件架构是一种抽象的结构,由软件的各个组成部分和它们之间的依赖关系构成。计算机系统的软件架构是构建这个系统所需要的一组结构,包含软件元素、它们之间的关系以及两者的属性

将软件分解成元素和定义这些元素之间的关系,决定了软件的能力。

应用程序有两个层次的需求:功能性需求质量性需求,后者决定一个应用在运行和开发时的质量,由所选择的软件架构决定。

1.2 软件架构的 4 + 1 视图模型

软件架构可以像建筑一样有多个架构视角(+1指场景,把视图串联在一起):

每个视图的目的:

  • 逻辑视图:开发人员创建的元素,在面向对象语言中是类(Class)和包(Package),关系包括继承、关联和依赖。
  • 实现视图:构建编译系统的输出,组件是由一个或多个模块组成的可执行或可部署单元。在Java中模块是JAR文件,组件是WAR文件或可执行JAR文件;关系包括模块间的依赖关系和组件模块间的组合关系。
  • 进程视图:运行时的组件,元素是进程,进程间关系即进程间通信。
  • 部署视图:进程如何映射到机器,元素由计算机和进程组成,机器之间的关系即网络。

1.3 架构的风格

分层架构将软件元素按层的方式组织,每层都有自己的职责,上一层只能依赖于下方的层。

应用程序可以根据分层结构分为:

  • 表现层
  • 业务逻辑层
  • 数据持久化层

但表现层无法体现应用可能由多个系统调用的情况,持久化层也无法体现多个数据库交互,业务逻辑层实际上也并不一定依赖于数据持久化层。

六边形结构是分层结构的替代架构:业务逻辑不再依赖于适配器,而是相反适配器都依赖于业务逻辑。

  • 入栈端口是业务逻辑公开的API,外部程序都可以调用它。

  • 出栈端口是业务逻辑调用外部系统的方式,比如存储接口定义了一系列数据访问操作。

  • 入栈适配器调用入栈端口处理外部的请求,Spring MVC Controller 或 订阅消息的消息代理客户端。

  • 出栈适配器调用外部应用或服务处理业务逻辑的请求,如数据访问对象(DAO)类或调用远程服务的代理类。

1.4 微服务架构风格

单体架构是一种架构风格,实现视图是单个组件,将应用构建为单个可执行或可部署组件。

微服务架构是一种架构风格,实现视图由多个组件构成,将应用构建为松耦合、可独立部署的一组服务。

1.4.1 什么是服务?

单一的、可独立部署的软件组件就是服务,其API封装了内部实现,强制实现了应用程序的模块化。每个服务都有自己的架构甚至技术栈,往往都是六边形架构,业务逻辑和适配器交互。

1.4.2 什么是松耦合?

服务自身的持久化数据就像类的私有属性一样被封装,这样开发者就可以任意修改服务的数据结构而不用担心影响到别的服务。

1.4.3 共享类库的角色

开发时会把常用的功能打包以便多个应用可以重用,而不必复制代码,这是减少重复代码的好方法,但有可能会意外的在服务之间引入耦合。

把可能会变更的功能作为服务来实现(如业务功能),而不变的可以打包成库。

1.4.4 服务大小并不重要

“微”并不特指服务很小,更多的含义应该是单一,我们需要能够识别服务,并确定它们之间如何协作。

第二节 为应用程序定义微服务架构

定义程序架构和软件开发一样,没有一个机械化的流程可以保证输出一个合理的架构,我们只能使用一些不断迭代和创新的方法:

大致流程:

  • 第一步将应用程序的需求提炼为各种系统操作,是程序必须要处理的请求。
  • 第二步确定如何分解服务
  • 第三步确定每个服务的API。将每个系统操作分配给服务,服务可以独立或协作实现操作。

服务分解的几个障碍:

  • 网络延迟:服务间往返太多会出现问题。
  • 同步通信:服务间的同步通信会降低可用性,需要使用自包含服务
  • 跨服务的数据一致性:使用Saga
  • 上帝类(God Class):使用领域驱动设计(DDD)消除上帝类。

2.1 识别系统操作

第一步创建由关键类组成的抽象领域模型,关键类提供用于描述系统操作的词汇表。第二步确定系统操作,根据领域模型描述每个系统操作的行为。

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

(1)创建抽象领域模型

通过需求整理用户故事,从中提炼得到:

每个类的作用:

  • Consumer:下订单的用户。
  • Order:用户下的订单,描述订单信息并跟踪状态。
  • OrderLineItem:订单的一个条目。
  • DeliveryInfo:送餐的时间和地址。
  • Restaurant:餐馆,准备生产订单,同时要发起送货。
  • MenuItem:餐馆菜单的一个条目。
  • Courier:送餐员负责把订单送到用户手里,可跟踪送餐员可用性和位置。
  • Address:用户和餐馆的地址。
  • Location:送餐员当前经纬度。

(2)定义系统操作

系统操作包括:

  • 命令型:创建、更新或删除数据的系统操作。
  • 查询型:查询和读取数据的系统操作。

分析用户故事中的动词,识别系统指令:

命令规范定义了命令对应的参数、返回值和领域模型类的行为,行为规范包括前置条件和后置条件:

抽象的领域模型和系统操作可以回答这个应用是做什么的问题,每一个系统操作的行为都通过领域模型的方式来描述。

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

(1)什么是业务能力?

业务能力:指一些能够为公司产生价值的商业活动。

组织的业务能力指组织的业务是什么,这通常是固定的,而组织采用何种方式来实现其业务能力往往是不断变化的。每个组织有哪些业务能力,通过对组织的目标、结构和商业流程的分析得来。每个业务都可以看作是一个服务,除非只面向业务而非技术。

业务能力集中在业务对象上,如理赔业务对象是理赔管理功能的重点,能力又可以划分为子能力,理赔管理又包括理赔信息管理、理赔审核和理赔付款管理。

FTGO的业务能力:

  • 供应商管理
    • Courier management:送餐员相关信息管理
    • Restaurant information management:餐馆菜单和其他信息管理(如营业地址和时间)
  • 消费者管理
    • 消费者有关信息的管理
  • 订单获取和履行
    • Order management:让消费者可以创建和管理订单
    • Restaurant order management:让餐馆可以管理订单的生产过程
    • 送餐管理
    • Courier availability management:送餐员实时状态
    • Delivery management:把订单送到用户手中
  • 会计记账
    • Consumer accounting:管理跟消费者相关的会计记账
    • Restaurant accounting:管理跟餐馆相关的会计记账
    • Courier accounting:管理跟送餐员相关的会计记账
  • 其他

(2)根据业务能力映射到服务

确定了业务能力后可以为每个能力定义服务,能力有级别划分,哪层级别映射到服务是很主观的判断,有以下理由:

  • 供应商管理映射到两个服务,因为餐馆和送餐员是完全不同的供应商。
  • 订单获取和履行映射到三个服务,每个服务负责流程的不同阶段。其中送餐员可用性管理和交付管理结合在一起,映射到单个服务,因为二者交织在一起。
  • 会计记账能力映射到一个独立服务,不同类型的会计也很相似。

如下图所示:

架构定义流程的一个重要步骤是调查服务如何在每个关键架构服务中协作,比如你发现由于过多的进程间通信导致特定的分解效率低下,导致必须要把一些服务组合在一起。而在复杂性方面则会增长到很值得拆分的程度。

2.3 根据DDD子域进行服务拆分

DDD通过定义多个领域模型解决传统模型难以让所有团队保持一致的难题(如术语),每个领域模型都有明确的范围。

  • 子域:领域驱动为每个子域定义单独的领域模型,领域用来描述应用程序问题域的一个术语。识别子域和识别业务能力一样,分析业务并识别业务的不同专业领域,如FTGO的子域有(订单获取、订单管理、餐馆管理、送餐与会计)
  • 限界上下文(bounded context):领域模型的边界被称为限界上下文,包括实现这个模型的代码集合。

通过DDD的方式定义一个个子域,并把每个子域对应为对应的一个服务,从而完成微服务的设计工作:

2.4 拆分的原则

面向对象的设计原则也可以应用于微服务架构:

  • 单一职责原则(SRP):每个类都应该只有一个职责,这样就只有一个理由对它进行修改。多个职责就会使类变得不稳定,对于微服务而言就如FTGO中为客户获取餐食中每个方面(订单获取、订单准备、送餐)都由单一的服务承载。
  • 闭包原则(CCP):在包中所包含的类都是对同类变化的集合,如果要对包进行修改,需要调整的类都应该在包内。若两个类的修改必须耦合的先后发生,它们应该在同一个包内,这样开发者只须对一个交付包进行修改,而不是大规模的修改整个应用。对于微服务架构,CCP是解决分布式单体的利器。

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

  • 网络延迟
    • 问题:服务分解导致两个服务间大量的往返调用。
    • 解决:可以通过批处理一次往返多个对象,或者把多个相关服务组合用编程语言的函数调用代替高昂的进程间通信来解决。
  • 同步进程间通信导致可用性降低
    • 问题:如新增订单 createOrder() 通常让 OrderService 使用REST同步调用其他服务,REST这样的协议会降低服务可用性,任一被调用服务不在可用状态就导致订单无法创建。
    • 解决:异步消息降低同步调用的紧耦合,同时提高可用性。
  • 在服务之间维持数据一致性
    • 问题:需要更新多个服务的数据时,保持服务间的数据一致性。如餐馆接受订单时,要在KitchenService和DeliveryService中同时更新,前者修改Ticket状态,后者安排订单交付,都要以原子化的方式完成更新。
    • 解决:传统的解决方案是基于两阶段提交的分布式事务管理,但并非最好的选择;Saga是一系列使用消息协作的本地事务,唯一的限制是最终一致性,需要原子更新数据都在单个服务中。
  • 获取一致的数据视图
    • 问题:无法跨过多个数据库获得一致的真实视图。单体应用中ACID保证返回一致视图,微服务即使每个服务数据库都一致也无法得到。
    • 解决:视图必须驻留在单个服务中。
  • 上帝类阻碍了拆分
    • 问题:整个应用程序都要使用的全局类,上帝类代表对应用至关重要的概念如银行账户、电子商务订单、保险政策等等。对于FIGO就是Order类,系统大部分服务都涉及订单。如下图2-10是传统建模创建的Order类结构。
    • 解决:
      • 将Order类打包到库,创建一个中央数据库,处理订单的所有服务都要访问该库。但此方案破坏了微服务架构的原则,并导致了紧耦合。
      • 将Order数据库封装到OrderService供其他服务调用,但OrderService将成为一个纯数据服务,成为缺乏业务逻辑的贫血领域模型
      • 好的方案是遵循DDD将每个服务视为领域模型的单独子域,FTGO的每个与订单相关的服务都有自己的领域模型及对应的Order类的版本。如下图2-11,对于DeliveryService,Order可以命名为更合适的Delivery。

DeliveryService对Order的其他属性不感兴趣:

KitchenService的Order就是一个Ticket(后厨工单),只包含状态、请求送餐时间、准备时间等,以及告诉餐馆准备的订单项列表。而不关心如消费者、付款、交付这些无关属性。

每个领域模型的Order类都表示同一个订单业务实体的不同方面,应用必须维持不同服务中这些不同对象之间的一致性。比如一旦OrderService授权消费者的信用卡,必须触发KitchenService中创建Ticket。同样,如果KitchenService拒绝订单,必须在OrderService中取消订单,并为客户退款。

可以通过事件驱动机制Saga来维护服务间的一致性

2.6 定义服务API

到这个阶段,我们有了一个系统操作列表和潜在服务列表,下一步是定义每个服务的API:服务的操作和事件

定义API操作的原因:

  1. 某些操作对应系统操作,由外部客户端调用,或者其他服务调用。
  2. 支持服务之间协作的操作,仅供其他服务调用。

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

(1)将系统操作分配给服务

第一步,确定哪个服务是请求的初始入口点。如 noteUpdateLocation() 更新送餐员位置,因为与送餐员相关,所以应该分配给CourierService,但它也是需要送餐地点的DeliveryService。这种情况最好把操作分配给需要操作所提供信息的服务,其他情况下分配给具有处理它所需信息的服务更有意义。

(2)确定支持服务协作所需要的API

第二步,是确定在处理每个系统操作时,服务之间如何交互。

createOrder() 操作需要OrderService调用一下服务以验证其前置条件并使后置条件成立:

  • ConsumerService:验证消费者是否可以下订单并获取付款信息。
  • RestaurantService:验证订单行项目,验证送货地址和时间是否在餐厅的服务区域内,验证订单最低要求,并获得订单行项目的价格。
  • KitchenService:创建Ticket后厨工单。
  • AccountingService:授权消费者的信用卡。


参考:

🔗 《微服务架构设计模式》