电商微服务体系中分层设计和领域的划分
- 前言 -
比起“高并发、多线程”、“分布式CAP、一致性、Paxos”、“高可用SLA”等具体的干货技术点,软件体系知识显得很“湿”,似乎人人都有自己的认识,但又很少有人能说完整。
有一点可以确定的是,如果你未来需要独立设计一个复杂的系统中台,并使之未来能快速应对各种需求变化的话,科学合理的领域划分和边界界定需要我们“处女座级”的坚持下去,这对防止人力失控、减少项目烂尾很有帮助。合理的界定了边界后,即便某个微服务很糟糕,也可以就输入输出以很少的人力投入进行重构,相反的就是牵一发而动全身,加上业务需求频繁而来,很容易烂尾或是达不到如期的效果。
其实很多技术大神都是某一个技术点的好手,但可能在整体软件体系上思考并不多,每个人都有自己的设计方法,大部分容易想到的设计方法处理一般的系统已经够了,后面发生问题慢慢打补丁就行了,当我们面对各种需求变化陷入开发困境的时候我们就该想想了,咱们系统的体系设计上是否出了问题?本文不打算涉及领域建模和设计模式等代码级别的详述,而是探讨如何将一个复杂的大系统进行分层和拆分,这是设计一个优美系统的第一步,相信对各BU同事们快速搭建系统中台也是很有参考意义的。
文中的一些例子大家也可能遇到过,大家如果在开发中遇到困境,可以多来圈子交流和发表问题,大家一起学习进步。大概知道内容背景的可以直接跳到第3部分。想了解一个大项目如何进行科学人员安排的可以直接看5.4部分。如果你的组里还有人把数据库模型当接口契约用,可以建议他看下5.1部分。假如你在开发过程中遇到一些别人的开发设计习惯,你觉得不是很好,但是又不知道如何说服他,都可以到评论区聊聊,大家一起讨论讨论。
- 摘要 -
- 背景介绍 -
- 分层设计 -
用户界面层: 负责向用户显示和解释用户指令。 这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人(比如外部应用调用对应接口)。 前后端分离时,该层可以扮演前端所需数据的组装者。 网关层: 负责提供对外的HTTP服务或者其他应用层协议(这里是指OSI七层协议中的应用层,别混淆了哈)服务。 应用服务层: 定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。 这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道。 应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使他们互相协作。 它没有反应业务情况的状态,但是却可以具有另外一种状态,为用户或者程序显示某个任务的进度。 领域服务层: 负责表达业务概念,业务状态信息以及业务规则。 尽管保存业务状态的技术细节是由基础设施层实现的,但是反应业务情况的状态是由本层控制并且使用的。 领域层是业务软件的核心。 基础设施层: 为上面各层提供通用的技术能力,为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件(PS: 这个在互联网应用中几乎用不到)等等。 互联网Web应用系统中基础设施包含了数据持久化服务,中间件服务(数据库,Redis,Memcached,zookeeper,ELK等等)以及第三方服务等。
每一层设计保持内聚,并且只依赖于它的下方的层。
下层向上层发起的通信只能通过中间件等间接方式进行。
上层和下层只能有松散耦合(各自为独立个体,通过简单引用关联)。在某些微服务框架比如Dubbo中,可以把api包提供给上层引用即可。这也符合依赖倒置原则。
- 领域划分和微服务化 -
从《领域驱动设计:软件核心复杂性应对之道。》中我学到的主要有两块:领域设计思想和领域建模模式。本文更多的是对前者的运用,后者的对立模式是贫血模型,大家日常用到的也都是贫血模型,我也觉得贫血模型有存在的必要性,所以本文我们主要从其中借鉴一下领域设计思想。本文所描述的设计理念,并不影响具体的模型设计方法,我们仍然可以在每个微服务中使用DDD领域建模。
领域设计一定要有清晰的功能边界。 一个领域服务对应了一个功能集合,这些功能一定是有一些共性的。 比如,订单服务,那么创建订单、修改订单、查询订单列表,一般是订单域的功能集合。 领域拆分并不是一步到位的,应当根据实际情况逐步展开。 从单体应用到微服务体系的拆分过程能很好的说明这个问题,一上来拆的很细的改造方案一定会死的很惨。 所以如果一开始不知道应该划分多细,完全可以先粗粒度划分,然后随着需要,初步拆分。 比如一个电商一开始索性可以拆分为商品服务和交易服务,一个负责展示商品,一个负责购买支付。 随后随着交易服务越来越复杂,就可以逐步的拆分成订单服务和支付服务。 领域拆分并不是一成不变的,应当具体情况具体分析。 2015年在大众点评的时候,其订单服务就拆分为了order-service和order-query-service,一来为了读写分离,二来order-query-service作为单独应用可以按需水平扩容。 领域可以是多个子领域的一个虚拟集合,换句话说多个微服务也可以形成一个大域,不必纠结于领域和微服务之间的数量对应关系。 我们在做架构设计PPT的时候可能就把订单域作为一个领域,代表了这个域就是关于订单的,具体该有几个微服务,这需要更细的详细设计来提供。 领域层服务设计应当是调用者无关的。 这一点有点像第一点,但是它强调的是领域层服务的设计不应该受调用者的影响,这个观点在《领域驱动设计: 软件核心复杂性应对之道》这本书里也可以找得到[4]。 领域层服务开发和设计的理念是关注自己的域,一旦边界划分清楚了,开发所需要考虑的永远都只是输入和输出,提供的服务一定是尽可能通用的,面向功能来开发的,而不是面向调用方来开发的。 比如某个调用方提出了一个需求: 调用方B希望A服务提供一个买汽车的接口,那么A服务设计的接口就应该是buyCar(),而不是buyCarForB()。
- Q&A -
能不能在所有层使用数据持久层模型,简单快捷?
应用层对接网关层,是向面向C端或者调用者的一个数据出口,但是调用者只需要这个出口输出用户感兴趣的数据,并且有些敏感数据不能吐出去。 如果应用层(面向调用者)使用的仍然是数据库模型,而开发人员没有在应用层把无关值置空的话(相信我,需求一多,工作一忙,鬼才会在意这些细节),那么数据库里的整条记录就作为接口输出吐出去了。 比如订单记录,用户订单列表可能只需要订单ID,商品名称,订单金额。 而像商家结算价这种就不能吐出去,万一被有心人察觉到了,用户一定会投诉——你跟商家结算价200,卖给我400? 前端或者接口调用方会很痛苦,一个接口契约多一两个无关字段是没关系的,但是一个契约里给的30个字段,我只用到5个,前端会骂娘的,我亲眼见过这种事,设计原则里有个原则叫”迪米特法则”,也叫最小知识原则,接口设计也可以参考这个原则,尽量让你的调用方知道尽可能少的信息点就能完成相关的任务。 “一层一模型”本质是解耦模型依赖。 我在上家公司做架构师时为了兼顾开发的感受,决定让他们可以在领域层和基础设施层都是用数据持久层模型,而只需要在应用层做数据控制(解决第一个问题),然而我的妥协也慢慢露出弊端,开发有时候觉得某个数据库字段命名不合适修改之后,整个引用了该模型的微服务都需要修改,如果一层一模型的话,只需要关联数据库访问的服务修改下DPO和DM的映射就行了,其他上层微服务都是依赖DM的。 虽然我们不鼓励随意改动数据库字段,但设计框架上最好能支持这种情况。
为啥需要应用层,领域层微服务直接通过网关暴露不就行了吗?
这两幅图的区别在于,其实第一幅图中的每个服务都包含了完整的2~3层,所以不再需要单独的应用层。而第二幅图各个领域模块互相协作,对外提供服务时,则需要有一层直面用户需求的应用层。
统一权限校验: 如上文所说,网关层只负责网络级的安全防护,业务层的权限校验则需要应用层来完成,试想一个没有应用层的微服务体系,就意味着每一个微服务都需要加上权限校验逻辑,这不仅编码上困难(可以用过滤器,AOP),而且对于成千上万个微服务(据了解,腾讯目前微服务数量已经超过2万,大众点评有将近千个微服务)来说,这会浪费大量时间,调用链越长,浪费的时间越多。 换句话说,微服务体系有一个不突出但是很重要的特征—— 领域间环境安全,领域间的通信应当是可信的 ,否则分布式的缺点(多服务意味着多次通信)会被加剧。 业务数据网关: 举个例子,一个order-service提供了一个queryOrder的接口,输入起始日期查询对应的订单列表,其有2个消费者: C端网站应用服务 和 报表应用服务 ,C端网站应用服务 只需要知道订单的基本信息如下单时间、商品名称、金额就可以了,而报表应用服务是给管理者看的,需要的订单数据很全,除了C端网站应用服务需要的之外,还需要看平台与商家的结算金额。 根据第4部分最后一点的思路,我们肯定不能为调用方写定制接口(写不完的,有的要这个数据,有的要那个数据,每次新增调用方,领域服务还得找人修改)。 而如果我们统一使用的全量数据,并且没有应用层(同样的也没有应用层模型DTO了),那么很可能我们吐出去的数据包含了我们与商家的结算价,这会引发很多不必要的麻烦的。 所以应用层还充当了业务数据网关的作用,应用层应用服务需要保证仅吐出调用方感兴趣的数据。 资源控制和缓存: 想象一下双十一高并发的情况,如果查询库存每次都查库是多么恐怖的一件事。 所以一般仅在支付的时候做一次库存校验,而在商品展示时查缓存的库存即可。 那么问题来了,如果没有应用层,缓存直接放在库存微服务上是否可行呢? 首先这会入侵库存领域,库存微服务需要按照调用方的需求做特定时间的缓存,而不是自己想缓存多久就多久,我想库存微服务的开发者也会很不满的,他会提出,让你自己去做缓存。 他的方案是科学的,因为还有一些其他服务可能需要实时的数据。 这时候就需要有一层来做对其下方微服务返回的数据按照应用自身的需求进行必要的缓存,而不是把这些需求都推给资源提供方,想象一下一个资源提供方有多少需求者,每个需求方都有自己的定制需求,该多痛苦。 当然这一点也不是说微服务自身不能做缓存,微服务自身的缓存一定是考虑自身域的合理性后的一个措施(比如订单查询服务会做一个500ms的缓存,因为不会有正常人500ms里点两次查询还必须要求两次都是最新的),而不是由调用方来决定的。 资源聚合和加工: 其实第2点也有加工的味道在里面,只是这里更多的是描述应用层应用根据自身需求来对下层返回的数据进行聚合和处理的过程。 举个例子就能很好的说明这一点: 任何APP都有首页,而首页的数据可能是五花八门的,可以有用户昵称、最近下的订单简要信息、最近支出曲线、积分信息等。 这4个信息可以来自4个领域微服务,他们是: 用户中心、订单中心、支付中心和积分中心。 那么有读者会说,直接暴露微服务让前端分别调用4个接口再做聚合不是也行吗? 显然这种粗暴的方式是极其不合理的,会额外增加广域网网络调用3次不说,还传输了很多不必要的信息。 应用隔离和流控: 如果将每个领域服务直接暴露到网关层对外提供服务,那么在多应用场景下,多个应用间是共享这些服务能力的,在服务降级的时候,如果需要按照应用进行降级(比如将优先级不高的应用进行限流),就很难实现。 但如果每个应用对应了一个应用层服务,只需要对其暴露的网关接口进行统一限流就行了,或者在应用层做一个开关,将其流量阻止在应用层,而不是拖垮整个领域服务。 举个例子,假如我们的平台不仅有自己的网站服务,还有第三方的对接服务,如果某个第三方被攻击而我们直接将领域服务暴露了出去,那么我们就需要在各个领域层服务里去编写对应的开关,这将侵入领域层服务,导致不必要的耦合。 而有了应用层这些都不是问题,因为应用层充当了一个调度者的角色,调度者可以很轻松的决定是否调度下层的服务。
Response getHomeData(Request request){
String nickName = userService.getNickName(request.getUserId());
OrderInfo orderInfo = orderService.queryLatestOrder(request.getUserId());
CostTrend costTrend = payService.queryCostTrend(request.getUserId());
Integer points = pointService.queryAvailablePoints(request.getUserId());
return new Response(nickName, orderInfo, costTrend, points);
}
什么是反模式?
分层设计的开发步骤是怎样的?
业务、产品、开发PM进行需求评审(可行性等) 产品准备好原型 产品、开发(前后端)、架构师(或有架构师能力的资深开发)开会过PRD,了解要做什么 架构师开始设计领域(资深架构师一下午就能搞定),前端开始切图,应用层开发开始按照UI和PRD设计前端每个页面使用的Restful接口(比如直接Springfox代码生成Swagger) 架构师设计完领域后分工给领域层开发,进行领域边界明确,然后领域层开发开始设计数据库表等。 这样前后端开发就同时开工了。 开发初步完成后,自测加连调。 后续就是测试发布了。
- 总结 -
作者:巨人大哥
来源:
https://www.cnblogs.com/jurendage/p/13795640.html