如何实现微服务的架构演进
在进行微服务设计时,我们可以通过事件风暴来确定领域模型的限界上下文边界,划定微服务边界,定义业务和系统运行边界,从而保证微服务的单一职责和随需而变的架构演进能力。
重点落到边界时,微服务的设计会涉及逻辑边界、物理边界和代码边界等。
那么这些边界在微服务架构演进中到底起到什么样的作用?我们又该如何理解这些边界呢?
演进式架构
在微服务设计和实施的过程中,可能会有很多人认为:"将单体拆分成多少个微服务,是微服务的设计重点。"
可事实真是这样吗?其实并非如此!
Martin Fowler在提出微服务时,他提到了微服务的一个重要特征:演进式架构。
演进式架构以支持增量的、非破坏的变更作为第一原则,同时支持在应用程序结构层面的多维度变化。
那如何判断微服务设计是否合理呢?其实很简单,你只需要看它是否满足这样的情形就可以了。即随着业务的发展或需求变更,在领域模型和微服务不断被重新拆分,或者组合成新的微服务过程中,不会大幅增加软件开发和维护的成本,并且这个架构演进的过程是非常轻松和简单的。这才是微服务设计的重点,也是企业在微服务设计时最应该关心的问题。
微服务设计是否合理,我们关键要看它能否支持微服务架构的长期、轻松的演进。毕竟,我们不能每次遇到大的业务模式变化时都去推倒重做!
我们设计的是微服务还是小单体
有些项目团队在将集中式单体应用拆分为微服务时,并不是先建立领域模型,而是按照业务功能将原来的单体应用,从一个软件包拆分成多个所谓的"微服务"软件包。这些"微服务"内的代码仍然采用三层架构的设计模式,即这些代码依然高度耦合,逻辑边界不清晰,我们暂且称它为"小单体微服务"。
在从单体向微服务演进的过程中,我们是需要边界清晰的微服务呢?还是需要很多很多的小单体微服务呢?
随着新需求的提出和业务的发展,这些"小单体微服务"会慢慢膨胀起来。
当有一天这些膨胀了的小单体有一部分业务功能需要拆分出去,或者部分功能需要与其他微服务进行重组时,你会发现这些看似边界清晰的微服务,不知不觉已经变成了一个"臃肿油腻"的大单体了。在这个单体内,代码依然是高度耦合且边界不清晰的。这个时候你又需要一遍又一遍地,重复着从大单体向小单体的重构过程。想想,这个代价是不是有点高了呢?
其实问题已经很明显了,那就是边界不清晰。这种单体式微服务只是定义了一个维度的边界,就是微服务之间的物理边界。虽然我们对它进行了分布式技术架构的升级,给它披上了一件微服务架构的外衣,但本质上它依然停留在单体架构的设计思维上。微服务设计时要考虑的,不仅仅只有这一层微服务之间的物理边界,还需要定义好微服务内的逻辑边界和代码边界,这样才能得到你想要的结果。
现在你知道了,我们要避免将微服务设计为小单体。那应该如何设计才能避免将微服务设计成小单体呢?清晰的边界人人都想要,可是究竟应该如何实现呢?DDD已然给出了答案。
用DDD方法设计的微服务,不仅可以通过限界上下文和聚合,实现微服务内外的解耦,同时也可以很容易地实现微服务积木式模块化的重组,支持微服务的架构演进。
## 微服务边界的作用
你应该还记得DDD方法里的限界上下文和聚合吧?它们就是用来定义领域模型和微服务边界的。我们再来回顾一下DDD的设计过程。
在领域建模时,我们会梳理出业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出实体等领域对象,再根据实体对象之间的业务关联性,将业务紧密相关的多个实体进行组合形成聚合。这里聚合之间就形成了第一层边界。然后根据业务及语义边界等因素将一个或者多个聚合划定在一个限界上下文内,构建领域模型。这里限界上下文之间的边界就形成了第二层边界。
为了方便理解,我们将这些边界分为:逻辑边界、物理边界和代码边界。
- 逻辑边界
逻辑边界主要定义同一业务领域或微服务内,紧密依赖的领域对象所组成的不同聚合之间的边界。微服务内聚合的边界就是逻辑边界。一般来说微服务会有一个以上的聚合。在开发时,不同聚合的代码会被隔离在不同的聚合代码目录中。
聚合之间的逻辑边界,在微服务设计和架构演进中具有非常重要的意义!
微服务架构的演进并不是随心所欲的,也需要遵循一定的规则,这个规则就是逻辑边界。当业务模型发生大的变化时,在业务端可以以聚合为单位进行领域模型的重组,在应用端可以以聚合的代码目录为单位,进行微服务代码的重构。
由于按照DDD方法设计的微服务逻辑边界清晰,业务高内聚,聚合之间松耦合,聚合之间代码目录结构边界清晰,因此在领域模型和微服务代码重构时,我们就不需要花费太多的时间和精力了。
随着业务的快速发展,如果微服务需要将部分高频的业务能力独立出去,我们就可以以这段业务逻辑所在的聚合为单位,将聚合目录下的所有代码整体独立拆分为一个新的微服务。这样是不是就很容易地完成了微服务的拆分呢?
另外,我们也可以对多个微服务,存在相似功能的聚合进行功能和代码重组,组合为新的聚合或微服务,独立为通用的微服务。这个功能沉淀的过程是不是有点做中台的感觉呢?
- 物理边界
物理边界主要是从部署和运行的视角来定义微服务之间的边界。不同微服务部署位置和运行环境相互物理隔离,分别运行在不同的JVM中。这种边界有别于同一个微服务内聚合之间的逻辑边界,不同微服务可以独立开发、测试、构建和部署,它们在物理上是相互独立的。
为了实现微服务的解耦,应尽量减少微服务之间的同步服务调用,优先采用领域事件驱动(DDD)机制,采用数据最终一致性。
- 代码边界
代码边界主要用于微服务内不同职能代码之间的隔离。
在微服务开发过程中,我们根据代码模型建立相应的微服务代码目录,实现不同功能代码边界的隔离。由于领域模型与代码模型的映射关系,代码边界会直接体现为业务边界。代码边界可以控制代码重组的影响范围,避免业务和服务之间的相互影响。
我们在进行微服务代码目录设计时,已经根据聚合这个逻辑边界进行代码结构边界的设计,将同一个聚合的领域层核心业务逻辑代码与基础资源逻辑代码控制在一个聚合代码目录中。当领域模型出现变化,微服务需要进行功能重组时,我们只需要以聚合代码目录为单位进行代码重组就可以了。由于聚合内部高内聚,聚合之间低耦合的特点,在进行聚合重组时,就不需要考虑聚合之间的代码和服务解耦了,因此微服务的功能和代码演进过程会非常容易。
正确理解微服务的边界
从上述内容中我们了解到,按照DDD设计的微服务逻辑边界和代码边界可以简化微服务架构演进过程。
微服务的拆分可以参考领域模型的限界上下文,也可以参考聚合逻辑边界。因为聚合是可以拆分为微服务的最小业务单元。那么,实施过程是否一定要做到逻辑边界与物理边界一致呢?或者说,聚合是否也一定要拆分成微服务呢?
答案是不一定,这里要考虑微服务过度拆分的问题。
微服务的过度拆分会使软件维护成本上升,比如:集成成本、版本发布成本、运维成本以及监控和定位问题的成本等。
在项目建设初期,如果不具备微服务的服务治理和运维能力,不宜将应用拆分得过细,我们甚至可以按照DDD方法将它设计为单体应用。由于按照DDD方法设计的单体应用的逻辑和代码边界非常清晰,在企业具备微服务运行的条件以后,我们可以随时根据需要将单体应用按照限界上下文边界组合聚合,并拆分为新的微服务,完成从逻辑边界到物理边界的拆分,实现微服务架构的演进。
当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖,也需要符合"高内聚,松耦合"设计原则和开发规范,否则你也不能很快完成微服务架构演进。
小结
微服务的边界在架构演进中有非常重要的作用,我们可以将这些边界分为三类。
▪逻辑边界:微服务内聚合之间的边界是逻辑边界。它是一个虚拟的边界,强调业务的内聚性,可根据需要拆分为物理边界。也就是说聚合也可以独立为微服务,但不建议过度拆分。
▪物理边界:微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。
▪代码边界:不同层或者聚合之间代码目录的边界是代码边界。它强调的是不同职责代码之间的隔离,方便架构演进时代码的重组和不同层的解耦。
通过定义上述边界,我们可以实现业务的高内聚和代码的松耦合。
清晰的边界,可以快速实现微服务代码的重组,轻松实现微服务的架构演进。但要记住一点:在从单体应用向微服务架构演进时,我们需要的是边界清晰的微服务,而不是从一个大单体向多个分布式小单体的演进。