引言
Microservice指微服务这类架构风格,构成微服务架构的每一个具体实例仍然是Service(服务) 。我们常常说,这个系统采用了微服务架构设计,它由若干服务构成。
关于微服务架构,我们不妨给出一个概括性定义:
- 根据功能把应用拆分为一组服务
- 服务间通信采用轻量级协议
- 每个服务相对较小且容易维护
- 每个服务拥有自己的数据库
- 每个服务可以独立部署和扩展
一般来说,对于服务有两种拆分方式:
- 起源于业务架构, 按业务能力拆分;
- 基于DDD的理论,按限界上下文拆分。
按业务能力拆分服务,粒度较粗,我们一般推荐按限界上下文拆分服务。
按业务能力拆分
所谓业务能力是一个来自于业务架构建模的术语,指一些能够为组织产生价值的商业活动,与业务类型强相关。例如,保险公司业务能力通常包括承保、理赔管理、账务和合规,在线商店的业务能力包括订单管理、库存管理和发货,等等。
业务能力定义了一个组织的工作,比较直接且粒度粗些,容易理解,通常是稳定的。与之相反,组织采用何种方式来实现它的业务能力,是随着时间不断变化的。例如,曾经你通过把支票交给银行柜员的方式来兑现支票,后来很多ATM机都支持直接兑现支票,现在人们可以使用智能手机的银行App来直接兑现支票。正如你所见,“兑现支票”这个业务能力是稳定不变的,但是这个能力的实现方式一直在发生着戏剧性的变化。
一个组织具体有哪些业务能力,是通过对组织的目标、结构和商业流程的分析得来的。业务能力通常可以分解为多个子能力。例如,理赔管理能力可以分解为三个子能力,即理赔信息管理、理赔审核和理赔付款管理。每一个业务能力或子能力都可以被认为是一个服务,有一定的主观因素。
按限界上下文拆分
领域驱动设计(Domain-Driven Design,DDD)是一种主流的软件设计方法,它可以帮助我们设计高质量的软件模型。在正确实现的情况下,我们通过DDD完成的设计恰恰就是软件的工作方式。
DDD的根基是统一语言(Ubiquitous Language,UL)和模型驱动设计(Model-Driven Design,MDD),而领域驱动设计的过程,就是建立起统一语言和识别模型的过程。
业务方和技术方一起创建一套适用于领域建模的统一语言,统一语言必须在团队范围内达成一致。所有成员都使用统一语言进行交流,每个人都能听懂别人在说什么,统一语言也是对软件模型的直接反映。业务方和技术方在一起工作,这样开发出来的软件能够准确的表达业务规则。
有了统一语言后,就可以进行MDD了,而MDD一般分为两个阶段:战略设计(Strategic Design)和战术设计(Tactical Design)。战略设计属于高层设计,一般包括子域划分、限界上下文识别和上下文映射。战术设计属于低层设计,就是如何具体地组织不同的业务模型。
本文主要关注点是服务边界,所以我们仅讨论战略设计。从广义上来讲,领域(Domain)是一个组织所做的事情以及其中所包含的一切,表示整个业务系统,对应着要解决的问题。软件开发要解决问题,而解决问题要分而治之。所谓分而治之,就是要把问题分解了,对应到领域驱动设计中,就是要把一个大领域分解成若干的小领域,而这个分解出来的小领域就是子域(Subdomain)。子域的划分其实就是对统一语言的分类,而对于一个真实项目而言,划分出来的子域可能会有很多,但并非每个子域都一样重要。所以,我们还要把划分出来的子域再做一下区分,分成核心域(Core Domain)、支撑域(Supporting Subdomain)和通用域(Generic Subdomain)。有了划分出来的子域后,如何组织这些子域从而落地到解决方案中?这就引出了领域驱动设计中的一个重要的概念,限界上下文(Bounded Context,BC)。限界上下文,顾名思义,它形成了一个边界,一个限定了统一语言自由使用的边界,一旦出界,含义便无法保证。
传统的企业架构建模方式往往会为整个企业建立一个单独的模型,我们可能会认为应该为整个业务系统创建一个单一的、内聚的和全功能式的模型。然而,DDD采取了完全不同的方式,它的目标是多个领域模型,每个领域模型存在于BC这个显式的边界之内。传统建模方式的挑战在于,让组织内的所有团队都对全局单一的建模和术语定义达成一致是非常困难的。另外,对于组织中的特定团队而言,这个单一的业务实体定义可能过于复杂,超出了他们的需求。此外,这些传统的领域模型可能会造成混乱,因为组织内有些团队可能针对不同的概念使用相同的术语,而也有些团队会针对同一个概念使用不同的术语。DDD通过定义多个领域模型来避免这个问题,每个领域模型都有明确的范围。
服务边界
微服务架构提出后,一直没有很好的理论支撑如何合理地拆分服务边界,人们常常为服务如何合理拆分而争吵不休。
后来,DDD被发现恰好可以弥补微服务的营养不良:
- 服务最大不要大过一个BC,否则服务内可能会存在有歧义的领域概念;
- 服务最小不要小过一个聚合,否则会引入分布式事务的复杂度;
- 服务间最好通过Domain Event来进行交互,这样可以让服务保持松耦合。
坦白讲,DDD诞生十多年来,一直处于不温不火的状态,但微服务就像是DDD的心上人,使得DDD真正焕发起了青春。同时,微服务与DDD的结合,让微服务架构看起来似乎更加稳健了。我们不得不感叹,微服务和DDD简直就是天生一对!
按限界上下文拆分服务,就是将BC作为一个服务。因为BC有业务边界、工作边界和技术边界,所以服务也有这三重边界。
关于服务的三重边界,简单说明一下。
类型 | 说明 |
---|---|
业务边界 | 对领域模型的控制,维护了模型的完整性与一致性,从而降低系统的业务复杂度 |
工作边界 | 对团队协作的控制,建立了团队之间的合作模式,避免团队之间的沟通变得混乱,从而降低系统的管理复杂度 |
技术边界 | 对技术风险的控制,做出对系统质量属性的响应与承诺,功能复用,管理变化,确定服务之间的集成方式,从而降低系统的技术复杂度 |
业务边界
使用UML用例图对业务场景进行分析,每一个用例就是一个业务活动。
语义相关性
识别语义相关性的前提是准确地使用统一语言描述业务活动。在描述时,应尽量避免使用“管理(manage)”或“维护(maintain)”等过于抽象的词语。抽象的词语容易让我们忽视隐藏的领域语言,缺少对领域的精确表达。
从语义角度去分析业务活动的描述,倘若是相关的语义,可以作为归类的特征。例如,对于阅读作品场景,查询作品、收藏作品、分享作品、阅读作品和购买作品都具有“作品”相关语义,基于这一特征,我们可以考虑将这些业务活动归为同一类。如果存在领域概念不是清晰的无歧义的,则认为语义不相关。
在进行语义相关性判断时,还需要注意业务活动之间可能存在不同的语义相关性。例如,对于阅读作品场景,查询作品、收藏作品、分享作品、阅读作品和购买作品都具有“作品”相关语义,而评价作品与评价作者既具有“作品”相关语义,又具有“评价”相关语义,究竟应该以哪个语义为准呢?没有标准!我们只能按照相关性的耦合程度进行判断。如果我们将评价视为一个相对独立的服务,则评价作品与评价作者放入评价服务会更好。
功能相关性
从功能角度去分析业务活动是否彼此关联和依赖,倘若存在关联和依赖,可以作为归类的特征,这种关联性,代表了功能之间的相关性。倘若两个功能必须同时存在,又或者缺少一个功能,另一个功能是不完整的,则二者就是功能强相关的。我们就可以通过用例之间的关系来判别功能相关性,如用例的包含与扩展关系,其中包含关系展现了功能的强相关性。
两个强相关的功能,通常应该属于同一服务。仍以前面提到的阅读作品场景为例。发布作品与验证作品内容是功能相关的,且属于用例的包含关系,因为如果没有对发布的作品内容进行验证,就不允许发布作品。但也有例外,例如,对于下订单用例来说,必须包含支付订单费用用例,而支付功能已经作为支撑的公开服务被调用,则支付订单费用用例从订单服务迁移到支付服务。
除过功能强相关外,对于相同生命周期和相同变化方向的功能也应属于同一个服务。
数据相关性
对于数据有强一致性约束的两个功能应尽可能的归于同一个服务,尽量不引入分布式事务的复杂性,而对于数据有弱一致性约束的两个功能,可以拆分到不同的服务中去。
工作边界
一个理想的开发团队规模最好能符合亚马逊CEO贝索斯提出的两个披萨原则(The two pizza principle),他认为如果两个披萨不足以喂饱一个团队,那么这个团队可能就显得太大了。大体而言,就是将团队成员人数控制在6~10人左右。为何要保证这样的规模呢?因为人数过多的会议将不利于决策的形成,而让一个小团队在一起开会讨论,则更有利于达成共识,并能够有效促进企业内部的创新。
small-team.png
开发团队在组织时,应以特性团队为主,组件团队为辅。组件团队是有价值的,因为存在一些具有相当门槛的专有功能,需要具备专门知识或能够应对相关技术复杂度的团队成员去解决。一般情况下,我们说的团队专指特性团队。
康威定律认为:“任何组织在设计一套系统时,所交付的设计方案在结构上都与该组织的组织结构(沟通结构)保持一致。” 在康威定律中起到关键杠杆作用的是沟通成本。如果同一个服务的工作交给两个团队分工完成,为了合力解决问题,就必然需要这两个团队进行密切的沟通。然而,团队间的沟通成本显然要高于团队内的沟通成本。
如果一个服务过大,导致一个团队吃不下,则需要继续拆分,直到一个团队能完全驾驭为止。
站在工作边界的角度,服务之间的协作关系就是团队之间的协作关系,常见的协作关系如下:
- 合作关系(Partnership):两个团队要么一起成功,要么一起失败,是一种“反模式”,可能存在强耦合关系,甚至是糟糕的双向依赖,罪魁祸首是因为职责分配不当;
- 客户方-供应方开发(Customer-Supplier Development):是团队合作中最为常见的合作模式,体现的是上游(供应方)与下游(客户方)的合作关系;
- 遵奉者(Conformist):当需求的控制权发生了逆转,由上游团队来决定是响应还是拒绝下游团队提出的请求时,所谓的“遵奉者”模式就产生了,是一种“反模式,同时还有一层意思是下游限界上下文对上游限界上下文模型的追随;
- 分离方式(Separate Ways):两个团队没有协作关系,是一种最好的关系,可以独立变化而互相不产生影响。
技术边界
技术边界封装了技术方案的实现,避免不同的技术方案选择互相干扰导致架构的混乱。
质量属性
这里的质量属性主要针对服务运行期,而非服务开发期,常见的质量属性包括高并发、实时性、高可用和高性能等。
在大促期间,数亿用户高并发访问电商系统。如果我们将订单业务从整个系统中剥离出来,作为一个单独的服务对其进行设计,就可以从物理架构上保证它的独立性,在资源分配上做到高优先级地扩展,在针对领域进行设计时,尽可能地引入异步化与并行化,来提高服务的响应能力。
在电商系统中,商品自然是核心,而价格(Price)则是商品概念的一个重要属性。倘若仅仅从业务的角度考虑,在进行领域建模时,价格仅仅是一个普通的领域值对象,可倘若该电商系统的商品数量达到数十亿种,每天获取商品信息的调用量在峰值达到数亿乃至数百亿次时,价格就不再是业务问题,而变成了技术问题。对价格的每一次变更都需要及时同步,真实地反馈给电商客户。
为了保证这种在高并发情况下的实时性,我们就需要专门针对价格领域提供特定的技术方案,例如,通过读写分离、引入 Redis 缓存、异步数据同步等设计方法。此时,价格领域将作为一个独立的服务,形成自己与众不同的架构方案,同时,为价格限界上下文提供专门的资源,并在服务设计上保证无状态,从而满足快速扩容的架构约束。
对于高可用和高性能,需要将业务功能按物理资源维度(CPU、内存和IO等)的亲和性进行拆分,以便服务的水平扩展。X轴扩展和Z轴扩展都需要多个实例,但X轴扩展是在多个实例之间实现请求的负载均衡,而Z轴扩展是每个实例仅负责数据的一个子集,根据请求的属性路由请求到相应的服务。
功能复用
Eric Evans 总结的上下文映射(Context Map)中,共享内核(Shared Kernel)是解除不必要依赖实现功能复用的重要手段,往往被用来解决合作关系引入的问题,仍然属于领域的一部分,它不是横切关注点,也不是公共的基础设施,属于非核心子领域。
管理变化
Eric Evans 总结的上下文映射(Context Map)中,防腐层和开放主机服务模式是用来管理变化的:
- 防腐层(Anticorruption Layer):往往属于下游限界上下文,是下游限界上下文对抗上游变化的利器,用以隔绝上游限界上下文可能发生的变化,是对变化的被动应对模式;
- 开放主机服务(Open Host Service):往往属于上游限界上下文,是上游服务用来吸引更多下游调用者的诱饵,是一种承诺,保证开放的服务不会轻易做出变化,是对变化的主动应对模式。
开放主机服务常常与发布语言(Published Language)模式结合起来使用。当然,在定义这样的公开服务时,为了被更多调用者使用,要力求语言的标准化,在分布式系统中,通常采用 RPC(Protocol Buffer)、RESTful 或 发布/订阅(Pub/Sub)。如果使用消息队列中间件,则需要事先定义消息的格式。服务间最好通过发布/订阅领域事件来进行交互,这样可以让服务保持松耦合。
第三方服务集成
一个电商系统需要支持多种常见的支付渠道,如微信支付、支付宝、中国银联以及各大主要银行的支付。
电商系统需要与这些第三方支付系统进行集成。不同的支付系统公开的API并不相同,认证、加密以及支付流程也不尽相同。
在技术实现上,一方面我们希望为支付服务的客户端提供完全统一的支付接口,以保证调用上的便利性与一致性,另一方面我们希望能解除第三方支付服务与电商系统内部模块之间的耦合,避免引起“供应商锁定(Vender Lock)”,也能更好地应对第三方支付服务的变化。因此,我们需要将这种集成划分为一个单独的服务。
遗留系统
我们一般将遗留系统作为一个单独的服务来处理,后续通过绞杀者模式,逐步完成绞杀。
strangler-mode.png
小结
对于服务拆分,一般有按业务能力拆分和按限界上下文拆分两种方式。微服务和DDD简直就是天生一对,我们推荐按限界上下文拆分服务。
我们认为服务和限界上下文是对等的,服务有三重边界:业务边界、工作边界和技术边界。我们在战略设计阶段,如果对识别出来的服务的准确性还心存疑虑,那么比较实际的做法是让服务保持一定的粗粒度。倘若觉得功能的边界不好把握分寸,可以考虑将这些模棱两可的功能放在同一个服务。待到该服务变得越来越庞大,以至于一个2PTs团队无法完成交付目标,又或者该服务的功能有不同的质量属性要求,要么就是因为重用或变化,使得我们能够更清楚地看到分解的必要性,此时我们再对该服务实施进一步拆分,就会更加有把握。
当我们给服务命名或绘制服务的协作关系时,如果存在挑战或发现了坏味道,则应该重新审视服务的边界,从而演进和精炼。
网友评论