微服务架构 - 服务的拆分

前言

上一篇文章我们介绍了什么是微服务以及选择微服务必须考虑的一些因素,如果我们最终的答案是肯定的,那么下一步我们就需要考虑如何保证服务设计的一致性、可复用性以及可维护性等一系列非功能性设计约束。这篇文章我们一起聊聊如何利用领域驱动设计(DDD)设计一个好的微服务(希望读者有一定的DDD知识或经验)。

1.面向业务能力定义服务

在上篇文章我们提到”服务”是基于业务能力的抽象。只有正确的理解业务,才可能定义出合理的服务。让团队高效的理解业务是非常有必要的,尤其对于一些企业级应用,业务知识是另一个非计算机领域的知识(医疗、金融、建筑等),对于程序员来说,这些领域知识非常复杂,很多时候需要借助相关的领域专家一起合作完成。服务的定义往往来自这些跨领域专家之间相互沟通达成的共识,所以建立一种通用语言(Ubiquitous Language),可以避免沟通的歧义,提高沟通效率,同时有利于领域知识在团队中沉淀。通用语言不是专业术语,而是团队成员在讨论理解业务过程中形成的,它应该是没有歧义、简单且易理解的,并被用于领域建模和代码的实现。

业务是服务的核心成员(first class),对于一个服务来说,相比其他方面(技术框架等),核心业务相对稳定不容易变化。相同的业务,我们可以使用不同的呈现方式,不同的持久化技术,所以周边的其他都在为业务提供服务。而且业务模块应对未来的衍化需要灵活性和独立性,这不仅可以充分复用领域模型来满足不同客户的需求,更重要的是他在突出强调领域的重要性,这有利于团队对领域知识的总结和归纳。而这些思想也贯穿在整个领域驱动开发过程中。

这里我们必须指出,传统的分层架构方式(Presentation - BusinessDomain - Persistence)并不能很好的凸显领域业务层的重要性,因为整个分层架构给人的感觉是每一层的重要性是一样的;另外分层架构必须满足上层对下层的依赖,反之则违反了分层架构原则。这会潜意识的引导开发人员将领域模型依赖于下一层的持久层以及相关技术,并习惯以存储结构来设计领域模型(容易形成贫血模型(Anemic Domain Model))。作为服务的核心,领域层理所应当来定义自己的出、入接口(因为只有领域层是最清楚业务需求是什么,以及未来功能的演化和迭代),但是这些接口的定义应该放在哪一层?放在领域层就会导致持久层依赖于领域层,违反了分层原则;放在持久层又违反了共同封闭原则(CCP)。

在领域驱动开发中六边形架构(Hexagonal Architecture)能够很好的凸显业务的重要性和独立性,从下图可以看出整个领域层都封装在六边形内,并暴露不同的业务能力作为接口,表现层和持久层被分散在六边形的外层,他们通过各自的适配端口来获取领域接口提供的数据,或通过实现领域接口为领域层提供服务。六边形架构强调以业务为核心(关注点分离),这不仅有利于内层核心业务的隔离,方便业务逻辑的独立测试,而且有利于外层应用的灵活适配。

Hexagonal Arch

当然六边形架构并不像分层架构那样直观,毕竟分层架构的逻辑结构和代码物理结构能很好对应,而六边形架构需要开发人员根据业务能抽象出六边形的架构,而且时刻映射在脑子,否则很容易在代码的维护过程中慢慢回到分层架构,偏离了架构的初衷。

2.边界的重要性

人们对复杂事物的认识都是从拆分归纳开始。分而治之就是老祖宗对解决问题的方法的精辟总结。系统设计的本质就是在解决一个复杂问题,我们常说的解耦,就是在做拆分工作,不合理的拆分不仅会使问题变得难易理解,而且会影响团队合作,所以如何拆分服务是微服务架构决定性的因素。回到上篇的提问,微服务和一般的服务有什么不同?很明显微服务强调在”微”这个形容词上。”微”不仅表示服务粒度的大小,它的深层含义是服务所表达的业务能力需要一个明确的范围(不是什么都在一个服务中体现,也不是没有根据的随意划分)。所以清晰定义服务界限是微服务架构的前提基础:

  1. 隔离关注点:人的精力是有限的,一旦涉及的变量过多,我们容易抓不住重点。通过定义清晰的领域边界来划分关注点,有利于团队对整个系统的理解以及对每个具体关注点的深入分析;定义清晰的边界,有利于我们时刻警惕边界间不合理的交互,避免破坏服务的封装以及关注点与外部环境的依赖(不可否认,边界是最容易发生冲突的地方,是不是突然觉得特朗普”Build Wall”的计划也有其合理性)。

  2. 明晰职责:定义清晰的边界,可以明晰业务职责,不但让系统的设计和实现更容易理解,而且明确了团队的职责,使团队工作更有目标性,工作自主性更强,效率更高。团队职责明晰了,相应的团队间扯皮现象也会减小,这样更有利团队间的合作。

  3. 建立一致性:边界的定义根据的是事物间的相关性,它可能是逻辑的相关性,文化相关性甚至是政治相关性等。一个清晰的边界,有利于我们构建一致的设计、一致的工作环境以及一致的领域通用语言。这些不仅能提高团队的工作效率以及系统的可维护性,而且有利于团队的认同感。

说到边界,其实我们对边界并不陌生,比如函数、类、模块这些我们经常挂在嘴边的概念,其实都是在为不同的逻辑定义直观的物理界限,也许我们对这些概念太熟悉,以至于我们已经模糊了边界的意义。同样,服务也是一种边界的定义,它关注的粒度更大,而且它对业务能力边界的关注远大于技术层面,这也是很多开发人员需要改变的一种思维方式。领域驱动开发提出的界限上下文(Bounded Context)概念也是在强调显性边界的重要性。通用语言必须在指定界限上下文才有意义,每个界限上下文意味着专有的职责,不同界限上下文应该是相互独立、自治的。这些特性非常符合微服务,所以越来越多的人结合领域驱动开发的方法来设计微服务。

3.边界的定义

那么如何为服务定义一个好的边界?首先边界必须是显示且明确的,团队成员必须结合领域模型深入讨论,达成一致的共识,否则很容易产生含糊的边界,导致服务内部模型不一致,外部依赖混乱。一旦明确了边界,我们可以通过独立的服务契约(这里之所以不用接口是因为它很容易和编程语言的接口混淆,契约不仅包括接口也包括传递的消息以及参数等)来固化服务边界的物理表现。任何来自界限上下文之外的访问必须通过契约来实现,边界内的领域模型必须对外部隐藏。这些说起来容易,做起来就不那么简单了。很多开发在面向对象开发中对函数的暴露太过随意,为了自己的方便,恨不得将对象的所有状态都通过接口暴露给外部,可是在服务中随意的暴露契约,将会是一个灾难。我个人的建议,一旦你想暴露一个公共函数或者接口给外部,你的第一反应应该先考虑是不是可以不需要这个接口。

一个好的服务必须满意以下原则:

  1. 高内聚低耦合:这个原则不需要太多解释,一个合格的开发人员的日常工作应该时刻在权衡这两个因素(如果你大部分时间不是在思考这个问题,那么你是时候改变你的工作方式)。边界内的模型和对象应该保持强相关性,否则随着服务的演变,里边的逻辑就会相互纠缠,最后形成一个大泥球(big ball of mud)。同样如果一个强相关的模型被分离在边界之外,服务就很容易引入其他依赖,导致边界内的模型缺乏独立性。

  2. 单一职责原则(SRP):在之前的SOLID原则中我们介绍了单一职责原则,这个原则不仅仅适合面向对象的设计,对于服务来说依然适用。在服务界限上下文中,它只能表达的一个指定的业务能力,其他任何有影响服务变化的原因,都说明服务违背了单一职责。比如在你修改仓储行为,你发现你的订单服务也需要改变时,那么你的订单服务上下文一定引入了本不应该属于这个上下文的领域模型。

  3. 共同闭包原则(CCP):将同时修改,目的相同的类放到同一个服务;不会同时修改,目的不同的类放到不同的服务。是不是和单一职责有异曲同工之妙?CCP更注重系统的集成和部署环节,当然设计实现阶段这些都是我们必须提前考虑的。

服务的边界并不是简单的几个原则和方法可以涵盖,它也不是一次性就能完全明晰整个界限,它需要团队不断的讨论,持续的重构才可能抽象出一个合理的界限上下文,而且界限也不是一尘不变的,随着软件的不团演变,服务的边界也在不断的更新-世界上唯一不变的就是变化。

后记

服务的拆分是微服务开发的基础,也是很多开发最容易忽视的一步。俗话说千里之行始于足下,如果没有定义的一致性,凭感觉拆分,那么很容易产生不合理的服务,随后将会导致集成、部署、可用性等一系列问题。很多开发人员开始做微服务,一上来就是什么docker、K8s、Kafka、Cloud一大堆技术名称,即使我们使用了这些技术,我们的实现也可能和微服务架构背道而驰。在专业的团队或企业背景下,我们需要有一个指导原则来帮助我们规范我们的设计与实现,而领域驱动开发正好为微服务架构提供了系统的理论与实践支持。

(转载本站文章请注明作者和出处,请勿用于任何商业用途)

上一篇:微服务架构 - 我们准备好了吗
下一篇:微服务架构 - 服务的通讯