“设计原则千万条,高内聚低耦合第一条,架构设计不规范,开发运维两行泪!”。
在分布式架构下,单体应用被拆分为多个微服务,为了保证微服务的单一职责和合理拆分,“高内聚、松耦合”是最宝贵的设计原则。
通俗点讲,高内聚就是把相关的行为聚集在一起,把不相关的行为放在别处,如果你要修改某个服务的行为,最好只在一处修改。如果做到了服务之间的松耦合,那么修改一个服务就不需要修改另一服务,一个松耦合的服务应该尽可能少的知道与之协作的那些服务的信息。
从集中式架构向分布式架构的技术转型,正如从盖砖瓦房向盖高楼大厦转变一样,必然要有组织、文化、理念和设计方法的同步更新,其中最不可或缺的能力就是架构设计能力。
如何做到“高内聚、低耦合”?我们先来学习几种典型的微服务架构模型。
微服务架构模型
整洁架构(又名洋葱架构)
在整洁架构里,同心圆代表应用软件的不同部分,从里到外依次是领域模型、领域服务、应用服务、最外围是容易变化的内容,如界面和基础设施(如数据存储等)。整洁架构是以领域模型为中心,不是以数据为中心。
image整洁架构
整洁架构最主要原则是依赖原则,它定义了各层的依赖关系,越往里,依赖越低,代码级别越高。外圆代码依赖只能指向内圆,内圆不知道外圆的任何事情。一般来说,外圆的声明(包括方法、类、变量)不能被内圆引用。同样的,外圆使用的数据格式也不能被内圆使用。
整洁架构各层主要职能如下:
- Entities:实现领域内核心业务逻辑,它封装了企业级的业务规则。一个 Entity 可以是一个带方法的对象,也可以是一个数据结构和方法集合。
- Use Cases:实现与用户操作相关的服务组合与编排,它包含了应用特有的业务规则,封装和实现了系统的所有用例。
- Interface Adapters:它把适用于 Use Cases 和 entities 的数据转换为适用于外部服务的格式,或把外部的数据格式转换为适用于 Use Casess 和 entities 的格式。
- Frameworks and Drivers:这是实现所有前端业务细节的地方:UI,Tools,Frameworks 等。
六边形架构(又名端口适配器架构)
追溯微服务架构的渊源,一般会涉及到六边形架构。六边形架构的核心理念是:应用是通过端口与外部进行交互的,这也是微服务架构下 API 网关盛行的主要原因。六边形架构中,内部业务逻辑(应用层和领域模型)与外部资源(APP,WEB 应用以及数据库资源等)完全隔离,仅通过适配器进行交互。它解决了业务逻辑与用户界面的代码交错的主要问题,从而可以很好的实现前后端分离。
image六边形架构
六边形架构将系统分为内部和外部两层六边形,内部六边形代表了应用的核心业务逻辑,外部六边形代表外部应用、驱动和基础资源等。内部通过端口和适配器与外部通信,对应用以 API 主动适配的方式提供服务,对资源通过依赖反转被动适配资源的形式呈现。一个端口可能对应多个外部系统,不同的外部系统使用不同的适配器,适配器负责对协议进行转换。这就使得应用程序能够以一致的方式被用户、程序、自动化测试、批处理脚本所驱动。
六边形架构各层的依赖关系与整洁架构类似。
CQRS(命令与查询职责分离)
CQRS 就是读写分离,读写分离的主要目的是为了提高查询性能,同时达到读、写解耦。而 DDD 和 CQRS 结合,可以分别对读和写建模。
imageCQRS(命令与查询职责分离)
查询模型是一种非规范化数据模型,它不反映领域行为,只用于数据查询和显示;命令模型执行领域行为,在领域行为执行完成后通知查询模型。
命令模型如何通知到查询模型呢?如果查询模型和领域模型共享数据源,则可以省却这一步;如果没有共享数据源,可以借助于发布订阅的消息模式通知到查询模型,从而达到数据最终一致性。
Martin 在 blog 中指出:CQRS 适用于极少数复杂的业务领域,如果不是很适合反而会增加复杂度;另一个适用场景是为了获取高性能的查询服务。
对于写少读多的共享类通用数据服务(如主数据类应用)可以采用读写分离架构模式。单数据中心写入数据,通过发布订阅模式将数据副本分发到多数据中心。通过查询模型微服务,实现多数据中心数据共享和查询。
领域驱动设计分层架构
分层架构的一个重要原则是每层只能与位于其下方的层发生依赖。
分层架构的好处是显而易见的。
首先,由于层间松散的耦合关系,使得我们可以专注于本层的设计,而不必关心其他层的设计,也不必担心自己的设计会影响其它层,对提高软件质量大有裨益。其次,分层架构使得程序结构清晰,升级和维护都变得十分容易,更改某层的代码,只要本层的接口保持稳定,其他层可以不必修改。即使本层的接口发生变化,也只影响相邻的上层,修改工作量小且错误可以控制,不会带来意外的风险。
关于分层架构的权威观点,Martin Fowler 在《Patterns of Enterprise Application Architecture》一书中给出了答案: 1. 开发人员只关注整个架构中的某一层。 2. 很容易的用新的方法来替换原有层次的方法。 3. 降低层与层之间的依赖。 4. 有利于标准化。 5. 利于各层逻辑的复用。
要保持程序分层架构的优点,就必须坚持层间的松耦合关系。设计程序时,应先划分出可能的层次,以及此层次提供的接口和需要的接口。设计某层时,应尽量保持层间的隔离,仅使用下层提供的接口。
imageDDD(领域驱动设计)分层架构
DDD 分层架构各层定义与职能:
展现层:它负责向用户显示信息和解释用户命令,完成前端界面逻辑。这里的用户不一定是使用用户界面的人,也可以是另一个计算机系统。
应用层:它是很薄的一层,负责展现层与领域层之间的协调,也是与其它系统应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,不保留业务对象的状态,只保留有应用任务的进度状态,更注重流程性的东西。它只为领域层中的领域对象协调任务,分配工作,使它们互相协作。
领域层:它是业务软件的核心所在,包含了业务所涉及的领域对象(实体、值对象)、领域服务以及它们之间的关系,负责表达业务概念、业务状态信息以及业务规则,具体表现形式就是领域模型。领域驱动设计提倡富领域模型,即尽量将业务逻辑归属到领域对象上,实在无法归属的部分则以领域服务的形式进行定义。
基础设施层:它向其他层提供通用的技术能力,为应用层传递消息(API 网关等),为领域层提供持久化机制(如数据库资源)等。
架构模型对比和分析
虽然整洁架构、六边形架构以及 DDD 分层架构三种架构模型展现方式以及解决问题的出发点不一样,但其架构思想与微服务架构高内聚低耦合的设计原则高度一致。
image整洁、六边形以及 DDD 三种架构模型关系
突破现象看本质,在变与不变中寻找平衡!
从上图可以看出,在六边形架构、DDD 分层架构的白框部分以及整洁架构 Use Cases 和 Entities 区域实现了核心业务逻辑。但是核心业务逻辑又由两部分来完成:应用层和领域层逻辑。领域层实现了最核心的业务领域部分的逻辑,对外提供领域模型内细粒度的领域服务,应用层依赖领域层业务逻辑,通过服务组合和编排通过 API 网关向前台应用提供粗粒度的服务。
系统需求变幻无穷,但变化总是有矩可循的,用户体验、操作习惯、市场环境以及管理流程的变化,往往会导致界面逻辑和流程的多变,但总体来说,不管前台如何变化,核心领域逻辑基本不会大变。把握好这个规律,我们就知道如何设计应用层和领域层,如何进行逻辑划界了。
上述三种架构模型正是通过分层方式来控制需求变化对系统的影响,确保从外向里受需求影响逐步减小。面向用户的展现层可以快速响应外部需求进行调整和发布,灵活多变,应用层通过服务组合和编排实现业务流程的快速适配上线,领域层基本就不需要太多的变化了。这样设计的好处是可以保证领域层的核心业务逻辑不会因为外部需求和流程的变动而调整,对于建立前台灵活、中台稳固的架构能力是很有好处的。
从几种架构模型看如何进行中台及微服务设计?
中台和微服务设计的关键在于合理的分层和领域模型的设计!
1、聚焦领域模型
中台属于后端业务领域逻辑范畴,重点关注领域内业务逻辑的实现,通过实现公共需求为前台应用提供共享服务能力。按 DDD 的方法,在领域模型建立的过程中会对业务和应用进行清晰的逻辑和物理边界划分。领域模型的设计结果会影响到后续的系统模型、架构模型和领域层代码模型的设计,最终影响到微服务的拆分和项目落地实施。
2、合理的架构分层
不要把与领域无关的业务逻辑放在领域层,避免领域业务逻辑被污染,保证领域层的纯洁,只有这样才能降低领域逻辑受外部变化的影响。在领域和架构模型建立后,代码模型的逻辑分层和微服务拆分要具体情况具体分析,根据自身研发和运维能力综合考虑。
(1) 项目级单应用
对于单应用系统的分层,遵循上述分层架构模型即可,核心领域逻辑在领域层实现,服务的组合和编排在应用层实现,两者组合形成中台,通过 API 对前台应用提供服务。
从部署和微服务拆分来讲,领域层代码部署时可能是一个微服务,也可能会根据限界上下文被拆分为多个微服务部署。应用层代码如果逻辑复杂,含较多个性业务逻辑,可以根据需要独立为微服务部署。如果逻辑简单,且领域层是一个微服务,在划分好应用层和领域层代码逻辑边界的情况下,如果符合微服务拆分原则,也可以考虑将应用层与领域层代码合并为一个微服务部署。
(2)企业级多中台应用
对于企业级多中台应用,多个中台应用通过 API 网关对外发布 API 服务。核心域业务中台在调用支撑域和通用域中台服务时通过核心域应用层完成多中台服务的组合和编排,为前台应用提供 API 服务。核心域中台的应用层是否独立成微服务部署,需考虑的情况与单应用系统相似。
3、服务的管理
应用层、领域层和基础设施层都有对应的服务,各司其职提供服务,其中基础设施层的服务通过依赖反转模式为领域层和应用层提供基础设施资源服务。应用层和领域层服务发布在 API 网关,通过 API 网关适配,为前台提供用户无差异化(应用 app、批处理或自动化测试)的服务。
4、资源的适配和解耦
由于上述架构模型中定义的外层只能依赖内层的架构原则,对于像数据库、缓存、文件系统等的外部基础设施资源,往往采用依赖反转的模式对外提供资源服务,实现应用层、领域层与基础设施层资源的解耦。在设计中应考虑资源层的代码适配逻辑,一旦基础设施资源出现变更(如换数据库),可以屏蔽资源变更对业务代码带来的影响,切断业务逻辑对基础资源的依赖,降低由于资源变更对业务逻辑的影响。
5、前台应用
从核心业务逻辑来看,中台实现了主要的业务逻辑,属于标准化的重量级应用。前台应用聚焦于界面交互以及业务流程等,属于轻量级应用,前台应用可以有个性的业务逻辑、流程和配置数据,甚至数据库,通过调用中台 API 服务完成交互界面和业务全流程。
中台、领域驱动设计及微服务
分析和设计模式的演进
在单机和集中式架构时代,系统分析和设计往往都是分阶段割裂进行的,容易导致需求、设计与代码实现的不一致,软件上线后才发现很多功能不是自己想要的,而且在这种模式下,软件也不能快速响应需求和业务变化。
领域驱动设计(DDD)打破了这种隔阂,它提出了领域模型概念,统一了分析、设计和开发语言和过程,使得软件能够更灵活快速响应需求变化。
软件分析和设计方法经历了三个阶段的演进:
- 第一阶段是单机架构时代:采用面向过程的设计方法,系统包括 UI 层和数据库两层,采用 C/S 架构模式,整个系统围绕数据库驱动设计和开发,新项目总是从设计数据库及其字段开始。
- 第二阶段是集中式架构时代:采用面向对象的设计方法,系统包括 UI 层、业务逻辑层和数据库层,采用经典的三层架构,也有部分应用采用传统的 SOA 架构,这种架构易使服务变得臃肿,难于维护拓展,伸缩性能差。这个阶段系统分析、软件设计和开发大多是分阶段进行的。
- 第三阶段是分布式架构时代:由于微服务架构的流行,采用领域驱动设计方法,应用系统包括 UI 层、应用层、领域层和基础层。这个阶段融合了分析和设计阶段,通过建立领域模型,划分领域边界,做到领域模型既设计,代码与设计保持一致。
领域驱动设计主要优势:1. 业务导向。2. 业务逻辑内聚,应用边界清晰。3. 建立领域模型优先。4. 分析、设计、代码和数据有机结合。5. 代码即设计。6. 扩展性好。
数据驱动设计主要特点:1. 技术导向。2. 数据库优先。3. 代码不能反映业务和设计。4. 业务逻辑分散。5. 扩展性不好。
领域驱动设计概述
2004 年 Eric Evans 发表《Domain-Driven Design –Tackling Complexity in the Heart of Software》 (领域驱动设计 )简称 Evans DDD。但在软件开发领域一直都是雷声大,雨点小,领域驱动设计核心思想是通过领域驱动设计方法定义领域模型,从而确定业务和应用边界,保证业务模型与代码模型的一致性。这几年之所以开始火起来,主要功劳要归功于队友“微服务”,领域驱动设计与微服务架构天生匹配。
领域驱动设计(DDD)是一种处理高度复杂域的设计思想,试图分离技术实现的复杂性,围绕业务概念构建领域模型来控制业务的复杂性,以解决软件难以理解,难以演化等问题。团队利用它可以成功的开发复杂业务软件系统,在系统变大时仍能保持敏捷性。
领域驱动设计分为两个阶段:
- 以一种领域专家、设计人员、开发人员都能理解的通用语言作为相互交流的工具,在交流的过程中发现领域概念,然后将这些概念设计成一个领域模型;
- 由领域模型驱动软件设计,用代码来实现该领域模型。
领域驱动设计的核心诉求是让业务架构和系统架构形成绑定关系,当我们去响应业务变化调整业务架构时,系统架构的改变也会随之发生。在领域驱动设计中业务架构的梳理和系统架构的梳理是同步进行的,其结果是设计出的业务上下文和系统模块结构是绑定的。同时技术架构也是解耦的,可以根据划分出来的业务上下文的系统架构选择最合适的实现技术。
领域驱动设计包括战略设计和战术设计两个部分。战略设计主要关注按领域定义,在限界上下文内形成统一语言,提升业务和技术的沟通效率; 战术设计主要关注领域设计在落地时与设计模型及实现模型的差异性,减小业务和技术之间的鸿沟。(本文对 DDD 知识点不做详述,如需了解或学习,请查阅《领域驱动设计:软件核心复杂性应对之道》和《实现领域驱动》)。
领域驱动设计可能会给你带来以下收获:
1、领域驱动设计是一套完整而系统的设计方法,它能带给你从战略设计到战术设计的规范过程,使得你的设计思路能够更加清晰,设计过程更加规范。
2、领域驱动设计尤其善于处理与领域相关的高复杂度业务的产品研发,通过它可以为你的产品建立一个核心而稳定的领域模型内核,有利于领域知识的传递与传承。
3、领域驱动设计强调团队与领域专家的合作,能够帮助团队建立一个沟通良好的团队组织,构建一致的架构体系。 领域驱动设计强调对架构与模型的精心打磨,尤其善于处理系统架构的演进设计。
4、领域驱动设计的思想、原则与模式有助于提高团队成员的架构设计能力。
5、领域驱动设计与微服务架构天生匹配,无论是在新项目中设计微服务架构,还是将系统从单体架构演进到微服务设计,都可以遵循领域驱动设计的架构原则。
为什么领域驱动设计是微服务架构的最佳设计方法?
领域驱动设计作为一种架构设计方法,微服务作为一种架构风格,两者从本质上都是为追求高响应力目标而从业务视角去分离复杂度的手段。 两者都强调从业务出发,其核心要义强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力(演进式架构) 。
领域驱动设计主要关注:业务领域,划分领域边界;构建通用语言,高效沟通;对业务进行抽象,建立领域模型;维持业务和代码的逻辑一致性。
微服务主要关注:运行时进程间通信,能够容错和故障隔离;去中心化管理数据和去中心化治理;服务可以独立的开发、测试、构建和部署,按业务组织全功能团队;高内聚低耦合,职责单一。
如果你的业务焦点在领域和领域逻辑,那么你就可以选择 DDD 进行微服务架构设计。
中台、DDD 与微服务
中台的定义来源于阿里的中台战略(详见《企业 IT 架构转型之道:阿里巴巴中台战略思想与架构实战》钟华编著)。2015 年年底,阿里巴巴集团对外宣布全面启动阿里巴巴集团 2018 年中台战略,构建符合数字时代的更具创新性、灵活性的“大中台、小前台”组织机制和业务机制,即作为前台的一线业务会更敏捷、更快速适应瞬息万变的市场,而中台将集合整个集团的运营数据能力、产品技术能力,对各前台业务形成强力支撑。
中台的本质是提炼各个业务条线的共同需求,并将这些功能打造成组件化产品,然后以 API 接口的形式提供给前台各业务部门使用。前台要做什么业务,需要什么资源可以直接找中台,不需要每次去改动自己的底层,而是在底层不变动的情况下,在更丰富灵活的“大中台”基础上获取支持,让“小前台”更加灵活敏捷。
中台战略的主要目标是实现公共需求和功能的中台化共享,减少重复建设和投入,为前台提供统一的一致服务。至于前台应用是否可以有数据库?抑或采用什么样的开发技术,这些都不是重点,重点需要考虑的是那些公共需求和需要共享的功能是否通过中台的方式被前台使用了。
领域驱动设计中领域的定义:一个领域本质上可以理解为就是一个问题域,只要是同一个领域,那问题域就相同。所以只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。领域的本质是问题域,问题域可能根据需要逐层细分,因此领域可分解为子域,子域或可继续分为子子域。。。
在领域驱动设计中根据重要性与功能属性将领域分为三类子域,分别是:核心子域、支撑子域和通用子域。决定产品和企业独特竞争力的子域是核心子域,它是业务成功的主要因素和企业的核心竞争力。没有个性化的诉求,属于通用功能的子域是通用子域,如登陆认证。 还有一种所提供的功能是必须的,但不是通用也不是企业核心竞争力的子域是支撑子域,如单证。
imageDDD: 核心域、支撑域和通用域
中台、领域以及微服务属于不同层面的内容,稍作分解我们理清他们之间的关系。
以保险领域为例,业务中台大致可分为两类:第一类是提供保险核心业务服务的专属业务中台(如承保、理赔等业务);第二类是支撑核心业务流程完成保险全流程的通用中台(如主数据、客户、用户以及电子保单等)。
专属业务中台是保险企业的核心竞争力,对应 DDD 的核心子域。通用中台对应 DDD 支撑子域和通用子域。不同领域可根据领域大小进一步细分多个子域,多个子域可对应到一个业务中台,一个业务中台也可能会分解成多个子域。
image中台、领域以及微服务
微服务是技术实现和部署的范畴,实现领域或中台的业务逻辑,为前台应用提供服务。领域根据限界上下文可以设计为多个微服务,而如果限界上下文过大,一个微服务也可能会包含多个子领域。
中台是由多个业务条线的共同需求所构成,是需要共享的业务功能和服务单元的集合,一个中台可由一个微服务来实现,也可根据领域驱动设计和微服务拆分原则细分为多个微服务,多个微服务功能集合共同组成一个中台。
基于 DDD 的微服务设计方法
DDD 设计包括战略设计和战术设计两个部分。在战略设计阶段主要完成领域建模和服务地图。在战术设计阶段,通过聚合、实体、值对象以及不同层级的服务,完成微服务的建设和实施。通过 DDD 可以保证业务模型、系统模型、架构模型以及代码模型的一致。
本部分主要讨论领域设计方法,如对战术设计和开发方法感兴趣可查阅 DDD 战术设计相关资料。
DDD 领域设计过程包括产品愿景、场景分析、领域建模和服务地图阶段,也可根据需要裁剪不必要的阶段和参与角色。领域驱动设计一般经历 2-6 周的时间,领域模型设计完成后,即可投入微服务实施。
1、产品愿景
产品愿景是对产品的顶层价值设计,对产品目标用户、核心价值、差异化竞争点等策略层信息达成一致,避免产品在演进过程中偏离方向。
- 阶段输入:产品初衷、用户研究、竞品知识和差异性想法 。
- 参与角⾊:业务需求方、产品经理、开发组长和产品发起人。
- 阶段产出:电梯演讲画布。
2、场景分析
场景分析是针对核心用户及顶层服务的一种定性分析,从⽤户视角出发,探索问题域中的典型场景分析。同时也是从用户视角对问题域的探索,产出问题域中需要支撑的场景分类及典型场景,用以支撑领域建模阶段。
- 阶段输⼊:核⼼干系人和服务价值定位。
- 参与角色:产品经理、开发组长和测试组长。
- 阶段产出:场景分类清单。
3、领域建模
领域建模是通过对业务和问题域进⾏分析,建⽴领域模型,向上通过限界上下⽂指导微服务的边界设计,向下通过聚合指导实体的对象设计。领域建模主要采用事件风暴方法。
- 阶段输入:业务领域知识和场景分类清单。
- 参与角色:领域专家、架构师、产品经理、开发组长和测试组长。
- 阶段产出:聚合模型和限界上下⽂地图。
4、服务地图
服务地图是整个产品服务架构的体现。结合业务与技术因素,对服务的粒度、边界划分、集 成关系进⾏梳理,得到反映系统微服务层面设计的服务地图。
- 阶段输⼊:限界上下⽂地图。
- 参与角⾊:产品经理、开发组长、测试组长和产品发起人。
- 阶段产出:服务地图。
在进行服务地图设计时需要考虑以下要素:1. 围绕限界上下⽂边界。2. 考虑不同业务变化速度 / 相关度、发布频率。3. 考虑系统非功能性需求,如系统弹性伸缩要求、安全性要求和可⽤性要求。4. 考虑团队组织和沟通效率。5. 软件包限制。6. 技术和架构的异构。
通过 DDD 战略和战术全流程设计可建立业务架构与系统架构的一一映射,保证业务和代码模型的一致性。
imageDDD 的业务架构与系统架构映射建立过程
DDD 分层架构中的服务
前面我们谈到了 DDD 的分层架构,分层架构主要包括:展现层、应用层、领域层和基础层(参考图:DDD(领域驱动设计)分层架构),各层都有不同的服务,但由于各层职责不一样,服务目的和实现方式也存在差异。
1、应用层服务
应用层是很瘦的一层,其服务主要用来表述应用和用户行为。它主要负责服务的组合、编排和转发,负责处理业务用例的执行顺序以及结果的拼装,拼装完领域服务后以粗粒度的服务通过 API 网关向前台应用发布。通过这样一种方式,隐藏了领域层的复杂性及其内部实现机制。 应用层除了定义应用服务之外,在这层还可以进行安全认证,权限校验,持久化事务控制或向其他系统发送基于事件的消息通知。
2、领域层服务
领域层是较“胖”的一层,它实现了全部业务逻辑并且通过各种校验手段保证业务正确性。业务逻辑包括:业务流程、业务策略、业务规则、完整性约束等。 当领域中的某个操作过程或转换过程不是实体或值对象的职责时,便将该操作放在一个单独的服务接口中,这就是领域服务,领域服务是无状态的。
3、基础设施层服务
基础设施层服务位于基础设施层,根据依赖倒置原则,封装基础资源服务,实现资源层与应用层和领域层的调用依赖反转,为应用层和领域层提供基础资源服务(如数据库、缓存等基础资源),实现各层的解耦,降低外部资源的变化对核心业务逻辑的影响。
4、总结
应用层服务是展现层和领域层的桥梁,通过调用领域对象和领域层服务来表达用例和用户故事。领域对象负责单一操作, 领域层服务用于协调多个领域对象共同完成某个业务操作。 应用服务原则上不处理业务逻辑,领域服务处理业务逻辑。
微服务的边界设计
逻辑边界与物理边界
在领域模型设计时,我们通常会根据限界上下文将领域分解成不同的子域,划分业务领域的逻辑边界。在限界上下文内不同的实体和值对象可以组合成不同的聚合,从而形成聚合与聚合之间的逻辑边界。一般来说,限界上下文可以作为微服务拆分的依据,而限界上下文内的聚合由于其业务逻辑的高度内聚,也可以根据需要将同一领域内的聚合业务逻辑代码拆分为微服务,聚合是领域中可以拆分为微服务的最小单元。
限界上下文与限界上下文之间以及聚合与聚合之间的边界是逻辑边界,微服务与微服务的边界是物理边界。逻辑边界强调业务领域逻辑或代码分层的隔离,物理边界强调部署和运行的隔离。
微服务设计时是否一定要做到逻辑边界与物理边界一致?
逻辑边界的划分是否可以细于物理边界?
过度的微服务拆分会导致服务、安全和运维管理更复杂,领域之间的服务协同或应用层的处理逻辑更复杂,总之一句话就是:需要更高的研发技能要求和软件维护成本。因此领域和代码分层的逻辑边界的细分是必要的,但是物理边界不宜过细,也就是说在不违反微服务拆分原则的情况下,不宜过度拆分微服务。
为什么要细分业务和代码逻辑边界?
在从单体向微服务演进后,随着新需求的出现,新的微服务会开始慢慢的膨胀起来,有一天你会发现膨胀的微服务有一部分业务能力需要拆分出去时,如果没有提前进行逻辑边界的细分,微服务内代码的过度耦合将会让你无从下手,你是否还需要再做一次从单体向微服务的拆分?
如果你在微服务设计时已经根据业务领域边界提前进行了领域代码的分层和逻辑隔离,在微服务再次拆分时,分别对逻辑分离的领域代码打包,同步进行数据库拆分,就可以快速完成微服务的拆分,而不需要重复从单体应用向微服务痛苦的演进过程。
当然,在同一个微服务内逻辑隔离的代码,在内部领域服务之间调用以及数据访问设计上需要有合理的松耦合的设计和开发规范,否则也不能很快的完成微服务再次拆分。
总之,我们需要内外部逻辑边界清晰的微服务,而不是从一个大单体重构为多个小单体。
要做微服务而不是小单体
很多时候大家对微服务设计的理解都以为只要最后确定拆分出多少个微服务就可以了,其实拆成多少个微服务并不是微服务架构的要点。如何设计或拆分才能避免拆分出来的微服务不是小单体?这才是所有微服务架构团队需要关注和解决的问题,这也是 DDD 的价值所在。
image要做微服务而不是小单体
评判微服务设计合理的一个简单标准就是:微服务在随着业务发展而不断拆分或者重新组合过程中不会过度增加软件维护成本,并且这个过程是非常轻松且简单的。
微服务代码逻辑分层和结构
为了方便在微服务变大时实现快乐的拆分和合并,在明确各层代码职责后,我们需要对微服务代码合理分层和逻辑隔离,以下图为例对代码分层和结构进行简要说明。
基础层代码:本层主要包括两类适配代码:主动适配和被动适配。主动适配代码主要面向前端应用提供 API 网关服务,进行简单的前端数据校验、协议以及格式转换适配等工作。被动适配主要面向后端基础资源(如数据库、缓存等),通过依赖反转为应用层和领域层提供数据持久化和数据访问支持,实现资源层的解耦。
应用层代码:本层代码主要通过调用领域层服务或其他中台应用层服务,完成服务组合和编排形成粗粒度的服务,为前台提供 API 服务。本层代码可进行业务逻辑数据的校验、权限认证、服务组合和编排、分布式事务管理等工作。
领域层代码:本层代码主要实现核心的业务领域逻辑,需要做好领域代码的分层以及聚合之间代码的逻辑隔离。相关的开发方法请查阅 DDD 战术设计相关资料,并遵循相关设计和开发规范。
image代码逻辑分层和结构
对代码进行逻辑隔离和分层的主要意义在于:
1、避免各层代码的交叉,保持领域代码的纯洁,保证中台领域层业务逻辑的稳定。
2、业务和代码模型的逻辑保持一致,有利于微服务的拆分和组合。
微服务的设计和拆分
微服务拆分方法
绞杀者模式
绞杀者模式类似建筑拆迁,在新建筑分阶段建设完成入住后,分步拆除旧建筑物。
“绞杀者模式”是在遗留系统外围,将新功能用新的方式构建为新的服务 。通过在新的应⽤中实现新特性,保持和现有系统的松耦合,随着时间的推移,新的服务逐渐“绞杀”老的系统。以此逐步地替换原有系统。 对于那些老旧庞大难以更改的遗留系统,推荐采用绞杀者模式。
修缮者模式
修缮者模式类似文物修复,将存在问题的部分建筑重建或者修复后,重新加入到原有的建筑中,保持建筑原貌。
“修缮者模式”是在既有系统的基础上,通过剥离新业务和功能,逐步“释放”现有系统耦合度,解决遗留系统质量不稳定和 Bug 多的问题。就如修房或修路一样,将老旧待修缮的部分进行隔离,用新的方式对其进行单独修复。 修复的同时,需保证与其他部分仍能协同功能。 修缮模式适用于需求变更频率不高的存量系统。
微服务拆分原则
微服务拆分过程中需严格遵守高内聚、低耦合原则,同时结合项目的实际情况,综合考虑业务领域、功能稳定性、应用性能、团队以及技术等因素。
1、基于业务领域拆分,在领域模型设计时需对齐限界上下⽂,围绕业务领域按职责单一性、功能完整性进行拆分,避免过度拆分造成跨微服务的频繁调用。
2、基于业务变化频率和业务关联拆分,识别系统中的业务需求变动较频繁的功能,考虑业务变更频率与相关度,并对其进行拆分,降低敏态业务功能对稳态业务功能的影响。
3、基于应用性能拆分,考虑系统⾮功能性需求,识别系统中性能压力较大的模块,并优先对其进行拆分,提升整体性能,缩小潜在性能瓶颈模块的影响范围。
4、基于组织架构和团队规模,提高团队沟通效率。
5、基于软件包大小,软件包过大,不利用微服务的弹性伸缩。
6、基于不同功能的技术和架构异构以及系统复杂度。
分布式架构设计的关注点
企业一旦采用分布式架构和微服务技术体系,在设计时需要关注商业模式、业务边界、数据体系、微服务设计、前台交互以及多活容灾等多领域的协同。
1、数据是本难念的经
分布式架构下数据面临的问题远比集中式架构复杂。诸如:分布式数据库的选型、数据的分库和分表、数据的同步与异步、跨库和联表查询、数据的分布与集中、在线业务数据与统计分析数据的协同、集中式数据库向分布式数据库的迁移以及面向场景的集中数据复制等。
(1)分布式数据库的选择
从集中式架构向分布式架构转型,第一步就需要考虑选择什么样的分布式数据库。
为解决交易型分布式数据库的横向计算能力,目前主要有三种类型的分布式数据库:一体化交易型分布式数据库方案(如阿里 OceanBase 和华为高斯数据库,多采用 Paxos 协议实现多副本数据一致)、单机交易数据库加数据库中间件方案(如腾讯 TDSQL 和 TBase 等,多采用数据同步实现多副本数据一致)和单机交易数据库加分库基础类库(如 ShardingSphere 等,主要实现数据路由和归集)方案。三者的使用场景基本相同,都是通过对大表数据作水平切分,业务请求动态路由到指定节点,以此达到计算能力的线性扩展。一体化方案是以数据库和中间件一体化产品的形式解决线性扩展问题,支持多副本,高可用,提供统一的运维界面。 数据库中间件方案是以独立数据库中间件结合集中式数据库的方式来解决线性扩展问题,高可用功能由中间件和数据库自身功能分别保证。分库基础类库方案是一种类似中间件的轻量级解决方案,适合简单快速的交易操作,在强一致性和聚合分析查询方面较弱。
(2)数据的分库和分库主键
选择完分布式数据库后,第二步就需要考虑如何按照领域模型和微服务进行数据库的分库设计,选择合适的分库主键将是一个关键技术点。
对于与客户接触的业务领域,个人认为可以以客户维度作为数据分库主键,以客户为实体,确保所有与本客户接触和服务的数据都在一个单元内,通过集中共享的中台服务,为所有渠道的客户提供一致性体验。如果后序管理流程需要基于区域管理要求,也可以考虑在后序业务环节的数据库中以区域维度作为数据库分库主键,满足业务基于区域的管理要求。
如何将客户维度的数据传输到以区域为维度的数据库中?我们可以考虑基于消息队列的事件驱动模型。
系统如果做不到“以客户为中心”,又如何能实现“以客户为中心”的业务需求呢?
(3)高频热点数据的缓存
对于像产品基础数据、主数据之类的热点高频访问数据,在进行系统设计时需考虑将这些数据加载至缓存中,降低数据库的压力,对外提供高性能的数据访问能力。
缓存技术的使用就像调味料一样,投入小见效快,用户体验提升快。
(4)数据副本与跨库联表查询
采用分布式技术后,数据将碎片化,为了减轻由于跨库以及联表查询给分布式数据库的压力,需要建立多维的全局数据视图(如客户统一视图、业务统计数据视图等)和面向具体场景的预处理好的数据聚合副本,提供复杂场景的数据查询服务,减轻交易型数据库的压力。
全局数据视图其数据来源于各业务条线的分布式数据库,从源端分布式数据库通过准实时的方式汇集(可以基于数据库日志捕获技术加消息队列)。全局视图的数据库也可以是分布式数据库,根据业务要求选择合适的分库主键进行数据重分布。
对于分布式数据库跨库关联查询性能低的问题,有两种解决方案,根据具体场景采用合适的方案:
1)面向场景的数据副本查询库。将这些需要关联查询的数据副本集中存放在一个分布式数据库中。在进行数据汇集时,提前做好数据关联处理(如多表数据合并成一个宽表),通过查询微服务,专职提供关联查询服务。
2)小表广播模式。有些业务场景中少量表(如用户、机构表等)需要跟业务数据进行关联查询,这种场景可以考虑在业务数据库中新建一张复制表(无需全部字段,取必要字段即可),在主表发生变化时,可以通过发布订阅的消息队列模式刷新复制表的数据,保证数据的一致性。
(5)合理的数据冗余
完成领域模型和微服务设计后,集中式数据库的数据将被分散到不同微服务的分布式数据库中。数据实体的依赖关系将被打破,如果需要调用前序或后序微服务的数据实体(如:投保微服务生成的投保单、保单管理微服务的保单需要关联投保单,理赔的报案需要关联保单等,或电商业务中:销售过程中的商品、运输过程中的货物需要关联商品信息),这时候就会跨库或者跨微服务调用了,必然影响系统性能。
如何处理这些跨微服务的关键实体数据?
最好的方式就是数据冗余,将前序或后序环节的关键数据以数据清单复制表(只需必要的关键数据,不需要所有明细数据)的方式冗余存储。冗余的好处是,前台页面可以一次性获取本领域实体数据和关联实体清单数据,同时也可以在本库对关联清单数据进行查询。只有在需要获取关联实体数据明细时,才调用前序或后续微服务获取全量数据。
合理的数据冗余可以减少跨库查询,提升系统性能。
(6)如何数据迁移?
从集中式数据库向分布式数据库切换时,数据迁移的复杂度将大大增加。需要考虑如何进行数据迁移?现有技术条件下,是不是不做数据迁移也可以无缝切换?
传统集中式架构数据多集中在一个集中式数据库中,数据关联度高。
分布式架构下,数据会随着微服务而同步拆分,数据将变得碎片化,存在复制表,数据重分布,数据关联被打破,甚至还可能需要重建数据关联。另外,分布式架构的容灾和多中心多活要求,数据迁移时还需要考虑数据的多副本和多中心的数据复制。分布式架构下数据迁移的复杂度大增。
互联网公司大多采用演进式架构模式,有计划分阶段的进行技术体系的升级,很多时候用户无感知就完成了架构的升级。而传统企业在做技术升级时如采用绞杀者重构模式,是否必须要做数据迁移?如果不做数据迁移是否也可以顺利切换?是否通过数据路由加全量数据视图的方案就可以不做数据迁移,实现新旧并存,无缝切换?数据切换方案需要详细设计和慎重考虑(尚在考虑中,且听下回分解)。
(7)数据的异步和同步
分布式架构下事件驱动设计模式是常用的方法,通过基于消息队列的发布订阅模式,可以很好的实现业务异步化。非实时业务场景可以采用事件驱动的模式实现异步化,减轻数据库压力。
也可以通过异步模式实现准实时的数据读写分离,提高数据库性能。
2、中台和微服务要处理好边界
条条道路通罗马,不管走哪条路,凭感觉或拍脑袋也可以设计出微服务,拆分结果可能与按照 DDD 方法出来的结果类似。但是如果有好的理论和方法指导,不但做事情有矩可循的,而且可以避免走弯路。由于 DDD 在设计的时候已经做好了逻辑的边界划分,在微服务需要组合和重新拆分时也会变得容易得多。
还是有必要提一下:中台和微服务设计可以借鉴 DDD 的设计原则和理念,不过战术设计部分由于过于复杂和学习成本过高,可以参考使用。
3、前、中台协同和前台数据的按需加载
前台应用未来可能多采用单页面(SPA)的微前端(对应于微服务的前端展现,一个微服务对应一个微前端)方式,通过前端集成框架(类似门户)实现多页面组合,提供统一的用户体验,在微服务和数据库设计时也需要协同考虑前端页面逻辑。
为减轻跨微服务的访问,前端页面展示时应以清单数据方式按需加载,后端数据设计时也应同步考虑如何组合前端数据展示。如需要展示明细数据,通过调用 API 服务的方式获取全量数据,减少不必要的跨微服务调用。
另外,符合条件的应用也可考虑页面的动静分离和路由接入,将静态页面通过 CDN 的技术,部署在靠近用户的机房,降低交互次数,减少跨广域网访问带来的网络延迟。
前端知识有限,就写这么多了,哈哈。
4、容灾和多活的全局考虑
分布式架构的高可用是在应用、数据和基础设施的分布式技术升级后,通过多数据中心协同来实现的。
为了容灾和多活,在设计方面需要考虑:1)合适的分布式数据库。2)合理的数据分库主键设计,数据的多副本和同步技术。3)单元化架构设计,处理好通用中台和专属中台的部署和依赖关系,实现业务的自包含,减少跨数据中心调用。4)访问层的接入,对外部访问进行路由、限流以及灰度发布。5)统一的全局配置数据,每个数据中心都有实时同步的全量配置数据,实现容灾和多活的一键切换。
5、避免过度拆分和硬件依赖
过度过细的微服务拆分带来更多的软件维护成本和运维压力,过多的分布式事务也会带来性能和数据一致性的压力。在进行设计时,要在保证逻辑边界清晰的情况下,严控微服务的过度拆分和采用过多的分布式事务。
分布式架构的自动的弹性伸缩大多是通过软件的方式去实现的,为保证应用的弹性伸缩能力,在设计中应实现去硬件的无中心化(如可采用软负载,就不用 F5 之类的硬负载),尽量通过软件实现弹性伸缩。因为一旦绑定硬件设备,在硬件遇到瓶颈需要自动弹性伸缩的时候,就需要人工干预,无法自动弹性伸缩。
写在最后
正如老马说的采用微服务的企业需具备一定的高度,如文化、组织和技术,DDD 同样也需要站一定的高度。如果高度不够,我们是否可以站在巨人的肩上呢?
在领域模型和微服务设计时,守住领域模型和边界,各司其职,才能长治久安!
谨记:边界!边界!边界!
1、聚合根、实体、值对象的区别?
从标识的角度:
聚合根具有全局的唯一标识,而实体只有在聚合内部有唯一的本地标识,值对象没有唯一标识,不存在这个值对象或那个值对象的说法;
从是否只读的角度:
聚合根除了唯一标识外,其他所有状态信息都理论上可变;实体是可变的;值对象是只读的;
从生命周期的角度:
聚合根有独立的生命周期,实体的生命周期从属于其所属的聚合,实体完全由其所属的聚合根负责管理维护;值对象无生命周期可言,因为只是一个值;
2、聚合根、实体、值对象对象之间如何建立关联?
聚合根到聚合根:通过ID关联;
聚合根到其内部的实体,直接对象引用;
聚合根到值对象,直接对象引用;
实体对其他对象的引用规则:1)能引用其所属聚合内的聚合根、实体、值对象;2)能引用外部聚合根,但推荐以ID的方式关联,另外也可以关联某个外部聚合内的实体,但必须是ID关联,否则就出现同一个实体的引用被两个聚合根持有,这是不允许的,一个实体的引用只能被其所属的聚合根持有;
值对象对其他对象的引用规则:只需确保值对象是只读的即可,推荐值对象的所有属性都尽量是值对象;
3、如何识别聚合与聚合根?
明确含义:一个Bounded Context(界定的上下文)可能包含多个聚合,每个聚合都有一个根实体,叫做聚合根;
识别顺序:先找出哪些实体可能是聚合根,再逐个分析每个聚合根的边界,即该聚合根应该聚合哪些实体或值对象;最后再划分Bounded Context;
聚合边界确定法则:根据不变性约束规则(Invariant)。不变性规则有两类:1)聚合边界内必须具有哪些信息,如果没有这些信息就不能称为一个有效的聚合;2)聚合内的某些对象的状态必须满足某个业务规则;
1.一个聚合只有一个聚合根,聚合根是可以独立存在的,聚合中其他实体或值对象依赖与聚合根。
2.只有聚合根才能被外部访问到,聚合根维护聚合的内部一致性。
网友评论