15 | 边界:微服务的各种边界在架构演进中的作用?

你好,我是欧创新。

前几讲我们已经介绍过了,在用DDD进行微服务设计时,我们可以通过事件风暴来确定领域模型边界,划定微服务边界,定义业务和系统运行边界,从而保证微服务的单一职责和随需而变的架构演进能力。

那重点落到边界的时候,总结一下就是,微服务的设计要涉及到逻辑边界、物理边界和代码边界等等。

那么这些边界在微服务架构演进中到底起到什么样的作用?我们又该如何理解这些边界呢?这就是我们今天重点要解决的问题。

演进式架构

在微服务设计和实施的过程中,很多人认为:“将单体拆分成多少个微服务,是微服务的设计重点。”可事实真的是这样吗?其实并非如此!

Martin Fowler 在提出微服务时,他提到了微服务的一个重要特征——演进式架构。那什么是演进式架构呢?演进式架构就是以支持增量的、非破坏的变更作为第一原则,同时支持在应用程序结构层面的多维度变化。

那如何判断微服务设计是否合理呢?其实很简单,只需要看它是否满足这样的情形就可以了:随着业务的发展或需求的变更,在不断重新拆分或者组合成新的微服务的过程中,不会大幅增加软件开发和维护的成本,并且这个架构演进的过程是非常轻松、简单的。

这也是微服务设计的重点,就是看微服务设计是否能够支持架构长期、轻松的演进。

那用DDD方法设计的微服务,不仅可以通过限界上下文和聚合实现微服务内外的解耦,同时也可以很容易地实现业务功能积木式模块的重组和更新,从而实现架构演进。

微服务还是小单体?

有些项目团队在将集中式单体应用拆分为微服务时,首先进行的往往不是建立领域模型,而只是按照业务功能将原来单体应用的一个软件包拆分成多个所谓的“微服务”软件包,而这些“微服务”内的代码仍然是集中式三层架构的模式,“微服务”内的代码高度耦合,逻辑边界不清晰,这里我们暂且称它为“小单体微服务”。

下面这张图也很好地展示了这个过程。

而随着新需求的提出和业务的发展,这些小单体微服务会慢慢膨胀起来。当有一天你发现这些膨胀了的微服务,有一部分业务功能需要拆分出去,或者部分功能需要与其它微服务进行重组时,你会发现原来这些看似清晰的微服务,不知不觉已经摇身一变,变成了臃肿油腻的大单体了,而这个大单体内的代码依然是高度耦合且边界不清的。

“辛辛苦苦好多年,一夜回到解放前啊!”这个时候你就需要一遍又一遍地重复着从大单体向单体微服务重构的过程。想想,这个代价是不是有点高了呢?

其实这个问题已经很明显了,那就是边界。

这种单体式微服务只定义了一个维度的边界,也就是微服务之间的物理边界,本质上还是单体架构模式。微服务设计时要考虑的不仅仅只有这一个边界,别忘了还要定义好微服务内的逻辑边界和代码边界,这样才能得到你想要的结果。

那现在你知道了,我们一定要避免将微服务设计为小单体微服务,那具体该如何避免呢?清晰的边界人人想要,可该如何保证呢?DDD已然给出了答案。

微服务边界的作用

你应该还记得DDD设计方法里的限界上下文和聚合吧?它们就是用来定义领域模型和微服务边界的。

我们再来回顾一下DDD的设计过程。

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

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

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

逻辑边界在微服务设计和架构演进中具有非常重要的意义!

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

现在我们来看一个微服务实例,在下面这张图中,我们可以看到微服务里包含了两个聚合的业务逻辑,两个聚合分别内聚了各自不同的业务能力,聚合内的代码分别归到了不同的聚合目录下。

那随着业务的快速发展,如果某一个微服务遇到了高性能挑战,需要将部分业务能力独立出去,我们就可以以聚合为单位,将聚合代码拆分独立为一个新的微服务,这样就可以很容易地实现微服务的拆分。

另外,我们也可以对多个微服务内有相似功能的聚合进行功能和代码重组,组合为新的聚合和微服务,独立为通用微服务。现在你是不是有点做中台的感觉呢?

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

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

正确理解微服务的边界

从上述内容中,我们知道了,按照DDD设计出来的逻辑边界和代码边界,让微服务架构演进变得不那么费劲了。

微服务的拆分可以参考领域模型,也可以参考聚合,因为聚合是可以拆分为微服务的最小单位的。但实施过程是否一定要做到逻辑边界与物理边界一致性呢?也就是说聚合是否也一定要设计成微服务呢?答案是不一定的,这里就涉及到微服务过度拆分的问题了。

微服务的过度拆分会使软件维护成本上升,比如:集成成本、发布成本、运维成本以及监控和定位问题的成本等。在项目建设初期,如果你不具备较强的微服务管理能力,那就不宜将微服务拆分过细。当我们具备一定的能力以后,且微服务内部的逻辑和代码边界也很清晰,你就可以随时根据需要,拆分出新的微服务,实现微服务的架构演进了。

当然,还要记住一点,微服务内聚合之间的服务调用和数据依赖需要符合高内聚松耦合的设计原则和开发规范,否则你也不能很快完成微服务的架构演进。

总结

今天我们主要讨论了微服务架构设计中的各种边界在架构演进中的作用。

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

物理边界:微服务之间的边界是物理边界。它强调微服务部署和运行的隔离,关注微服务的服务调用、容错和运行等。

代码边界:不同层或者聚合之间代码目录的边界是代码边界。它强调的是代码之间的隔离,方便架构演进时代码的重组。

通过以上边界,我们可以让业务能力高内聚、代码松耦合,且清晰的边界,可以快速实现微服务代码的拆分和组合,轻松实现微服务架构演进。但有一点一定要格外注意,边界清晰的微服务,不是大单体向小单体的演进。

思考题

分享一下你们公司目前采用了什么样的方法来实现微服务的架构演进?和DDD设计方法相比,有何区别,你觉得哪种方式更好呢?可结合业务场景进行分析。

期待你的分享,你可以在留言区中畅所欲言,我们一同交流!

精选留言

  • 清涧飞鸟

    2020-05-28 09:20:52

    老师,这节课程看了好几次了,但是对于几个概念还是有点朦胧,您帮我判断下以下我的几个说法:
    1)一个聚合可以看作为子域
    2)一个限界上下文可以作为子域
    3)聚合是最小的边界
    4)限界上下文内可以包含一个或多个聚合
    5)领域模型是一个子域、一个限界上下文、一个聚合??或者都可以,只要可独立为微服务的就可以叫一个领域模型.
    作者回复

    在DDD中如果领域很大的话,内部由小到大的顺序大概是这样的:值对象-》实体-》聚合(最小业务功能单元,如果非常必要可以拆分为微服务)-》限界上下文(一般作为拆分为微服务的依据,包含一到多个聚合)-》子域(包含一到多个限界上下文)-》领域(包含支撑子域、核心子域或通用子域等一到多个子域)。
    在有些情况下,一个子域可能就是一个限界上下文,一个限界上下文可能只包含一个聚合。一般来说,在一个限界上下内来构建领域模型,一个领域模型可以设计出一个微服务。
    服务的从低向上关系是这样的:实体方法-》领域服务-》应用服务-》Facade接口。
    不知道是否清楚了?

    2020-05-28 15:15:17

  • 密码123456

    2019-11-18 07:57:09

    之前2个月前,要做微服务,当时不了解是啥,就感觉好厉害。然后按照业务划分。最后做成的结果,就像文中说的。大单体变成小单体。,高度耦合。做的自己都快看不下去了。
    作者回复

    在没有用DDD之前,我们也是这么干的^_^。

    2019-11-18 09:16:36

  • 开心小毛

    2020-05-09 10:53:44

    请问老师,为什么再给出的例子中会出现某某工厂类(leaveFactory)呢?
    是用来从数据库中读取并创建leave实体的么?如果是的话,为何不在leaveDomainService类中加一个GetLeave方法呢,不知leaveFactory有何必要。
    是用来从数据库中插入并返回新建leave实体的么?我一直是通过DTO中的ID是否为零来判断是否为新建实体的,不知leaveFactory有何必要。
    谢谢老师解惑。
    作者回复

    看看这段话能不能解答你的问题。
    聚合中实体和值对象等DO对象的创建和持久化是聚合必不可少的操作。为了保证聚合内数据在修改时符合聚合业务规则要求,聚合内实体和值对象在创建和持久化时,需要遵循聚合统一的业务规则。
    对于实体和值对象比较多和依赖关系复杂的聚合,在DO对象创建时,需要确保聚合根和它依赖的对象实例同时被创建。如果将这项工作全部交由聚合根来实现,聚合根构造函数的逻辑将会非常复杂,聚合根也无法聚焦于自身的领域逻辑。
    为了让聚合根专注于表达领域模型,我们尽量将这些比较通用的,与领域模型业务逻辑无关的工作,从聚合根中剥离。将它们放到工厂(Factory)中实现,通过工厂封装聚合内复杂对象的创建过程,完成聚合根、实体和值对象等DO对象的创建。这样也可以隐藏聚合内DO对象的创建过程,避免暴露聚合的内部结构。
    聚合内DO对象创建的初始化数据来源于PO对象,PO对象是仓储从基础层获得的。在工厂服务中完成DO对象构建和从PO到DO对象的数据转换,完成DO对象的数据初始化。
    除了构建DO对象,在将DO对象持久化时,还会有从DO对象到PO对象的转换,这类通用的持久化对象的转换操作,也可以放在工厂实现。
    但并不是所有聚合对象的构造都需要用工厂来实现。如果聚合内领域对象简单,构造过程也并不复杂,你仍然可以用聚合根构造函数,完成聚合所有依赖对象的构建和数据初始化。

    2020-05-14 10:51:23

  • 秦伟

    2020-04-01 23:01:10

    老师,我们正在做组件化,公司架构师说只对核心领域用ddd设计,其他的还是安三成架构开发,这样可以吗?
    作者回复

    可以的,一步一步来。

    2020-04-02 11:24:59

  • alex

    2019-12-22 23:57:33

    欧总,你好,我在拆分微服务时遇到一个好纠结的问题,例如三个微服务,分别是入库微服务,出库微服务,库存微服务,而库存微服务主要提供两种能力,一是:查询库存,二是:更新库存数据,而入库服务与出库服务都会在自己的业务逻辑内调用库存服务的“查询库存”和“更新库存”方法,为了避开分布式事务的场景,我们现在用的方案是在入库,出库的操作时,通过接口形式调用库存服务的“查询库存”接口方法,但当要在入库或出库操作步骤中要更新库存数据时,我们为了保证入库操作与库存更新在同一个事务内执行,达到事务一致性,我们通过引用库存服务对应的jar包形式,在入库和出库的服务代码中调用库存服务模块的“更新库存”的service方法来实现的,请问对于这种场景,你们有没有遇到,如果有,那你们是如何处理的?请指教,谢谢。
    作者回复

    感觉你这种方案也没法解决数据一致性的问题。一般来说产生数据一致性的主要原因在数据库,也就是说一个写成功,另一个写失败。虽然JAR包和业务执行逻辑在一起了,但是由于数据库是分开的,在数据库层面还是无法保证数据的一致性的,所以虽然它们在一个jar包中,应该还是需要采用分布式事务的。
    我们没有遇到过你说的场景,但是这个场景应该在电商里面是非常常见的,比如查询商品数量,商品售出后,更新商品库存数据。

    2019-12-23 09:01:06

  • J.Smile

    2021-09-06 11:13:38

    DDD梳理流程:
    ①先梳理出实体,再根据业务联系紧密的实体形成聚合,再根据业务及语义边界等因素将多个聚合划定在一个限界上下文(领域模型)。
    ②聚合之间的边界是逻辑边界,也就是逻辑隔离。
    ③逻辑边界之间的聚合不一定非得拆非单独的微服务,防止过度设计。
  • Eric

    2019-11-22 12:50:12

    在同一个微服务内的应用层使用领域层的服务时,数据传递建议使用哪种类型的对象呢?是用DTO还是DO?

    如果因为性能问题,讲应用层对领域服务的调用从微服务内调用改为跨微服务调用(例如RESTful或者RPC),那么调用接口传递数据是否使用DTO比较合适?
    作者回复

    应用层与领域层之间是DO。
    微服务之间用DTO比较合适。

    2019-11-22 13:25:37

  • 玉宸道君

    2020-11-16 14:55:26

    我刚刚加入的一个公司,现在拆分的微服务代码结构就是“小单体微服务”。并且其中多个服务的业务都有评论、点赞这些操作,于是他们把评论、点赞单独抽取到了一个微服务中去了。我觉得这样的拆分方式是把自己的业务给拆分出去了,这样的微服务不是业务高内聚的。打个比方,这样拆分的微服务,就像一个只有自己身体的躯干部分的人,手、脚不在自己身上,做事情的时候还需要临时去找手、脚。接口内部微服务间调用变多了,对接口的可靠、性能都造成影响。

    不知道老师对我说的情况有什么看法呢?
    作者回复

    从单体到微服务肯定是有代价的,需要有相关的设计方法论指导,规范和降低服务设计、开发和集成的复杂度,还有诸如分布式架构下的一些设计理念也是需要掌握的。比如如何通过数据冗余,比如值对象,避免跨微服务的数据访问,如果通过领域事件驱动的方式实现聚合和微服务之间的解耦等等。
    如果单体应用完全可以满足业务要求,也可以不转成微服务。但是目前由于基础环境的变化,比如云计算,微服务比单体应用有更强的适应能力,更容易上云,生态也更齐全,所以从单体到微服务是未来的趋势,这就需要掌握一套好的设计方法。

    2020-11-19 11:54:57

  • 0084

    2020-08-31 15:41:39

    有些项目团队在将集中式单体应用拆分为微服务时,首先进行的往往不是建立领域模型,而只是按照业务功能将原来单体应用的一个软件包拆分成多个所谓的“微服务”软件包,而这些“微服务”内的代码仍然是集中式三层架构的模式,“微服务”内的代码高度耦合,逻辑边界不清晰,这里我们暂且称它为“小单体微服务”

    我们公司2017年中开始用SpringCloud全家桶搞微服务,目前的现状和老师讲的小单体微服务一摸一样,每个微服务中的代码还是按照传统三层架构设计。比如有客户系统、影响系统、消息系统、支付系统、账务系统等
    作者回复

    这种情况应该很常见。在没有接触到DDD之前,很多企业可能还是在按照三层架构模式进行微服务设计。在理解了DDD的核心设计思想后,你会理解ddd的方法在微服务设计中还是很有前瞻性的。架构设计中“高内聚,低耦合”强调了很多年,这个原则基本会一直延续下去。DDD虽然早于微服务很多年出现,其根本目的就是为了“高内聚和低耦合”和实现架构轻松演进。

    2020-09-07 16:30:16

  • 王海林

    2020-06-11 09:15:55

    老师,能进一步说说,逻辑边界与微服务API设计的原则吗?感觉这似乎是结构优雅与否的关键。
    作者回复

    是的,要保持逻辑边界的清晰,就必须保证聚合之间的松耦合。松耦合有两个问题要避免:一是不同聚合的领域服务之间不要直接调用;二是不同聚合之间的DO实体或持久化实体之间不要产生表级关联,这样在从逻辑边界变成物理边界过程中就不需要进行太多的解耦的操作了。
    如何结合聚合之间的服务调用呢?
    聚合之间的领域服务调用可以上升到应用层,通过应用服务来组合和编排不同聚合之间领域服务,实现不同聚合之间的服务协同。在应用服务组合编排不同聚合的领域服务时,要尽量避免在不同聚合的服务调用时的对象传参方式,因为这样也会产生聚合之间的对象耦合。

    2020-06-24 16:09:35

  • 妙手空空

    2020-04-19 15:00:50

    领域服务之间能互相调用吗?
    作者回复

    理论上可以,但不建议聚合之间的领域服务直接调用,主要是为了聚合之间的解耦。你可以将这种聚合之间的调用,上升到应用层,通过应用服务来进行编排和调用。

    2020-04-19 19:47:56

  • William.加

    2019-11-18 08:40:00

    老师,是不是不通过DDD拆分的微服务基本就是大单体到小单体的拆分?
    作者回复

    如果还是原来的架构模式,不做解耦的话,应该还是小单体模式。我也是找了好久才找到了DDD这个方法,不知道还有没有其它更好的方法。

    2019-11-18 09:13:52

  • Geek_13d0e8

    2023-03-06 12:10:15

    聚合查询的场景,再DDD中,如何体现呢,这种聚合查寻又结合逻辑加工的场景
  • 陈斌

    2023-02-02 02:51:56

    一般拆分为服务的动机是业务扩大,性能要求较高,或者是能应对高流量。
    按照领域拆分微服务,能够快速适应业务扩大动机,业务扩大一般会导致数据量的增加,因为领域一般对应着自己独立Mysql存储,如果吧木偶个领域拆分出一个微服务,则这个微服务专门负责该领域的业务,可以很大程度减少Mysql(存储)的压力。按照领域拆分微服务 可以通过事件机制降低微服务之间的耦合。
    如果按照业务、功能拆分微服务,会导致微服务之间有很多不必要的耦合(即:业务A需要这部分功能,业务B也需要这部分功能,但是A、B又不在同一个微服务中,就会导致A调用B,或者B调用A),如果硬不耦合的话,就会出现代码重复。
  • 陈斌

    2023-02-02 02:21:39

    大单体变成小单体。高度耦合。 想知道怎么判断耦合是不合理的,按照领域划分微服务,微服务之间也会关联的,那我怎么怎么判断它这个耦合就属于合理的,低耦合呢?
  • 剑八

    2022-07-17 22:26:04

    3界:逻辑,物理,代码边界
    逻辑边界是聚合为单位,是一个业务纬度,如支付账号聚合,支付收付款聚合。
    物理则是实际服务运行的物理情况,
    支付中当量不大业务初期,可以一个微服务。
    代码边界则是聚合的代码体现,用代码模块隔离不同聚合,后续重组可以修改小,演进架构。
    边界很重要
  • 徐李

    2021-12-20 20:42:10

    我们现在的微服务拆分主要是分着业务走,业务不一样就独立处理,业务相关的代码就放在一个微服务,有时候这个微服务业务很大,实际上就是一个单体了,然后再这个单体中再拆分出微服务,再拆分出微服务。与DDD相比较,可能DDD强调的是,就算在一个微服务中,他的包命名结构,也能保证拆分服务时候,或者代码重组的时候,简单可行。
  • 俯瞰风景.

    2021-10-09 21:33:08

    微服务架构的核心在于实现演进式架构,即随着业务的增长和需求的变化,能够支持架构长期、轻松的演进。

    而实现这个目标的关键在于对于服务的拆分和服务内部实现的“高内聚低耦合”,对于微服务架构来说,聚合是可以拆分为微服务的最小单位,所以聚合和聚合之间的逻辑边界要清晰。以便于之后需要把一个聚合单独拆分为一个微服务时能够轻松实现。
  • Rootrl

    2021-09-28 11:09:45

    这个工程目录对应的是一个微服务对吧?到时候整体打包发布对吧?那么问题来了,这每个微服务应该是不同团队负责的,那么这些工程之间会用到公共的代码怎么办呢?比如Infrastructrue里面的utils,比如具体的DateUtil
  • 王智洋

    2021-09-05 21:28:39

    老师 实体方法必须要用领域服务封装吗?
    如果我在应用服务层把实体构建出来了 直接调实体方法可以吗?

    实体构建这里有个困惑点。
    如果构建实体的指令在领域层的话 有时候接口层传进来的参数对象需要传进领域层 耦合有点高