在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题:
谈谈你的DDD落地经验?
谈谈你对DDD的理解?
如何保证RPC代码不会腐烂,升级能力强?
微服务如何拆分?
微服务爆炸,如何解决?
最近有小伙伴在字节,又遇到了相关的面试题。小伙伴懵了, 他从来没有用过DDD,面挂了。关于DDD,尼恩之前给大家梳理过一篇很全的文章:阿里一面:谈一下你对DDD的理解?2W字,帮你实现DDD自由
但是尼恩的文章, 太过理论化,不适合刚入门的人员。所以,尼恩也在不断的为大家找更好的学习资料。
前段时间,尼恩在阿里的技术公众号上看到了一篇文章《殷浩详解DDD:领域层设计规范》 作者是阿里 技术大佬殷浩,非常适合与初学者入门,同时也足够的有深度。
美中不足的是, 殷浩那篇文章的行文风格,对初学者不太友好, 尼恩刚开始看的时候,也比较晦涩。
于是,尼恩在读的过程中,把哪些晦涩的内容,给大家用尼恩的语言, 浅化了一下, 这样大家更容易懂。
本着技术学习、技术交流的目的,这里,把尼恩修改过的 《殷浩详解DDD:领域层设计规范》,通过尼恩的公众号《技术自由圈》发布出来。
特别声明,由于没有殷浩同学的联系方式,这里没有找殷浩的授权,
如果殷浩同学或者阿里技术公众号不同意我的修改,不同意我的发布, 我即刻从《技术自由圈》公众号扯下来。
另外, 文章也特别长, 我也特别准备了PDF版本。如果需要尼恩修改过的PDF版本,也可以通过《技术自由圈》公众号找到尼恩来获取。
本文是 《阿里DDD大佬:从0到1,带大家精通DDD》系列的第5篇, 《从0到1,带大家精通DDD》系列的第3篇, 第1、2、3、4篇的链接地址是:
《阿里DDD大佬:从0到1,带大家精通DDD》
《阿里大佬:DDD 落地两大步骤,以及Repository核心模式》
《阿里面试:让代码不腐烂,DDD是怎么做的?》
《阿里大佬:DDD 领域层,该如何设计?》
大家可以先看第1篇、第2篇,第3篇,第4篇, 再来看第5篇,效果更佳。
另外,尼恩会结合一个工业级的DDD实操项目,在第34章视频《DDD的顶奢面经》中,给大家彻底介绍一下DDD的实操、COLA 框架、DDD的面试题。
微服务架构是为了让系统变得更容易拓展、更富有弹性。
所以,因此各大公司纷纷转向微服务架构,但是在实际的微服务拆分过程中也会遇到不少的问题。甚至会导致 微服务爆炸。
尼恩曾经见过,一个单体应用被拆分成40个微服务, 带来非常大的部署、运维成本。
拆成微服务之后,部署一套这样的系统,需要三天甚至一周的时间,痛苦不堪。这就是所谓的微服爆炸。
如何合理的进行微服务拆分,减少微服务爆炸?
DDD 中的领域模型构建以及边界上下文的划分,天然的和微服务划分有着异曲同工之妙,因此结合 DD 领域驱动设计来进行微服务拆分是一种比较好的微服务拆分方案。
在进行微服务拆分之前,我们首先应该搞清楚为什么要进行微服务拆分?
微服务拆分后会带来怎样的业务价值?在后期维护上面会不会比以前的维护成本更低?
先看看单体应用在业务不断发展的过程中会遇到怎样的问题。
随着业务的不断发展,单体应用的功能越来越多,需求不断变化,修改不断进行,单个应用多团队维护就会出现各种团队协作问题,不知不觉中降低了产品的研发效能。而且由于各个业务模块杂糅在一起,一个需求过来后,到底改哪个团队来做,经常在开会的时候吵得脸红脖子粗,增加了时间以及沟通成本。
进行需求迭代的时候,也许只修改了某个模块的功能,但是每次发布都是一整个大的包进行发布,其中构建、发包的时间成本会随着应用的迭代逐渐增加,导致整个需求上线过程变更显得十分笨重,不利于进行团队规模的敏捷开发。
由于单体应用故障隔离范围只是线程级别的,单体应用可能会由于某个模块的功能有问题而导致整个服务平台的不可用,因此平台稳定性方面显然不能满足经常变化的业务发展的需要。
鉴于上述问题,我们需要对大泥球似的单体大应用进行合理拆分,以便于适用业务的快速发展。微服务架构拆分之后,团队成员不用都围绕一个大泥球应用转了,根据拆分的不同的业务域,各自负责自己的业务域,维护起来相对来说更加方便。
同事如果有需求迭代,没有功能修改的业务域可以不用发生变更,不需要进行重新部署,大大降低了修改变更导致的平台稳定性性问题。另外由于是微服务分布式架构,不再是单点应用,不再存在单点问题,性能方面也会有所提升。
微服务到底应该怎么拆呢?
应该按照怎样的标准来进行拆分呢?
DDD 的理论中提供了我们进行领域驱动设计的指导方针,对于我们进行微服务的拆分具备天然的指导意义。
DDD 指导我们首先要对当前系统平台的业务进行全面的分析,可以通过用例分析法、事件风暴法以及四色建模法来进行业务分析,使用统一的业务语言进行业务领域建模以及边界上下文的划定,后面我们就可以根据边界上下文来进行具体的微服务的划分了。
采用 DDD 来进行业务建模和服务拆分时,可以参考下面几个阶段:
所谓业务能力就是平台的具体实现的业务功能是什么,
比如:在电商业务中物流域我们按照业务可以划分为仓储、运输、配送、计费等业务领域。大的领域划分出来之后,我们可以用真实的业务流程来串联这些业务领域。
当业务流程经过这些业务领域的时候,必定会触发一些领域事件,经历一些业务流程,那么在这个过程中我们就可以梳理出对应的实体、值对象以及聚合根,我们将具有紧密业务逻辑关系的实体以及值对象收敛在聚合根的周围,从而形成聚合。
例如在仓储领域中就会涉及到入库、库内操作以及出库这三大流程,其中入库主要包括质检、收货以及上架。
这其中涉及到的实体主要用入库单、货品、操作员等,其中入库单就是聚合根,通过它可以将货品、操作员等实体以及值对象聚合起来,形成入库聚合。
DDD的方法论中,是如何找到上下文边界呢?
事件风暴法中,要求业务需求提出者和技术实施者协作完成领域建模。
把系统状态做出改变的事件作为关键点,从系统事件的角度出发,提取能反应系统运作的业务模型。再进一步识别模型之间的关系,划分出限界上下文,限界上下文可以看做逻辑上的微服务。
事件是系统数据流中的关键点,类似于电影制作中的关键帧。
在未建立模型之前,系统就像是一个黑盒,不断的刺探系统的状态的变化就可以识别出某种反应系统变化的实体。
例如:系统管理员可以登录、创建商品、上架商品,对应的系统状态的改变是用户已登录、商品已创建、商品已经上架;
相应的,顾客可以登录、创建订单、支付,对应的系统状态改变是用户已登录、订单已创建、订单已支付。
于是可以通过收集上面的事件看出:
商品相关事件是对系统中商品状态做出的改变,商品可以表达系统中某一部分,商品可以作为模型”。
订单相关事件是对系统中订单状态做出的改变,订单可以表达系统中某一部分,订单可以作为模型”。
在得到模型之后,通过分析模型之间的关系得出限界上下文。
例如商品属性和商品相对于用户、用户组关系更为密切,通过这些关系作出限界上下文拆分的基本线索。
其次是识别模型中的二义性,进一步让限界上下文更为准确。
在电商领域,另外一个不恰当设计的例子是:
把订单的订单项当做和商品同样的概念划分到了商品服务,当订单需要修改订单下的商品信息时,需要访问商品服务,这势必造成了订单和商品服务的耦合。
合理的设计应该是:
商品服务提供商品的信息给订单服务,但是订单服务没有理由修改商品信息,而是访问作为商品快照的订单项。订单项应该作为一个独立的概念被划分到订单服务中,而不是和商品使用同一个概念,甚至共享同一张数据库表。
典型具有”二义性“陷阱的场景,如下:
”地址“和”商品“在不同的系统中实际上表达不同的含义,这就是术语”上下文“的由来。
一组关系密切的模型形成了上下文(context),二义性的识别能帮我们找到上下文的边界(bounded)。
前面我们说到限界上下文可以作为逻辑上的微服务,并不意味着我们可以直接把限界上下文变成微服务。
在这之前很重要的一件事情是对模型进行验证,如果我们得到的限界上下文被抽象的不良好,在微服务实施后并不能得到良好的拓展性和重用。
限界上下文被设计出来后,验证它的方法可以从我们采用微服务的两个目的出发:降低耦合、容易扩展,可以作为限界上下文评审原则:
一般抽象程度的领域模型
上图是一个电信运营商的领域模型的局部,这部分展示了电信号码资源以及群组、用户、宽带业务、电话业务这几个限界上下文。主要业务逻辑是,系统提供了号码资源,用户在创建时会和号码资源进行绑定写卡操作,最后再开通电话或宽带业务。在开通电话这个业务流程中,号码资源并不需要知道调用者的信息。
但是理想的领域模型往往抽象程度、成本、复用性这几个因素中获取平衡,软件设计往往没有理想的领域模型,大多数情况下都是平衡各种因素的苟且,因此评审领域模型时也要考虑现实的制约。
“抽象”的成本
用一个简单的图来表达话,我们的领域模型设计往往在复用性和成本取得平衡的中间区域才有实用价值。
前面电信业务同样的场景,业务专家和架构师表示,我们需要更为高度的抽象来满足未来更多业务的接入,因此对于两个业务来说,我们需要进一步抽象出产品和订单的概念。
但是同时需要注意到,我们最终落地时的微服务会变得更多,也变得更为复杂,当然优势也是很明显的 —— 更多的业务可以接入订单服务,同时订单服务不需要知道接入的具体业务。对于用户的感知来说,可以一次办理多个业务并统一支付了,这正是某电信当前的痛点之一。
高度抽象的领域模型
这里的通用服务其实包含两个意思,
所谓通用服务就是在各个微服务之间都会碰到的问题,比如说接口的鉴权、日志的监控和管理、服务状态的监控和管理以及服务幂等等分布式系统问题。
因此,我们需要将这些微服务的通用服务进行统一的抽象,形成通用的基础服务,这样微服务本身只需要关注自身的业务,这些微服务通用的能力由单独的基础服务来进行实现就 OK 了。
我们还是拿大家最熟悉的电商业务来举个栗子吧,
电商的业务形态有很多种,就阿里巴巴来说,有淘宝、天猫、主打生鲜的盒马、天猫超市等等。
不管上层的业务形态有怎样的变化,实际上他们都是有比较核心的业务域是通用的,比如用户、支付、仓储、物流等等。
那么实际上这些通用的业务对于整个电商平台来说实际就是通用能力,因此我们需要将这些通用的公共的能力进行下沉,形成业务中台,实现企业级的通用业务能力复用。
在进行微服务拆分的过程中,有几条笔者总结的原则大家可以参考下,在实操的时候如果没有原则来遵循,
实际我们自己也没办法去评判微服务拆分的效果到底有没有达到我们的预期。
大致的原则如下:
1.功能性原则:微服务尽可能的按照业务逻辑上的功能去拆分,保证每个模块只包含一种主要的业务能力。
2.低耦合原则:微服务拆分的最终目的是要使系统中的各个模块之间兼容性最大化,稳定性最高,避免出现耦合性太强,维护和开发困难的情况。
在进行微服务拆分之前,应该对平台进行完整的领域划分,建立合适的领域模型,确定好边界上下文,并以此作为微服务拆分的指导。
隔离变与不变:将领域模型的稳定与不断变化的外部需求进行隔离,保证核心领域模型的稳定,避免领域模型之间的强依赖。从而达到实现微服务高内聚低耦合的目的。
3.可复用原则:微服务拆分的时候,在可复用的地方要做到复用,但复用并不会改变各单元功能的相对稳定性和可拆分性。
4.扩展性原则:微服务拆分的时候,要做到不同模块之间的功能关系简单清晰,尽可能保证不同单元的拓展性。
5.服务拆分要把握度, 避免过度拆分,导致微服务爆炸
如果在微服务拆分过程中发生过度拆分,就会导致微服务爆炸的情况。
微服务爆炸不可避免的增加软件系统的维护成本,同时由于拆分也会导致业务流程变长.
原本一两个服务就完成的业务,拆分后需要在五六个甚至更多的微服务才能完成,增加了平台出现 Bug 的概率,不知不觉中降低了平台的稳定性。
微服务爆炸意味着需要更多的服务器资源,从而在无形中增加了业务成本。因此我们可以借助于 DDD 划分的边界上下文,防止微服务过度拆分情况的发生。
以上内容如果没有弄清楚,尼恩会在 《第34章:DDD的顶奢面经》,为大家通过视频做深入解读。
Uber 2018 年微服务架构依赖关系图
随着Uber业务的发展,微服务个数达到了近 2200 个,微服务爆炸式的增长以及依赖关系的复杂性,为Uber带来了不少问题。
使用微服务架构,单体变成了很多个黑盒,黑盒的功能随时都可能发生变化,很容易导致意外行为发生**。
1)例如,为了找到一个问题的根源,工程师们需要跨 12 个不同的团队,检查大约 50 个服务。
2)理解服务之间的依赖关系变得相当困难,因为服务之间的调用层次可能会非常深; 一个依赖项的延迟会导致上游出现级联问题。如果没有对的工具,是不可能了解系统中发生了什么事情的,调试也变得非常困难。
3)为了构建一个简单的特性,工程师通常要跨多个服务,而这些服务可能属于不同的个人和团队。这需要在会议、设计和代码审查方面花费大量时间进行协作。
4)当团队在彼此的服务中修改代码,修改彼此的数据模型,甚至代替服务所有者进行部署时,原本清晰的服务所有权边界就被破坏了。于是就形成了分布式单体,为了部署一个变更,原本看似独立的服务必须一起部署。
微服务变多之后带来了问题,总结一下 :
调用依赖多导致的开发协作变多、链路变长理解以及排查困难、部署依赖导致需要同时发布。
那么,Uber是怎么解决这些问题的呢?
答案是: 面向领域微服务架构 DOMA。
“面向领域微服务架构”(Domain-Oriented Microservice Architecture,DOMA)借鉴了已有的组织代码的方法,如领域驱动设计、Clean Architecture、面向服务架构,以及面向对象和面向接口的设计模式。
我们认为 DOMA 的创新之处在于,它是在大型组织的大型分布式系统中利用已有的设计原则建立起来的一种相对新颖的方式。
与 DOMA 相关的核心原则和术语如下:
领域:
我们不是围绕单个微服务,而是围绕相关的微服务集合。我们把这个叫作领域。
分层设计:
我们进一步创建领域集合,称之为层。
领域所属的层建立了领域微服务可以拥有的依赖关系。我们把这个叫作分层设计。
网关:
我们为集合的单一入口点提供了干净的接口,称之为网关。
**扩展:**每个领域应该与其他领域无关
也就是说,一个领域不应该在其代码库或数据模型中硬编码与另一个领域相关的逻辑。
由于团队经常需要在另一个团队的领域里包含逻辑 (例如,自定义验证逻辑或数据模型的元上下文),我们提供了一个扩展架构来支持领域的扩展点。
换句话说,通过提供系统性架构、领域网关和预定义扩展点,DOMA 让微服务架构变得更好理解: 一组灵活、可重用和分层的结构化组件。
Uber 领域表示一个或多个微服务的集合,这些微服务与功能的逻辑分组相关联。
在设计领域时的一个常见问题是“一个领域应该多大?”
我们在这里不提供任何建议。有些领域可以包含数十个服务,有些领域只能包含一个服务。
关键是要仔细思考每个集合的逻辑角色。
例如,我们的地图搜索服务构成一个领域,收费服务是一个领域,匹配平台 (匹配乘客和司机) 是一个领域。
这些也并不总是遵循公司的组织结构。
Uber Maps 本身被分为三个领域,在 3 个不同的网关后面部署了 80 个微服务。
分层设计回答了在优步的微服务架构中“什么服务可以调用其他服务?”。
因此,我们可以将分层设计看作是“规模化的关注点分离”。
或者,我们可以把分层设计看作是“规模化的依赖管理”。
分层设计描述了一种机制,用于考虑Uber服务依赖关系中的故障影响范围和产品专用性。
随着domain从底层迁移到顶层,它们在停机时影响的服务更少,并代表更具体的产品用例。
相反,底层的功能具有更多的依赖关系,因此往往具有更大的故障影响范围,并代表一组更通用的业务功能。
下图说明了这个概念。
我们可以将顶层看作是特定的用户体验(如移动功能),而底层则是通用的业务功能(如帐户管理)。
特定层只依赖于它们下面的层,这为我们思考故障影响范围和domain集成等问题提供了有益的启发。
值得注意的是,功能通常会从这个图表的特定部分“向下”移动到更一般的部分。
可以想象,随着需求的发展,一个简单的特性最终会越来越像一个平台。
事实上,这种向下迁移是可预见的,优步的许多核心业务平台一开始是乘客或司机特定的功能,随着我们发展更多的业务线,它们变得更加一般化,并具有更多的依赖性(如Uber送餐或货运)。
在Uber内部,我们建立了以下五个层。
正如您所看到的,每个后续层都代表了一个日益特定的功能分组,并且具有越来越小的故障影响范围(换句话说,依赖于该层功能的组件越来越少)
“网关”在微服务架构中已经是一个很广泛的概念。
我们的定义与已有的定义差别不大,只是我们倾向于将网关看作是底层服务集合 (我们称之为领域) 的单个入口点。
Uber的网关视图示例,这是网关的一个高级视图。
网关隐藏了领域的内部细节:微服务、数据表、ETL 管道等
网关只有接口被公开给其他领域:RPC API、消息传递事件和查询。
由于上游只需要调用网关,而不是依赖某个领域中可能存在的多个下游服务,所以,网关在未来迁移、可发现性和降低系统复杂性方面提供了许多好处。
如果我们从面向对象设计的角度来,网关就是接口定义,让我们能够基于底层“实现”(在这里就是底层微服务集合) 做任何我们想做的事情。
我们可以看到,从逻辑上划分【业务领域】,从物理上使用【网关】聚合领域内的微服务对外接口封装,从而减少调用方对内部多个微服务细节的了解,体现了抽象和封装的价值。
扩展表示一种扩展域的机制。扩展的基本定义是,它提供了一种扩展基础服务功能的机制,而无需更改该服务的实际实现,也不会影响其整体可靠性。在Uber,我们提供了两种不同的扩展模型:逻辑扩展和数据扩展。扩展的概念使我们能够将架构扩展到能够独立工作的多个团队。
逻辑扩展提供了一种扩展服务的底层逻辑机制。
对于逻辑扩展,我们使用提供程序或插件模式的变体,其接口是以服务为基础定义的。
这样一来, 使得扩展团队可以在不修改底层平台核心代码的情况下,以接口驱动的方式实现扩展逻辑。
如,一个司机上线到系统的api接口,通常,我们会进行各种检查,来确保司机可以运营(安全检查、合规等)。每一个检查都属于一个单独的团队。
实现这一api接口的一种方法,是让每个团队在同一个接口编写各自的处理逻辑,但是,这可能会引入复杂性。并且,每个检查都需要定制的、完全不相关的逻辑。
怎么做逻辑扩展?
或者说,怎么把上线接口将变成一个可以逻辑扩展的接口。
一般情况是,通过插件式的机制,进行检查的扩展,每个扩展都符合预定义的请求类型和响应。每个团队将注册一个负责执行此逻辑的扩展。在这种情况下,它们可能只是取一些关于驱动程序的上下文,然后返回一个bool,说明司机是否可以上线。api接口将简单地遍历这些响应,并确定其中是否有错误。
尼恩在《10Wqps Netty api 网关架构与实操视频》中,无论是负载均衡策略组件、还是请求处理过滤器组件,都采用的是这种逻辑扩展式的、插件式的架构。
逻辑扩展的架构的优势:将核心代码与每个扩展解耦,并提供了扩展之间的隔离,它不知道其他逻辑在执行什么。围绕这一点,就能很容易建立更多的功能,比如可观察性等。
数据扩展提供了一种将任意数据附加到接口的机制,来避免核心平台数据模型中的臃肿。
对于数据扩展,我们利用Protobuf的Any功能,这样团队可以将任意数据添加到请求中。
服务通常会存储这些数据或将其传递给逻辑扩展,这样核心平台就永远不会负责反序列化(从而 “知道”)这个任意上下文。
Any 定义的变量就是一个基础类,就像Java中的Object.class。
类似于声明变量,然后可以转成自己需要的任意类
在java中封包和拆包的参考代码:
Foo foo = ...;
Any any = Any.pack(foo);
...
if (any.is(Foo.class)) {
foo = any.unpack(Foo.class);
}
在逻辑和数据扩展之外,Uber的很多团队都推出了自己适合自己领域的扩展模式。
例如,与我们的展示架构绑定的很多集成都使用了基于DAG的任务执行逻辑。
DOMA 是 Uber 产品和平台团队一致努力的结果。平台支持成本通常下降一个数量级。产品团队从护栏和加速开发中获益。
例如,我们的扩展体系结构的早期平台消费者能够通过采用一种扩展体系结构,将划分优先级和集成新功能的时间从三天缩短到三小时,从而缩短了消费者的代码审查、规划和学习曲线时间。
以前,产品团队需要调用许多下游服务来利用一个域;使用DOMA架构之后,现在他们只需要调用一个。
通过减少接触点数量,平台能够将登录时间减少 25-50%。此外,我们能够将 2200 个微服务分为 70 个域。其中大约有 50%已经实施,而且大多数都有一些未来采用的计划。
DDD 面试题,是非常常见的面试题。 DDD的学习材料, 汗牛塞屋,又缺乏经典。
《殷浩详解DDD:领域层设计规范》做到从0到1带大家精通DDD,非常难得。
这里,把尼恩修改过的 《殷浩详解DDD:领域层设计规范》,通过尼恩的公众号《技术自由圈》发布出来。
大家面试的时候, 可以参考以上的内容去组织答案,如果大家能做到对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。
另外在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
当然,关于DDD,尼恩即将给大家发布一波视频 《第34章:DDD的顶奢面经》。
https://juejin.cn/post/7042154456644845575
https://zhuanlan.zhihu.com/p/334999899
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓