本文转载自微信公众号「架构精进之路」,作者架构精进之路。转载本文请联系架构精进之路公众号。
软件架构发展经历
软件架构的发展经历了从单体架构、垂直架构、SOA架构到微服务架构以及到现在最新的service mesh(网格服务架构)的过程。借用dubbo的网站架构发展图和说明:
微服务存在的问题
进入微服务之后 , 解决了集中式架构的单体应用很多问题, 但是新的问题应运而生 , 微服务的力度应该多大 ?微服务如何设计呢?微服务如何拆分 ?微服务边界在哪里 ?
很长时间人们都没有解决这一问题,就连Martin Fowler在提出微服务架构的时候也没有告诉我们这该如何拆分微服务。
甚至在很长的时间里人们对微服务拆分产生了一些误解, 有人认为:"微服务很简单,就是将之前的单体应用拆分成多个部署包, 或者将原来的单体应用架构替换为一套支持微服务的技术架构,就算是微服务了。" 还有人认为微服务应该拆分得越小越好。
鉴于上述情形, 很多项目因为前期拆分过度, 导致复杂度过高, 导致后期难以运维甚至难以上线。
可以得出一个结论:微服务拆分困境产生的根本原因就是不知道业务或者微服务的边界到底在什么地方。换句话说,确定了业务边界和应用边界,这个困境也就迎刃而解了。
DDD的诞生
而DDD就是解决了这个确定业务边界的问题,可见DDD并不是一种技术架构,而是一种划分业务领域范围的方法论。DDD的兴起是由于很多熟悉领域驱动建模(DDD)的工程师在进行微服务设计时, 发现用DDD的思路进行业务梳理可以很好规划服务边界, 可以很好实现微服务内部和外部的"高内聚、低耦合"。于是越来越多的人将DDD作为业务划分的指导思想。
DDD是一种拆解业务、划分业务、确定业务边界的方法, 是一种高度复杂的领域设计思想,将我们的问题拆分成一个个地域, 试图分离技术实现的复杂性,主要解决的是软件难以理解难以演进的问题,DDD不是一种架构, 而是一种架构方法论, 目的就是将复杂问题领域简单化, 帮助我们设计出清晰的领域和边界, 可以很好的实现技术架构的演进。DDD包括两部分,战略设计部分和战术设计部分。
- 战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
- 战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。
微服务拆分难题
开发者在刚开始尝试实现自己的微服务架构时,往往会产生一系列问题 :
- 微服务到底应该怎么划分?
- 一个典型的微服务到底应该有多微?
- 如果做了微服务设计,最后真的会有好处吗?
回答上面的问题需要首先了解微服务设计的逻辑,科学的架构设计应该通过一些输入并逐步推导出结果,架构师要避免凭空设计和“拍脑门”的做法。
服务的划分有一些基本的方法和原则,通过这些方法能让微服务划分更有操作性。最终在微服务落地实施时也能按图索骥,无论是对遗留系统改造还是全新系统的架构都能游刃有余。
微服务拆分的几个阶段
在开始划分微服务之前,架构师需要在大脑中有一个重要的认识:微服务只是手段,不是目的。
微服务架构是为了让系统变得更容易拓展、更富有弹性。在把单体应用变成靠谱的微服务架构之前,单体系统的各个模块应该是合理、清晰地。
也就是说,从逻辑上单体系统和微服务没有区别,某种理想情况下微服务只是把单体系统的各个模块分开部署了而已
大量的实践教训告诉我们,混沌的微服务架构,比解耦良好的单体应用会带来更多麻烦。
混乱的微服务VS良好的单体
领域驱动设计立足于面向对象思想,从业务出发,通过领域模型的方式反映系统的抽象,从而得到合理的服务划分。
采用 DDD 来进行业务建模和服务拆分时,可以参考下面几个阶段:
- 使用 DDD(领域驱动建模) 进行业务建模,从业务中获取抽象的模型(例如订单、用户),根据模型的关系进行划分限界上下文。
- 检验模型是否得到合适的的抽象,并能反映系统设计和响应业务变化。
- 从 DDD 的限界上下文往微服务转化,并得到系统架构、API列表、集成方式等产出。
使用DDD划分微服务的过程
如何抽象?
抽象需要找到看似无关事务的内在联系,对微服务的设计尤为重要。
然而现实的例子比比皆是,电信或移动营业厅还需要用户分两步办理号卡业务、宽带业务。原始是不合适的抽象模型造成的,并最终影响了微服务的划分。
我们可以使用概念图来描述一些概念的抽象关系。
商品这一概念的概念图
如果没有抽象出领域模型,就得不到正确的微服务划分。
使用DDD进行业务建模
通过利用DDD对系统从业务的角度分析,对系统进行抽象后,得到内聚更高的业务模型集合,在DDD中一组概念接近、高度内聚并能找到清晰的边界的业务模型被称作限界上下文(Bounded Context)。
限界上下文可以视为逻辑上的微服务,或者单体应用中的一个组件。
在电商领域就是订单、商品以及支付等几个在电商领域最为常见的概念;在社交领域就是用户、群组、消息等。
DDD的方法论中是如何找到子系统的边界的呢?
其中一项实践叫做事件风暴工作坊,工作坊要求业务需求提出者和技术实施者协作完成领域建模。把系统状态做出改变的事件作为关键点,从系统事件的角度触发,提取能反应系统运作的业务模型。再进一步识别模型之间的关系,划分出限界上下文,可以看做逻辑上的微服务。
事件是系统数据流中的关键点,类似于电影制作中的关键帧。
例如系统管理员可以登录、创建商品、上架商品,对应的系统状态的改变是用户已登录、商品已创建、商品已经上架;相应的顾客可以登录、创建订单、支付,对应的系统状态改变是用户已登录、订单已创建、订单已支付。
利用事件刺探业务黑盒并抽象出模型
在得到模型之后,通过分析模型之间的关系得出限界上下文。例如商品属性和商品相对于用户、用户组关系更为密切,通过这些关系作出限界上下文拆分的基本线索。
其次是识别模型中的二义性,让限界上下文划分更为准确。
例如,在电商领域,另外一个不恰当设计的例子是:把订单中的订单项当做和商品同样的概念划分到了商品服务
但订单中的商品实际上和商品库中的商品不是同一个概念。当订单需要修改订单下的商品信息时,需要访问商品服务,这势必造成了订单和商品服务的耦合。
合理的设计应该是:商品服务提供商品的信息给订单服务,但是订单服务没有理由修改商品信息,而是访问作为商品快照的订单项。
订单项应该作为一个独立的概念被划分到订单服务中,而不是和商品使用同一个概念,甚至共享同一张数据库表。
典型具有”二义性“陷阱的场景
一组关系密切的模型形成了上下文(context),二义性的识别能帮我们找到上下文的边界(bounded)。
验证和评审领域模型
前面我们说到限界上下文可以作为逻辑上的微服务,但并不意味着我们可以直接把限界上下文变成微服务。
限界上下文被设计出来后,验证它的方法可以从我们采用微服务的两个目的出发:降低耦合、容易扩展,可以作为限界上下文评审原则:
原则1:设计出来的限界上下文之间的互相依赖应该越少越好,依赖的上游不应该知道下游的信息。
原则2:使用潜在业务进行适配,如果能在一定程度上响应业务变化,则证明用它指导出来的微服务可以在相当一段时间内足以支撑应用开发。
但是理想的领域模型往往抽象程度、成本、复用性这几个因素中获取平衡。
”抽象”的成本
用一个简单的图来表达话,我们的领域模型设计往往在复用性和成本取得平衡的中间区域才有实用价值。
几个典型的误区
在大量使用DDD指导微服务拆分的实践后,我们发现很多系统设计存在一些常见的误区
主要分为两类:未成功做出抽象、抽象程度过高、错误的抽象。
1)未成功做出抽象
在实际开发过程中,大家都有一个体会,设计阶段只考虑了一些常见的服务,但是发现项目中有大量可以重用的逻辑,并应该做成单独服务。
当我们在做服务拆分时,遗漏了服务的结果是有一些业务逻辑被分散到各个服务中,并不断重复。
2)抽象程度过高
抽象程度过高最典型的一个特征是得到的限界上下文极端的微小。
抽象程度过高带来的成本有:更多的微服务部署带来的运维压力、开发调试难度提高、服务间通信带来的性能开销、跨服务的分布式事务协调等。因此抽象不是越高越好,应根据实际业务需要和成本考虑。
那相应的,微服务到底应该多小呢?
业界流传一句话来形容,微服务应该多小:“一个微服务应该可以在二周内完成重写“。
这句话可能只是一句调侃,如果真的作为微服务应该多微的标准是不可取的。
微服务的大小应该取决于划分限界上下文时各个限界上下文内聚程度。
3)错误抽象
对微服务或DDD理解不够。模型具有二义性,被放到不同的限界上下文。
例如,订单中的收货地址、用户配置的常用地址以及地址库中的标准地址。
这三种地址虽然名称类似,但是在概念上完全不是一回事
假如架构师将”地址“划分到了标准地址库中,势必会造成用户上下文和系统配置上下文、订单上下文存在不必要的耦合。
抽象错误带来的依赖
上图的右边为正常的依赖关系,左边产生了不正常的依赖,会进一步产生双向依赖。
从限界上下文到系统架构
在通过 DDD 得到领域模型和限界上下文后,理论上我们已经得到了微服务的拆分。但是,限界上下文到系统架构还需要完成下面几件事。
1)设计微服务之间的依赖关系
一个合理的分布式系统,系统之间的依赖应该是非常清晰地。依赖,在软件开发中指的是一个应用或者组件需要另外一个组件提供必要的功能才能正常工作。因此被依赖的组件是不知道依赖它的应用的,换句话说,被调用者不需要知道调用方的信息,否则这不是一个合理的依赖
在微服务设计时,如果 domain service 需要通过一个 from 参数,根据不同的渠道做出不同的行为,这对系统的拓展是致命的。例如,用户服务对于访问他的来源不应该知晓;用户服务应该对订单、商品、物流等访问者提供无差别的服务。
因此,微服务的依赖关系可以总结为:上游系统不需要知道下游系统信息,否则请重新审视系统架构。
2)设计微服务间集成方式
拆分微服务是为了更好的集成到一起,对于后续落地来说,还有服务集成这一重要的阶段。
微服务之间的集成方式会受到很多因素的制约,前面在讨论微服务到底有多微的时候就顺便提到了集成会带来成本,处于不同的目的可以采用不同的集成方式。
- 采用 RPC(远程调用) 的方式集成。
使用RPC的方式可以让开发者非常容易的切换到分布式系统开发中来,但是RPC的耦合性依然很高,同时需要对RPC平台依赖。业界优秀的RPC框架有dubbo、Grpc、thrift等
- 采用消息的方式集成。
使用消息的方式异步传输数据,服务之间使用发布-订阅的方式交互。另外一种思想是通过对系统事件传递,因此产生了 Event Sourcing 这种集成模式,让微服务具备天然的弹性。
- 采用RESTful方式集成。
RESTful是一种最大化利用HTTP协议的API设计方式,服务之间通过HTTP API集成。这种方式让耦合变得极低,甚至稍作修改就可以暴露给外部系统使用。
这三种集成方式耦合程度由高到低,适用于不同的场景,需要根据实际情况选择,甚至在系统中可能同时存在。
服务间集成的方式还有其他方式,一般来说,上面三种微服务集成的方式可以概括目前常见系统大部分需求。
总结
这篇文章主要研讨了DDD火起来的原因, 解决了什么业界难题, 知道DDD主要思路 , 以及DDD大概的实现步骤等 。
逻辑往往比经验更为重要。写这篇文章的初衷是为了得到微服务划分的依据是什么,我该怎么有说服力的回复?
是具体情况具体分析?By experience?还是说,我是通过一套方法对业务逻辑进行分析得到的。
当没有足够的经验直接解决问题,或问题庞大到不足以使用经验解决时,能支撑你做出决策就只有对输入问题进行有效的分析。
使用 DDD 指导微服务划分,能在一定程度上弥补经验的不足,做出有理有据的系统架构设计。