服务拆分与边界

微服务的设计原则

在实践微服务软件架构之初,一开始大家所关注的焦点是“如何拆”、“拆多大”以及技术与组织架构的配称(康威定律),核心思路是通过将单体应用通过分拆去变成更小的软件发布单元,以解决单体应用的软件迭代速度慢的问题(背后导致了商业价值创造慢的后果)。然而,当微服务改造工作完成且微服务的个数达到一定的规模时,各服务之间的连接、排错、安全保障、监控等问题就逐渐地浮出了水面,那时行业深刻地体会并认识到微服务软件架构其实是将复杂度从单体应用内转移到了微服务之间。随着分布式应用规模的进一步增大,所涉开发和运维人员增长到一定数据时,效率问题再一次变得像单体应用时代那样不可小视。不过,这一次所面临的问题域和规模比那时大了很多。要解决微服务软件架构所带来的新问题,需要探索更加体系化、规范化和全局一致的解决方案,那就不可避免地会采用新的概念切分手法去构建新的解决方案,期间不可并避免地会打破旧概念并创造出新概念。在微服务设计中,我们同样需要遵循中如《架构设计原则》中高内聚低耦合、复用、单一职责等设计原则。

服务的定义

服务怎么定义呢?需要从应用开发者角度看待怎么定义服务。服务在功能维度对应某一功能,如查询已买订单详情。在 Dubbo 中,对应某个接口下的方法;在 Spring Cloud 和 gRPC 对应一个 http 请求。如果从面向函数编程角度,一个服务就是一个 function。在 Java 语言中,class 是一切编程的基础,所以将某些服务按照一定的维度进行聚合,放到某个接口中,就成了一个接口包含了很多的服务。从 Dubbo 角度来解释下:Dubbo 是面向接口编程的远程通信,所以 Dubbo 是面向服务集的编程,你如果想调用某个服务,必须通过接口的方式引入,然后调用接口中的某个服务。Dubbo Ops 中提供的服务查询功能,其实不是查询单个服务,而是通过查询接口(服务集)之后获得具体的方法(服务)。

而在 Spring Cloud 的世界里,服务提供方会将自己的应用信息(IP + port)注册成应用下的一个实例,服务消费方和服务提供方按照约定的形式进行 Rest 请求。每个请求对应的也是一个服务。Kubernetes 里的 Service 其实是对应到一组 Pod + port 列表,和 DNS 联系紧密;用通俗易懂的方式表达:维护了 Pod 集合的关系映射。和上面讲的服务是属于不同场景下的两个概念。

一个应用下包含了很多接口,一个接口下包含了很多服务(Dubbo);或者一个应用包含了很多的服务(Spring Cloud)。按照这个方式定义服务,服务治理的粒度其实也是按照服务粒度,可以针对每个服务设置超时时间,设置路由规则等等。

领域驱动

微服务设计首先应建立领域模型,确定逻辑和物理边界后,然后才进行微服务边界拆分,而不是一上来就定义数据库表结构,也不是界面需要什么,就去调整领域逻辑代码。领域模型和领域服务应具有高度通用性,通过接口层和应用层屏蔽外部变化对业务逻辑的影响,保证核心业务功能的稳定性。

微服务的边界

在事件风暴中,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象。根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合,聚合之间是第一层边界。根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,形成领域模型,限界上下文之间的边界是第二层边界。

为了方便理解,我们将这些边界分为:逻辑边界、物理边界和代码边界。

  • 逻辑边界:微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚,可根据需要变成物理边界,也就是说聚合也可以独立为微服务。

    • 逻辑边界主要定义同一业务领域或应用内紧密关联的对象所组成的不同聚类的组合之间的边界。事件风暴对不同实体对象进行关联和聚类分析后,会产生多个聚合和限界上下文,它们一起组成这个领域的领域模型。微服务内聚合之间的边界就是逻辑边界。一般来说微服务会有一个以上的聚合,在开发过程中不同聚合的代码隔离在不同的聚合代码目录中。

    • 逻辑边界在微服务设计和架构演进中具有非常重要的意义!微服务的架构演进并不是随心所欲的,需要遵循一定的规则,这个规则就是逻辑边界。微服务架构演进时,在业务端以聚合为单位进行业务能力的重组,在微服务端以聚合的代码目录为单位进行微服务代码的重组。由于按照 DDD 方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间代码松耦合,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。

  • 物理边界:物理边界主要从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境是相互物理隔离的,分别运行在不同的进程中。这种边界就是微服务之间的物理边界。

  • 代码边界:代码边界主要用于微服务内的不同职能代码之间的隔离。微服务开发过程中会根据代码模型建立相应的代码目录,实现不同功能代码的隔离。由于领域模型与代码模型的映射关系,代码边界直接体现出业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。微服务如果需要进行功能重组,只需要以聚合代码为单位进行重组就可以了。

现在我们来看一个微服务实例,在下面这张图中,我们可以看到微服务里包含了两个聚合的业务逻辑,两个聚合分别内聚了各自不同的业务能力,聚合内的代码分别归到了不同的聚合目录下。那随着业务的快速发展,如果某一个微服务遇到了高性能挑战,需要将部分业务能力独立出去,我们就可以以聚合为单位,将聚合代码拆分独立为一个新的微服务,这样就可以很容易地实现微服务的拆分。

从领域到微服务的拆分

边界与职能清晰

微服务完成开发后其功能和代码也不是一成不变的。随着需求或设计变化,微服务内的代码也会分分合合。逻辑边界清晰的微服务,可快速实现微服务代码的拆分和组合。DDD 思想中的逻辑边界和分层设计也是为微服务各种可能的分分合合做准备的。微服务内聚合与聚合之间的领域服务以及数据原则上禁止相互产生依赖。如有必要可通过上层的应用服务编排或者事件驱动机制实现聚合之间的解耦,以利于聚合之间的组合和拆分。

分层架构中各层职能定位清晰,且都只能与其下方的层发生依赖,也就是说只能从外层调用内层服务,内层服务通过封装、组合或编排对外逐层暴露,服务粒度由细到粗。应用层负责服务的编排和组合,领域层负责领域业务逻辑的实现,基础层为各层提供资源服务。

避免过度拆分

微服务的过度拆分会使软件维护成本上升,比如:集成成本、发布成本、运维成本以及监控和定位问题的成本等。在项目建设初期,如果你不具备较强的微服务管理能力,那就不宜将微服务拆分过细。当我们具备一定的能力以后,且微服务内部的逻辑和代码边界也很清晰,你就可以随时根据需要,拆分出新的微服务,实现微服务的架构演进了。 当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖需要符合高内聚松耦合的设计原则和开发规范,否则你也不能很快完成微服务的架构演进。

Links

下一页