Skip to content

微服务拆分和设计原则

虽然DDD的设计方法很好,可以很好地指导中台领域建模和微服务设计,但由于企业发展历程以及企业技术和文化的不同,DDD和微服务的实施策略也会略有差异。

## 微服务的演进策略

如何实现从单体应用向微服务演进?ThoughtWorks提出了两种演进策略:"绞杀者策略"和"修缮者策略"。其实,还有一种"另起炉灶"的策略,不过这种策略一般不太推荐。

  1. 绞杀者策略

绞杀者策略是一种逐步剥离业务能力,用微服务逐步替代原有单体应用的策略。

它对单体应用进行领域建模,根据领域边界,在单体应用之外,将新功能和部分业务能力独立出来,建设独立的微服务。新微服务与单体应用之间保持松耦合关系,两者只通过服务或异步化的数据进行业务关联。随着时间的推移,大部分单体应用的功能就会被独立为微服务,这样就慢慢"绞杀"了原来的单体应用。

绞杀者策略类似建筑拆迁,在完成新建筑物建设和搬迁后,拆除原来的旧建筑物。

  1. 修缮者策略

修缮者策略是一种维持原有系统整体能力不变,通过优化局部以提升系统整体能力的策略。

它是在现有系统的基础上,剥离影响整体业务的部分功能,独立为微服务。比如有高性能要求的功能,代码质量不高或者版本发布频率不一致的功能等。通过这些功能的剥离,我们可以兼顾整体和局部,解决系统整体不协调的问题。

修缮者策略类似古建筑修复,将存在问题的部分功能重建或者修复后,重新加入原有的建筑中,保持建筑原貌和功能不变。一般人从外表感觉不到这个变化,但是建筑物的质量却得到了很大的提升。

  1. 另起炉灶策略

另起炉灶策略,顾名思义就是将原有的系统推倒重做。

在微服务建设期间,原有单体系统照常运行,一般会停止接收和开发新需求。而新系统则会组织新的项目团队,按照原有系统的功能域,重构领域模型,开发新的微服务。在完成数据迁移后,进行新旧系统切换。

对于大型核心应用一般不建议采用这种策略。这是因为系统重构后的不稳定,大量未知的潜在技术风险,新的开发模式下项目团队磨合等不确定性因素,会导致项目实施难度大大增加。

不同场景下的微服务建设策略

企业内情况千差万别,发展历程也不一样,有遗留单体系统的微服务改造,也有全新未知领域的业务建模和系统设计,还有遗留系统局部优化的情况。在不同场景下,领域建模的策略也会有差异。

下面我们就分几类场景来看看如何完成领域建模和微服务演进。

### 新建系统

新建系统又分为简单和复杂领域建模两种场景。

  1. 简单领域建模

简单的业务领域,一个领域就是一个小的子域。在这个小的问题域内,领域建模过程相对简单,直接采用事件风暴的方法进行业务场景分析,提取领域对象,找出服务和聚合,建立领域模型,完成微服务设计即可。

  1. 复杂领域建模

对于复杂的业务领域,可能需要将领域多级拆分后才能开始领域建模。领域需要拆分为子域,甚至子域还需要进一步拆分。比如:保险领域可以拆分为承保、理赔、收付费和再保等子域,承保子域还可以拆分为投保、保单管理等子子域。

复杂领域如果不做进一步的领域细分,由于问题域范围太大,领域建模的工程量会非常浩大。你不太容易在一个非常大的领域内通过事件风暴,完成领域建模,即使勉强完成,效果也不一定会好。

对于复杂领域,我们可以分三步来完成领域建模和微服务设计。

[第一步,拆分子域建立领域模型。]

根据业务领域的特点,参考流程节点边界或功能聚合模块等边界因素,结合领域专家的经验和项目团队的讨论结果,将领域逐级分解为大小合适的子域,然后针对子域采用事件风暴方法,划分聚合和限界上下文,初步确定子域内的领域模型。

[第二步,领域模型微调。]

梳理领域内所有子域的领域模型,对各子域领域模型进行微调。领域模型微调的过程重点考虑不同领域模型中聚合的重组。同步考虑领域模型之间的服务依赖关系,服务以及领域事件之间的依赖关系,确定最终的领域模型。

[第三步,微服务的设计和拆分。]

根据领域模型和微服务拆分原则,完成微服务的拆分和设计。

## 单体遗留系统

如果我们面对的单体遗留系统,从整体来说,要继续保持单体不变而只是将部分特定功能独立为微服务,比如将面临性能瓶颈的模块或代码质量比较差的模块拆分为微服务。我们可以将这一特定功能,理解为一个简单子领域,参考简单领域建模的方式就可以了。这种微服务演进策略采用的是"修缮者策略"。

如果单体遗留系统需要全部拆分为微服务,从整体上完成从单体向微服务架构的演进。可以根据具体的业务场景分析,采用以下两种领域建模方法。当单体应用功能模块非常多,业务范围非常大时,我们需要先将单体应用所对应的业务领域进行子域分解,当子域分解到大小合适,就可以用事件风暴完成领域模型构建。当单体应用业务领域大小正好适合开展事件风暴时,就不必分解子域,直接采用事件风暴构建领域模型即可。这种微服务演进策略采用的是"绞杀者策略"。

一般来说,项目团队对于单体遗留系统的业务模式和数据模型非常熟悉。在领域建模时,也可以采用事件风暴与单体系统数据模型相结合的方式,通过场景分析,快速从遗留系统的服务与数据模型中提取服务和领域对象,用更短的时间完成单体遗留系统的领域模型构建。

注意

在对微服务DO对象和PO对象设计时,要尽量兼顾微服务的数据模型与单体应用的数据模型,毕竟单体遗留系统中还有大量的历史数据,我们需要尽量降低新旧应用历史数据迁移或数据兼容而带来的复杂度。

其实,在某些业务场景下,不需要进行老单体应用与新微服务的数据迁移和应用切换。我们可以借鉴命令与查询职责分离设计模式(Command Query Responsibility Segregation,CQRS)。CQRS其实是一种读写分离设计模式。新微服务中只负责业务逻辑处理和业务数据变更,只存储微服务自身产生的业务数据,[专职提供数据新增和变更的写服务]{.yanse}。你可以单独建立一个CQRS查询库,存储查询必需的全量业务数据,这些数据包括老单体应用的历史数据和新微服务变更或新增后的数据,并基于查询库构建查询微服务,[专职提供数据查询的读服务]{.yanse}。当然,查询库的技术选型可以根据具体的技术或业务场景来选择,可以是分布式数据库,也可以是缓存或搜索引擎等。另外,查询库的数据模型可以根据具体的查询需求来设计,它的数据来源不必只局限于一个或两个应用,也可以专注于某一个查询主题域综合多个维度数据建立企业级查询数据模型。

对于新业务,微服务完成业务逻辑处理后,先完成本地数据写操作,然后通过领域事件驱动机制将数据复制到CQRS查询库。对于历史数据变更业务,微服务先从CQRS查询服务获取历史数据明细,然后在微服务内完成数据变更和写操作,再通过领域事件驱动机制将变更后的数据返回CQRS查询库。

在新微服务试运行时,如果老单体应用在构建CQRS的同时完成了读写分离改造,那么新老两套应用就可以基于同一个CQRS查询库完成数据查询逻辑,而各自又可以互不影响地并行独立完成写的业务处理逻辑。新老应用变更后的数据会通过领域事件驱动机制集中到同一个CQRS查询库。当微服务出现不可用时,老单体应用仍然可以不受影响地受理新业务,还能基于统一的查询库保证新老应用数据查询结果的一致性。这样,既实现了业务处理逻辑与数据查询逻辑的分离,又解耦了老单体应用与新微服务的服务和数据依赖,可以避免新微服务上线试运行带来的不确定性影响,保证业务的连续性。当微服务运行完全稳定后,老单体应用就可以无感地完成历史使命成功下线了。

经过CQRS改造后,你不再需要进行微服务和单体应用的数据模型适配,也省去了从单体应用向微服务的历史数据迁移和新旧应用切换的过程,从而避免了单体应用数据迁移对新的微服务运行的影响。而最关键的是:你再也不需要熬通宵来做应用切换和数据迁移了!

另外,在微服务与传统单体应用集成时,我们还要考虑新老系统之间服务和业务逻辑的兼容,必要时可引入防腐层,将微服务中需要与单体遗留应用交互的领域模型之外的业务逻辑放在防腐层实现,避免污染新构建的领域模型。当新旧应用完成过渡后,就可以抛弃防腐层代码了。

[]

## DDD使用误区

很多人在接触微服务后,但凡是系统,一概都想设计成微服务架构。其实有些业务场景,单体架构的开发成本可能会更低,开发效率会更高,采用单体架构也不失为好的选择。同样,虽然DDD很好,但有些传统设计方法在微服务设计时依然有它的用武之地。

下面我们就来聊聊DDD使用过程中常见的几个误区。

  1. 所有领域都用DDD方法设计

很多人在学会DDD后,可能会不加区分地将其用在所有业务领域,即在全部业务领域使用DDD来设计。DDD从战略设计到战术设计,是一个相对复杂的过程。首先,企业内要培养DDD的文化。其次,对团队成员的设计和技术能力要求相对较高。

在资源有限的情况下,应先聚焦核心子域。所以建议你先从富领域模型的核心子域开始,而不必一下就在全业务领域推开。待企业内具备了全员实施DDD条件,培养了DDD的团队文化后,再进行全面推广。

  1. 全部采用DDD战术设计方法

不同的设计方法有不同的适用场景和环境,我们应该将它用在它最擅长的业务场景中。

DDD有很多概念和战术设计方法,比如聚合根和值对象等。聚合根利用仓储管理聚合内实体数据之间的一致性,这种方法对于管理新建和修改数据非常有效,比如在修改订单数据时,它可以保证订单总金额与所有商品明细金额的一致。

但聚合根并不擅长复杂的联表查询场景,对于量较大的数据查询处理,甚至有延迟加载进而影响效率的问题。而传统的设计方法,可能用一条简单的SQL语句就可以很快解决。

在很多贫领域模型的业务场景,比如数据统计和分析,DDD的很多方法可能都用不上,或用得并不顺手,而采用传统的方法很容易就解决了。

因此,在遵守领域边界和微服务分层等大原则下,在进行战术层面设计时,我们应该更灵活地考虑选择多种方法,不只采用DDD设计方法,传统设计方法也应该在选项内。具体要结合企业情况,以快速、高效解决实际问题为目标,不要为做DDD而做DDD。

  1. 重战术而轻战略

很多DDD初学者学习DDD的主要目的,可能主要是为了设计和开发微服务,因此更看重DDD的战术设计实现。殊不知DDD是一种从领域建模到微服务落地的全方位的解决方案,领域模型与微服务之间是一种强关联关系。

DDD战略设计时构建的领域模型,是微服务设计和开发的输入。领域模型确定了微服务的限界上下文边界,以及聚合、聚合根、实体、值对象和领域服务等领域对象。领域模型边界划分得清不清晰,领域对象定义得明不明确,会决定微服务的设计和开发质量。

没有领域模型的输入,基于DDD的微服务设计和开发将无从谈起。因此我们不仅要重视战术设计,更要重视战略设计。

  1. DDD只适用于微服务

DDD是在微服务出现后才真正火爆起来的,很多人会认为DDD只适用于微服务,其实在DDD沉默的十多年里,它也一直被应用在单体应用的设计中。

很多企业可能有迫切的数字化转型的愿望,但是苦于没有实施微服务的条件,如云计算平台、DevOps工具等,这些技术能够提高企业自动化运维能力和打造微服务运营的平台。其实这种情况下,DDD正好可以发挥它的威力。

在项目推进时,我们可以用DDD做好应用的边界设计,虽然这时应用表现为单体形态,但是在这些单体的内部却有着清晰的聚合边界,层与层之间也都进了解耦,整个设计和开发完全遵守了DDD的解耦设计原则。

一旦企业具备了微服务的实施条件,这些边界清晰的单体就可以很容易地根据限界上下文和聚合的边界拆分为微服务,而这个拆分过程会比耦合度很高的传统单体的拆分过程容易得多,成本也会小得多。

综上,在具体项目实施时,我们要理解DDD的核心设计思想和理念,结合企业具体的业务场景和团队技术特点,组合多种设计方法,灵活运用,选择适合自己的正确方法解决实际问题。

[]

## 微服务设计原则

微服务设计原则中,如高内聚低耦合、复用、单一职责等这些常见的设计原则在此就不再赘述了。这里主要强调下面几条。

[第一条,要领域驱动设计,而不是数据驱动设计,也不是界面驱动设计。]

微服务设计应先建立领域模型,在确定了逻辑和物理边界,提取了领域对象,并建立了领域对象之间的依赖关系后,才开始微服务的拆分和设计。

不是先定义数据模型和库表结构,也不是前端界面需要什么,就去调整核心领域逻辑代码。在设计时,应将外部需求变化从用户接口层到应用层和领域层逐级消化,尽量降低前端需求对领域层核心领域逻辑的影响。

[第二条,要边界清晰的微服务,而不是分布式小单体。]

微服务上线后,其功能和代码也不是一成不变的。随着需求或设计变化,领域模型会迭代演进,微服务的代码也会分分合合。边界清晰的微服务,可快速实现微服务代码的重组。

微服务内聚合之间的领域服务和数据库实体原则上应杜绝相互依赖。你可将聚合之间的调用上升到应用层,通过应用服务对领域服务进行编排或者采用领域事件驱动,实现聚合之间的解耦,以便微服务的架构演进。

[第三条,微服务分层要职能清晰,而不是依赖混乱的"小泥球"。]

分层架构中各层职能定位清晰,且都只能与其下方的层发生依赖,即只能从外层调用内层服务,内层服务通过封装、组合或编排对外逐层暴露,服务粒度也会由细到粗。

应用层负责服务的组合和编排,不应有太多核心业务逻辑。其中,领域层负责核心领域业务逻辑的实现。各层应各司其职,职责边界不要混乱。

在服务演进时,应尽量将可复用的能力向下层沉淀。

[第四条,要做自己能掌控的微服务,而不是过度拆分的微服务。]

微服务过度拆分必然会带来软件维护成本的上升,比如集成成本、运维成本、监控和定位问题的成本。企业在微服务转型过程中还需要有云计算、DevOps、自动化监控等能力,而一般企业很难在短时间内提升这些能力,如果项目团队没有这些能力,将很难掌控这些微服务。

如果在微服务设计之初按照DDD的战略设计方法,定义好了微服务内的逻辑边界,做好了架构的分层,其实我们不必拆分太多的微服务,即使是单体也未尝不可。

随着技术积累和能力提升,当我们有了这些能力后,由于应用内有清晰的逻辑边界,我们可以随时轻松地重组出新的微服务,而这个过程不会花费太多的时间和精力。

[]

## 微服务拆分要考虑哪些因素

理论上,一个限界上下文内的领域模型可以被设计为微服务,但是由于领域建模主要从业务视角出发,没有考虑非业务的因素,比如需求变更频率、高性能、安全、团队以及技术异构等因素。这些非业务因素对于领域模型的系统落地也会起到决定性作用,因此在微服务拆分时也需要重点考虑它们。下面列出几个主要因素,供你参考。

  1. 基于领域模型

基于领域模型,也就是按照限界上下文边界进行拆分,围绕业务领域边界按职责单一性、功能完整性进行微服务拆分。

  1. 基于业务需求变化频率的不同

你需要识别领域模型中业务需求变动频繁的功能,考虑业务变更频率与相关度,将业务需求变动较高和功能相对稳定的业务进行分离。

这是因为需求的经常性变动必然会导致代码的频繁修改和版本发布。这种分离可以有效降低频繁发布版本的业务对不需要经常发布版本的业务的影响。

  1. 基于应用性能的要求不同

你需要识别领域模型中性能压力较大的业务。因为对性能指标要求高的业务在资源需求上要求会比其他业务高,这样可能会拖累其他业务,也会造成资源无谓的浪费。为了降低对应用整体性能和资源要求的影响,我们可以将对性能方面有较高要求的业务与对性能要求不高的业务进行拆分。

  1. 基于组织架构和团队规模

除非有意识地优化组织架构,否则微服务的拆分应尽量避免对团队和组织架构的调整,避免由于功能的重新划分,而增加大量且不必要的团队之间的沟通成本。

拆分后的微服务项目团队规模保持在10~12人为宜。在进行微服务拆分和组建项目团队时,应尽量将沟通边界控制在小团队内。

  1. 基于安全边界的不同

对于有特殊安全要求的业务,应从领域模型中拆分、独立出来。避免因为不同的安全要求,而带来不必要的成本,或带来泄密的风险。

  1. 基于技术异构等因素

领域模型中有些功能虽然在同一个业务领域内,但由于各种条件的限制,在技术实现时可能会存在较大的差异,也就是说领域模型内部不同的功能存在技术异构的问题。

由于业务场景或者技术条件的限制,有的可能采用.NET语言,有的则采用Java语言,甚至有的采用大数据技术架构。

对于这些存在技术异构的功能,你可以考虑按照技术栈的边界进行拆分。

综上,建立领域模型后,我们还需要考虑以上影响微服务拆分的非业务因素。说了这么多,需要注意一点:这些拆分都是以领域模型的聚合为单位拆分的。

在DDD里,聚合是可以拆分为微服务的最小单位,所以我们在领域建模时一定要把握好聚合这个逻辑边界,它随时可能会发挥出让你意想不到的威力。

小结

相信你在微服务落地的时候会有很多收获和感悟。

对于DDD和微服务,我想总结的就是:深刻理解DDD的设计思想和内涵,把握好边界和分层这个大原则,结合企业文化和技术特点,灵活运用战术设计方法,选择最适合的技术和方法解决实际问题,切勿为了DDD而做DDD!

Released under the MIT License.