DDD知识沉淀(一):浅谈微服务体系中的分层设计和领域划分
我们主张将其Web服务架构分为五层:基础设施层、领域服务层、应用服务层、网关层和用户界面层(表示层)
其中,各层的职能和作用为:
- 用户界面层:负责向用户显示和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人(比如外部应用调用对应接口)。
- 网关层: 负责提供对外的HTTP服务或者其他应用层协议(这里是指OSI七层协议中的应用层,别混淆了哈)服务。
- 应用服务层:定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使他们互相协作。它没有反应业务情况的状态,但是却可以具有另外一种状态,为用户或者程序显示某个任务的进度。
- 领域服务层:负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反应业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心。
- 基础设施层:为上面各层提供通用的技术能力,为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件(PS:这个在互联网应用中几乎用不到)等等。互联网Web应用系统中基础设施包含了数据持久化服务,中间件服务(数据库,Redis,Memcached,ZooKeeper,ELK等等)以及第三方服务等。
各层除了实现自己的功能外,还需要遵守以下原则:
- 每一层设计保持内聚,并且只依赖于它的下方的层。
- 下层向上层发起的通信只能通过中间件等间接方式进行。
- 上层和下层只能有松散耦合(各自为独立个体,通过简单引用关联)。在某些微服务框架比如Dubbo中,可以把api包提供给上层引用即可。而Spring Cloud的上下层耦合更为松散,通过契约约定即可。前者的优点是调用者可以直接使用提供方定义好的契约和方法。后者的优点则在于最大限度的降低了耦合,避免在上层无限制的进行下层包引入。
这里重点说明应用服务层和领域服务层之间的关系。举一个例子:有一家上市企业A公司,靠卖水果发家,其首席架构师科学合理的按照DDD搭建了一套基于微服务体系的卖水果应用,其架构图如下:
今年水果行情一般,而房地产十分火热,A公司高层发现房地产带动的五金行业也十分火热,于是下达任务给技术部,要求其立即着手搭建五金销售系统,货源已经谈好。得益于首席架构师之前优秀的架构设计,他发现只需要做一个卖五金的网站以及另外对微服务进行微量的调整即可满足老板的需求——因为卖五金和卖水果并无本质区别,他们涉及的环节几乎一致。加入五金售卖的系统架构图如下:
可见应用服务层代表是某一个业务应用,它代表的更多的是从需求出发的应用定义,而领域服务层则是业务领域按照自身的边界进行设计的一个高内聚的服务体。应用层通过协调和组合各个领域服务即可形成一个新的应用服务。《领域驱动设计》中明确指出,在设计领域服务时无需考虑表示层和持久层服务的东西。我在现实开发中总是遇到大量工程师按照产品的设计稿一溜烟的从上至下设计应用层服务和领域层服务,完全没有考虑业务领域的概念,导致后面微服务数量膨胀,功能重复度高。这种开发习惯代表的是《领域驱动设计》作者极力吐槽的一种模式——SMART UI “反模式”。
这里着重讲下应用层,为什么需要加应用层?
对于习惯了单体应用开发者来说,一个微服务很可能就直观对应成了一个个垂直的应用服务,每个服务间的关系是这样的:
其实这样的体系本质上仍然不能解决软件的复杂性,这只是把系统简单粗暴的拆分了,耦合问题仍然很严重,甚至这很有可能比原来的单体应用更复杂(多对多依赖),如果使用微服务体系来处理复杂系统,其服务体系应当是这样的:
这两幅图的区别在于,其实第一幅图中的每个服务都包含了完整的2~3层,所以不再需要单独的应用层。而第二幅图各个领域模块互相协作,对外提供服务时,则需要有一层直面用户需求的应用层。
达成了微服务体系是解决复杂系统的出路之一这个共识后,我们再来看“应用层服务存在的必要性”有哪些理由:
- 统一权限校验:如上文所说,网关层只负责网络级的安全防护,业务层的权限校验则需要应用层来完成,试想一个没有应用层的微服务体系,就意味着每一个微服务都需要加上权限校验逻辑,这不仅编码上困难(可以用过滤器,AOP),而且对于成千上万个微服务(据了解,腾讯目前微服务数量已经超过2万,大众点评有将近千个微服务)来说,这会浪费大量时间,调用链越长,浪费的时间越多。换句话说,微服务体系有一个不突出但是很重要的特征—— 领域间环境安全,领域间的通信应当是可信的 ,否则分布式的缺点(多服务意味着多次通信)会被加剧。
- 业务数据网关:举个例子,一个order-service提供了一个queryOrder的接口,输入起始日期查询对应的订单列表,其有2个消费者:C端网站应用服务 和 报表应用服务 ,C端网站应用服务 只需要知道订单的基本信息如下单时间、商品名称、金额就可以了,而报表应用服务是给管理者看的,需要的订单数据很全,除了C端网站应用服务需要的之外,还需要看平台与商家的结算金额。根据第4部分最后一点的思路,我们肯定不能为调用方写定制接口(写不完的,有的要这个数据,有的要那个数据,每次新增调用方,领域服务还得找人修改)。而如果我们统一使用的全量数据,并且没有应用层(同样的也没有应用层模型DTO了),那么很可能我们吐出去的数据包含了我们与商家的结算价,这会引发很多不必要的麻烦的。所以应用层还充当了业务数据网关的作用,应用层应用服务需要保证仅吐出调用方感兴趣的数据。
- 资源控制和缓存:想象一下双十一高并发的情况,如果查询库存每次都查库是多么恐怖的一件事。所以一般仅在支付的时候做一次库存校验,而在商品展示时查缓存的库存即可。那么问题来了,如果没有应用层,缓存直接放在库存微服务上是否可行呢?首先这会入侵库存领域,库存微服务需要按照调用方的需求做特定时间的缓存,而不是自己想缓存多久就多久,我想库存微服务的开发者也会很不满的,他会提出,让你自己去做缓存。他的方案是科学的,因为还有一些其他服务可能需要实时的数据。这时候就需要有一层来做对其下方微服务返回的数据按照应用自身的需求进行必要的缓存,而不是把这些需求都推给资源提供方,想象一下一个资源提供方有多少需求者,每个需求方都有自己的定制需求,该多痛苦。当然这一点也不是说微服务自身不能做缓存,微服务自身的缓存一定是考虑自身域的合理性后的一个措施(比如订单查询服务会做一个500ms的缓存,因为不会有正常人500ms里点两次查询还必须要求两次都是最新的),而不是由调用方来决定的。
- 资源聚合和加工:其实第2点也有加工的味道在里面,只是这里更多的是描述应用层应用根据自身需求来对下层返回的数据进行聚合和处理的过程。举个例子就能很好的说明这一点:任何APP都有首页,而首页的数据可能是五花八门的,可以有用户昵称、最近下的订单简要信息、最近支出曲线、积分信息等。这4个信息可以来自4个领域微服务,他们是:用户中心、订单中心、支付中心和积分中心。那么有读者会说,直接暴露微服务让前端分别调用4个接口再做聚合不是也行吗?显然这种粗暴的方式是极其不合理的,会额外增加广域网网络调用3次不说,还传输了很多不必要的信息。
- 应用隔离和流控:如果将每个领域服务直接暴露到网关层对外提供服务,那么在多应用场景下,多个应用间是共享这些服务能力的,在服务降级的时候,如果需要按照应用进行降级(比如将优先级不高的应用进行限流),就很难实现。但如果每个应用对应了一个应用层服务,只需要对其暴露的网关接口进行统一限流就行了,或者在应用层做一个开关,将其流量阻止在应用层,而不是拖垮整个领域服务。举个例子,假如我们的平台不仅有自己的网站服务,还有第三方的对接服务,如果某个第三方被攻击而我们直接将领域服务暴露了出去,那么我们就需要在各个领域层服务里去编写对应的开关,这将侵入领域层服务,导致不必要的耦合。而有了应用层这些都不是问题,因为应用层充当了一个调度者的角色,调度者可以很轻松的决定是否调度下层的服务。
领域划分和微服务化
根据DDD理论,领域建模主要发生在领域服务层,各领域模块都应该是高内聚低耦合的,具有清晰的业务边界。本文不打算讨论具体的DDD建模(服务,工厂,仓库,实体,值对象,聚合等),这需要对DDD有较深入的研究,就目前所从事过的公司来看,似乎没有一家真正严格按照DDD进行项目代码设计的,就像摘要中说的,这对整个软件工程链路上的人员都有较高的要求。有机会可以单独写一篇关于自己对DDD建模的思考和建议,本文更多的是讨论高视角下的领域服务拆分,从而搭建一个低耦合高内聚的微服务体系。如果一定要将微服务和DDD联系起来的话,领域层的微服务就对应了DDD中的领域模块Module,每个Module由多个Service模式对象以及对应的模型对象(实体, 值对象以及它们的聚合)组成。
从《领域驱动设计:软件核心复杂性应对之道》中我学到的主要有两块:领域设计思想和领域建模模式。本文更多的是对前者的运用,后者的对立模式是贫血模型,大家日常用到的也都是贫血模型,我也觉得贫血模型有存在的必要性,所以本文我们主要从其中借鉴一下领域设计思想。本文所描述的设计理念,并不影响具体的模型设计方法,我们仍然可以在每个微服务中使用DDD领域建模。
如何切分领域模块并没有一个明确的规则,不同的场景下可能相同的业务块边界也不尽相同。这里提几点领域划分的个人心得:
- 领域设计一定要有清晰的功能边界。一个领域服务对应了一个功能集合,这些功能一定是有一些共性的。比如,订单服务,那么创建订单、修改订单、查询订单列表,一般是订单域的功能集合。
- 领域拆分并不是一步到位的,应当根据实际情况逐步展开。从单体应用到微服务体系的拆分过程能很好的说明这个问题,一上来拆的很细的改造方案一定会死的很惨。所以如果一开始不知道应该划分多细,完全可以先粗粒度划分,然后随着需要,初步拆分。比如一个电商一开始索性可以拆分为商品服务和交易服务,一个负责展示商品,一个负责购买支付。随后随着交易服务越来越复杂,就可以逐步的拆分成订单服务和支付服务。
- 领域拆分并不是一成不变的,应当具体情况具体分析。2015年在大众点评的时候,其订单服务就拆分为了order-service和order-query-service,一来为了读写分离,二来order-query-service作为单独应用可以按需水平扩容。
- 领域可以是多个子领域的一个虚拟集合,换句话说多个微服务也可以形成一个大域,不必纠结于领域和微服务之间的数量对应关系。我们在做架构设计PPT的时候可能就把订单域作为一个领域,代表了这个域就是关于订单的,具体该有几个微服务,这需要更细的详细设计来提供。
- 领域层服务设计应当是调用者无关的。这一点有点像第一点,但是它强调的是领域层服务的设计不应该受调用者的影响,这个观点在《领域驱动设计:软件核心复杂性应对之道》这本书里也可以找得到。领域层服务开发和设计的理念是关注自己的域,一旦边界划分清楚了,开发所需要考虑的永远都只是输入和输出,提供的服务一定是尽可能通用的,面向功能来开发的,而不是面向调用方来开发的。比如某个调用方提出了一个需求:调用方B希望A服务提供一个买汽车的接口,那么A服务设计的接口就应该是buyCar(),而不是buyCarForA()。
原文地址:https://tbwork.org/2018/10/25/layed-dev-arch/