架构师训练营:第十周总结

用户头像
zcj
关注
发布于: 2020 年 08 月 11 日

一 微服务

1、为什么要微服务

原始大单体应用的弊端

  • 编译、部署困难:一次编译编译打包需要很久

  • 回归测试周期长:修复一个小小bug可能都需要对所有关键业务进行回归测试。

  • 应用容错性差:某个小小功能的程序错误可能导致整个系统宕机;

  • 伸缩困难:单体应用扩展性能时只能整个应用进行扩展,造成计算资源浪费。

  • 开发协作困难:一个大型应用系统,可能几十个甚至上百个开发人员,大家都在维护一套代码的话,代码merge复杂度急剧增加。

  • 新增业务困难:需要在原来一堆关系复杂的业务代码中新增业务,同时维护就功能稳定,难度可想而知。“一脚踏进去,发现全是雷。许多新工程师来公司半年了,还是不能接手业务,因为不知道水有多深。于是就出现一种怪象:熟悉原有业务的 ‘老人’忙的要死,加班加点干活;不熟悉网站产品的新人人一帮忙就出乱,跟着加班加点;整个公司热火朝天,加班加点,却还是经常出故障,新产品迟迟不能上线。”



微服务解决方案:将模块拆分独立部署,降低系统耦合性

  • 纵向拆分:将一个大应用拆分为多个小应用,如果新增业务较为独立,那么久直接将其设计部署为一个独立的web应用系统。

  • 横向拆分:将复用的业务拆分出来,独立部署为微服务,新增业务只需要调用这些微服务即可,快速搭建一个应用系统。



2、微服务框架

1、分布式服务提供方式webService

webService的服务提供者通过 WSDL (Web服务描述语言,Web Services Description Language)向注册中心(Service Broker)描述自身提供的服务接口属性,注册中心使用 UDDI (Universal Description,Discovery, and Intergration ,统一描述,发现和集成)发布服务提供者提供的服务,服务请求者从注册中心检索到服务信息后,通过 SOAP (Simple Object Access Protocol,简单对象访问协议 )和服务提供者通信,使用相关的服务。



Web Service 虽然有着成熟的技术规范和产品实现,以及在企业应用领域有许多成功的案例,但是也具有一些固定的缺点:

  • 臃肿的注册于发现机制

  • 低效的 XML 序列化手段

  • 开销相对较高的 HTTP 远程通信

  • 复杂的部署与维护手段

这些问题导致 Web Service难以满足大型网站对系统的高新能,高可用,易部署、易维护的要求。



2、微服务框架的需求

对于大型互联网系统,除了 Web Servie 所规范的服务注册于发现,服务调用等标准功能,还需要微服务框架能够支持:

  • 失效转移(Fail Over):对于大型网站的微服务而言,即使是很少访问的简单服务,也需要集群部署,同时微服务框架还需要支持服务提供者的失效转移机制,以实现服务高可用。

  • 负载均衡:对于集群部署的服务提供者,服务请求者可以使用加权轮询等手段访问,使服务提供者集群实现负载均衡。

  • 高效的远程通信:对于大型网站,核心服务每天的调用次数会达到数以亿计,如果没有高效的远程通信手段,服务调用可能会成为整个系统性能的瓶颈。

  • 对应用的最少入侵:网站技术是为业务服务的,是否使用微服务需要根据业务发展规划,微服务也需要渐进式的演化,甚至会出现反复,即使用了微服务后又退回到集中式部署,微服务框架需要支持这种渐进式演化和反复。当然服务模块本身需要支持可集中式部署,也可分布式部署。

  • 版本管理:为了应对快速变化的需求,服务版本升级不可避免,如果仅仅是服务实现升级,那么这种升级对服务请求者而言是透明的,无需关注。但是如果服务的访问接口发生变化,就需要服务请求者和服务提供者同时升级才不会导致服务调用失败。企业应用系统可以申请停机维护,同时升级接口,而网站服务不可能中断,需要服务提供者先升级接口,并同时提供历史版本的服务请求者调用,当请求者访问接口升级后才可以关闭历史版本服务。



现有的微服务框架

  • Dubbo

  • Spring Cloud



3、微服务的实现模式

1、微服务架构的核心技术问题
  • 服务发现:服务的消费方(Consumer)如何发现服务的提供方(Provider)?

  • 负载均衡:服务的消费方如何以某种负载均衡策略访问集群中的服务提供方实例?

作为架构师,如果你理解了这两个问题,可以说就理解了微服务架构在技术上的最核心问题。



2、三种服务发现模式

服务发现和负载均衡并不是新问题,业界其实已经探索和总结出一些常用的模式,这些模式的核心其实是代以及代理在架构中所处的位置,在服务消费方和服务提供方之间增加一层代理,由代理负责服务发现和负载均衡功能,消费方通过代理间接访问目标服务。根据代理在架构上所处的位置不同,当前业界主要有三种不同的服务发现模式:

  • 传统集中式代理:运维简单,存在单点问题 。

  • 客户端嵌入式代理:无单点,性能好,客户端复杂,多语言麻烦如Dubbo,spring eureka

  • 主机独立进程代理:折中方案,运维部署复杂 。Service Mesh (Sidecar 模式)



Service Mesh:服务网格,也被形象称为边车(Sidecar)模式,早期有一些摩托车,除了主驾驶位,还带一个边车位,可以额外坐一个人。在模式三中,业务代码进程(相当于主驾驶)共享一个代理(相当于边车),代理除了负责服务发现和负载均衡,还负责动态路由、容错限流、监控度量和安全日志等功能,这些功能是具体业务无关的,属于跨横切面关注点(Cross-Cutting Concerns)范畴。



在新一代的ServiceMesh架构中(下图上方),服务的消费方和提供方主机(或者容器)两边都会部署代理SideCar。ServiceMesh比较正式的术语也叫数据面板(DataPlane),与数据面板对应的还有一个独立部署的控制面板(ControlPlane),用来集中配置和管理数据面板,也可以对接各种服务发现机制(如K8S服务发现)。术语数据面板和控制面板,估计是偏网络SDN背景的人提出来的。



上图左下角,每个主机上同时居住了业务逻辑代码(绿色表示)和代理(蓝色表示),服务之间通过代理发现和调用目标服务,形成服务之间的一种网络状依赖关系,控制面板则可以配置这种依赖调用关系,也可以调拨路由流量。如果我们把主机和业务逻辑剥离,就出现一种网格状架构(上图右下角),服务网格由此得名。



3、微服务架构实践指导

  • 业务先行,先理顺业务边界和依赖,技术是手段而不是目的。

  • 现有独立的模块,后有分布式的服务。

  • 业务耦合严重,逻辑复杂多变的系统进行服务重构要谨慎。

  • 要搞清楚实施微服务的目的是什么,业务复用?开发边界清晰?分布式集群性能提升? 最终要的是为需求服务。



代码层面

  • 命令与查询职责隔离(CQRS) :读写分离

  • 得到更清晰的领域模型

  • 针对读写分别优化,实现更好的性能

  • 查询服务不会修改数据,更好的保护数据

  • 事件溯源:将用户请求过程中的每次状态变化都记录到事件日志中,并按时间序列进行持久化存储。

  • 可以精确复现任何用户状态,进行复核审计。

  • 可以有效监控用户状态变化,并在此基础上实现分布式事务。

  • 服务重试及超时时间:上游调用者超时时间要大于下游调用者超时时间之和。



4、基于网关的微服务

1、什么是服务网关

服务网关 = 路由转发 + 过滤器



1、路由转发:接收一切外界请求,转发到后端的微服务上去;

2、过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(其实路由转发也是通过过滤器实现的)。

2、为什么需要服务网关

上述所说的横切功能(以权限校验为例)可以写在三个位置:

  1. 每个服务自己实现一遍

  2. 写到一个公共的服务中,然后其他所有服务都依赖这个服务

  3. 写到服务网关的前置过滤器中,所有请求过来进行权限校验

第一种,缺点太明显,基本不用;

第二种,相较于第一点好很多,代码开发不会冗余,但是有两个缺点:

由于每个服务引入了这个公共服务,那么相当于在每个服务中都引入了相同的权限校验的代码,使得每个服务的jar包大小无故增加了一些,尤其是对于使用docker镜像进行部署的场景,jar越小越好;由于每个服务都引入了这个公共服务,那么我们后续升级这个服务可能就比较困难,而且公共服务的功能越多,升级就越难,而且假设我们改变了公共服务中的权限校验的方式,想让所有的服务都去使用新的权限校验方式,我们就需要将之前所有的服务都重新引包,编译部署。



而服务网关恰好可以解决这样的问题:

将权限校验的逻辑写在网关的过滤器中,后端服务不需要关注权限校验的代码,所以服务的jar包中也不会引入权限校验的逻辑,不会增加jar包大小;

如果想修改权限校验的逻辑,只需要修改网关中的权限校验过滤器即可,而不需要升级所有已存在的微服务。



3、服务网关基本功能

  • 智能路由:接收外部一切请求,并转发到后端的对外服务open-service上去;注意:我们只转发外部请求,服务之间的请求不走网关,这就表示全链路追踪、内部服务API监控、内部服务之间调用的容错、智能路由不能在网关完成;当然,也可以将所有的服务调用都走网关,那么几乎所有的功能都可以集成到网关中,但是这样的话,网关的压力会很大,不堪重负。

  • 权限校验:只校验用户向open-service服务的请求,不校验服务内部的请求。服务内部的请求有必要校验吗?

  • API监控:只监控经过网关的请求,以及网关本身的一些性能指标(例如,gc等);

  • 限流:与监控配合,进行限流操作;

  • API日志统一收集:类似于一个aspect切面,记录接口的进入和出去时的相关日志。

上述功能是网关的基本功能,网关还可以实现以下功能:

  • A|B测试:A|B测试时一块比较大的东西,包含后台实验配置、数据埋点(看转化率)以及分流引擎,在服务网关中,可以实现分流引擎,但是实际上分流引擎会调用内部服务,所以如果是按照上图的架构,分流引擎最好做在open-service中,不要做在服务网关中。



4、网关管道技术

网关本身没有什么业务,主要职责是做各种校验与拦截,这些职责可以通过管道技术连接起来



参考博客:

https://www.jianshu.com/p/7293b148028f

https://www.cnblogs.com/littlewrong/p/10558459.html

https://www.cnblogs.com/javastack/p/12911701.html



二 领域驱动设计 DDD

为什么需要 DDD?

很多项目的实际情况:

  • 用户或者产品经理的需求零零散散,不断变更。

  • 工程师在各处代码中寻找可以实现这些需求变更的代码,修修补补。

  • 软件只需要需求分析,并没有真正的设计,系统没有一个统一的领域模型维持其内在的逻辑一致性。

  • 功能特性并不是按照领域模型内在的逻辑设计,而是按照各色人等自己的主观想象设计。



项目时间一长,各种困难重重,需求不断延期,线上bug不断,管理者考虑是不是要推倒重来,而程序员则考虑是不是要跑路。

传统的开发模式,称为事务脚本的开发方式,Service 、Dao 这些对象只有方法,没有数值成员变量,而方法调用时传递的数值对象,没有方法(或者只有一些 getter、setter方法),因此事务脚本又被称作贫血模型。



DDD

领域模型中领域对象则包含了对象的数据和计算逻辑,比如合同对象,既包含合同数据,也包含了合同相关的计算。因此从面相对象的角度看,领域模型才是真正的面向对象。收入确认是和合同强相关的,是合同对象的一个职责,那么合同对象就应该提供一个方法计算收入。



对于一个业务复杂的系统而言,使用DDD有如下好处:

  • 开发者和熟悉业务的人一起工作,加强团队间不同角色的合作;

  • 能够帮助业务人员和开发人员梳理清楚复杂的业务规则;

  • 开发出来的软件是能够准确表达业务规则的,设计就是代码,代码就是设计;



DDD 战略与战术设计

DDD分为战略部分和战术部分,两者相辅相成。战略部分用于理解、梳理业务,找到核心业务,更好地划分系统(这也是DDD为什么可以用于指导微服务设计的原因);战术部分用于落地到代码上,用代码来清晰地表示业务,代码如何分层、如何设计都有一套成熟的指导方案。



DDD可以比较好地解决复杂业务的软件开发,但DDD不是“银弹”,DDD也并不是一些死板的术语和规范。事实上,当今业界仍然在DDD的实践道路上探索前行。

领域

具体来讲,每个组织都有它自己的业务范围和做事方式,这个业务范围以及在其中所进行的活动便是领域。如果你为某个组织开发软件时,你面对的就是这个组织的领域。



业务范围可能也很大,所以我们需要把这个大的业务范围拆开。如果我们把这个范围看成是一个空间的话,那它同时存在“问题空间”和“解决方案空间”。问题空间是从业务需求方面来看,而解决方案空间是从实现软件方面来看。两者是有一些细微的区别的,最终我们用子域来划分问题空间,用限界上下文来划分解决方案空间

子域

子域是对领域从业务需求方面进行拆分,是逻辑上的。比如一个电商系统,我们可以很明确地可以把它拆分成商品子域、销售子域、仓储子域、客服子域……



子域主要分为三种:

  • 核心子域:业务成功的核心竞争力;

  • 通用子域:不是核心,但被整个业务系统所使用 (可以直接购买的);

  • 支撑子域:不是核心,不被整个系统使用,完成业务的必要能力(可以外包出去的);



我们应该给予核心域最高的优先级,最资深的领域专家和最优秀的开发团队。在实施DDD的过程中,主要关注与核心域。



在划分好了子域以后,可以再回过头来,验证现在划分的子域时候已经解决了全部问题空间的问题。如果没有,则可以再进行适当调整。

限界上下文

通用语言

要理解限界上下文,首先要了解什么是通用语言。事实上实施DDD战略模式的时候,最重要的一点便是统一语言。我们知道,每个人对一个事物的理解可能都是有一些微小的差别的。比如在谈到“商品”的时候,在“店铺”这个业务场景可能比较关注的是商品的标题、图片、价格等等,但在库存这个业务场景,关注的可能是商品的编号、存量、入库时间和出库时间等等。



所以同一个词可能要不同的意思。尤其是一些比较宽泛的词,如“商品”、“订单”、“用户”等等。这样就可能会造成歧义,我说的模型和你说的模型可能不是同一个模型。如果不加以区分,最终设计出来的模型可能就是一个过于复杂和混乱的模型。



所以我们需要统一语言,比如在销售上下文中,商品叫做“销售商品”,在库存上下文中,叫做“库存货物”,在物流上下文中,叫做“物流货物”等等。



这样便可以在不同的业务场景关注不同的属性,有不同的操作,从而设计出更能准确表达业务意义的模型。



限界上下文是一个边界,领域模型便存在在这个边界之内。当模型被一个显示的边界所包围时,其中每个概念的含义便是确定的了。因此,限界上下文主要是一个语义上的边界。



限界上下文的划分原则

原则上,一个上下文就对应一个子域。但这并不是绝对的,限界上下文的目的是为了更加明确领域模型的职责和范围,是从“解决方案空间”来考虑问题的。通常来说,一个限界上下文可以是一个微服务,或者一个Module。



那么如何划分限界上下文呢?具体来讲,有三个原则可以参考:



  • 概念相同,含义不同:即上面所说的“通用语言”的例子,如果一个模型在一个上下文里面有歧义,那有歧义的地方就是边界所在,应该把它们拆到不同的限界上下文中。

  • 外部系统:有时候系统需要同外部系统打交道,这个时候可以把与外部系统打交道的那部分拆分出去以实现更好的扩展性。这样一旦外部系统发生了变化,就不会影响到我们的核心业务逻辑。

  • 像组织扩展:尽量不要两个团队一起在一个限界上下文里面开发,因为这样可能会存在沟通不顺畅、集成困难等问题。





上下文映射

当我们划分好了限界上下文后,就需要考虑一下上下文的映射关系了。为什么要考虑它们之间的映射关系呢?因为这样可以让我们从宏观上看到每个上下文之间的关系,从而能够更好地指导我们后续的程序设计。另一方面,如果一旦发现某个限界上下文与过多的其它限界上下文具有联系,那可能需要考虑拆分这个限界上下文了。



我们用Upstream的首字母U来标识“上游”,用Downstream的首字母D来标识“下游”。那怎么去判断“上游”还是“下游”呢?这里有一个原则:下游的模型依赖于上游的模型和服务。



我们对上面的图进行限界上下文的映射之后可以得到:



通过上述的过程,便可以把模型隔离开来,形成一个个限界上下文。这样具体在开发过程中,一个限界上下文就可以对应一个微服务或者module。限界上下文之间一般使用“事件”来进行信息交互,如此便可以实现低耦合。



实体

实体也就是领域模型对象,每个实体都是唯一的,具有一个唯一标识,一个订单对象是一个实体,一个产品对象也是一个实体,订单ID或者产品ID是他们的唯一标识。实体可能会发生变化,比如订单的状态会变化,但是他们的唯一标识不会变化.

实体设计是DDD的核心所在,首先通过业务分析,识别出实体对象,然后通过相关的业务逻辑设计实体的属性和方法。这里最重要的,是要把握住实体的特征是什么,实体应该承担什么职责,不应该承担什么职责,分析的时候要放在业务场景和界限上下文中,而不是想当然的认为这样的实体就英爱承担这样的角色。



值对象

并不是领域内的对象都应该被设计位实体,DDD推荐尽可能将对象设计位值对象,值对象的一个特点就是不变性,一个值对象创建以后就不会再改变了。如果改变了,那就是另外一个对象了。



聚合

聚合是一个关联对象的集合,我们将其作为一个单元来处理数据更改。每个集合都有一个根好一个边界。边界定义了聚合内部的内容。根是聚合中包含的单个特定实体。聚合根:将多个实体和值对象聚合在一起的实体。



DDD 分层结构



通过战略设计,划分模块和服务的边界及依赖关系,对微服务架构的设计至关重要。



关于DDD 对于工作多年程序员的帮助

工作多年的程序员的优势在何处?

如果工作一年多的程序员,还是仅仅写一些跟她工作第一年差不多的 CRUD 代码。那么他迟早会遇到自己的职业危机。公司必然愿意用更年轻,更努力,当然也更低薪水的程序员来代替他。 至于学习新技术的能力,其实工作多年并没有太多帮助,有时候还会是劣势。资深程序员真正优势是他在一个业务领域的多年积淀,对业务领域有更深刻的理解和认知。那么如何将这些业务沉淀和理解反映到工作中,体现在代码中了? 实践DDD是一种不错的方式。

如果一个人有多年的领域经验,那么必然对领域模型设计有更深刻的认识,把握好领域模型在不断的需求变更中的演进,使系统维持更好的活力,并因此体现自己真正的价值。



参考博客:

https://blog.csdn.net/yasinshaw/article/details/103306562

https://blog.csdn.net/yasinshaw/article/details/10330623



三、总结感悟

这几年写的代码都是面向过程式的代码,写起来也感觉没什么意思。想起当初在学校学习面向对象时,分析一个类,比如人。人是有名字、身高、体重这些数值,还有可以吃饭,睡觉,说话,创造等这些动作的。设计人这个类时,这些 数值就是 数据,动作就是方法,都包含在人这个类里面。 可是工作后,在业务线都是按照service,pojo这种形式去做。 自己常问,面向对象去哪了,为什么公司要选择这种开发模式。所以一直想能真正在工作中,运用面向对象的设计去写代码。 领域驱动设计就是面向对象式的开发,把领域中的实体设计为一个有血有肉的类,所有的方法都是实体自己产生的动作行为。我觉得很好理解。比看到原来的一个一个零散的业务方法舒服多了。



用户头像

zcj

关注

还未添加个人签名 2019.10.12 加入

精神小伙

评论

发布
暂无评论
架构师训练营:第十周总结