模块分解

用户头像
escray
关注
发布于: 2020 年 11 月 29 日
模块分解

10.1 微服务:服务本身的设计、维护以及治理



阿里早期微服务架构重构



巨无霸应用系统带来的问题



  1. 编译、部署困难

  2. 代码分支管理困难

  3. 数据库连接耗尽

  4. 新增业务困难



编译、部署困难



对于网站开发工程师而言,打包构建一个巨型应用是一件痛苦的事情。 也许只是修改了一行代码,输入build 命令后,抽完一支烟,回来一看,还在building; 又去喝了一杯水,回来一看,还在building;又去了一次厕所,回来一看,还在building; 好不容易build 结束,一看编译失败,还得重来… 想砸了显示器有木有?



代码分支管理困难



复用的代码模块由多个团队共同维护修改,代码 merge 的时候总会发生冲突。代码 merge 一般在网站发布的时候,和发布等问题互相纠结在一起,顾此失彼,每次发布都要半夜三更。



数据库连接耗尽



巨型的应用、大量的访问,必然需要将这个应用部署在一个大规模的服务器集群上,应用与数据库的连接通常使用数据库连接池,以每个应用 10 个连接计,一个数百台服务器集群的应用将 需要在数据库上创建数千个连接,数据库服务器上,每个连接都会占用一些昂贵的系统资源, 以至于数据库缺乏足够的系统资源进行一般的数据操作。



新增业务困难



想要在一个已经如乱麻般系统中增加新业务,维护旧功能,难度可想而知. 一脚踩进去,发现全都是雷,什么都不敢碰。许多新工程师来公司半年了,还是不能接手业务, 因为不知道水有多深。 于是就出现这种怪现象:熟悉网站产品的“老人”忙得要死,加班加点干活;不熟悉网站产品的新人一帮忙就出乱,跟着加班加点; 整个公司热火朝天,加班加点,却还是经常出故障,新产品迟迟不能上线。



解决方案就是拆分,将模块独立部署,降低系统耦合性



2008——2009 年



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



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



难点在于对于业务本身的认知,对业务的服务化划分。



微服务框架



Web Service 与企业级分布式服务



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



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



  • 臃肿的注册与发现机制

  • 低效的 XML 序列化手段

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

  • 复杂的部署与维护手段



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



微服务框架需求



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



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

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

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

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

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



微服务框架(Dubbo)架构





10.2 微服务:落地实践的策略与思路



Service Mesh 服务网格



Service Mesh 是一个基础设施层,用于处理服务间的通信,通常表现为一组轻量级网络代理,与应用程序部署在一起,对应用程序透明。





Service Mesh 的 Sidecar 模式







微服务架构实践



微服务架构落地



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

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

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

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



命令与查询职责隔离(CQRS)





在服务接口层面将查询(读操作)与命令(写操作)隔离,实现服务层的读写分离。 



  • 更清晰的领域模型

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

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



事件溯源



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



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

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



断路器



当某个服务出现故障,响应延迟或者失败率增加,继续调用这个服务会导致调用者请求阻塞,资源消耗增加,进而出现服务级联失效,这种情况下使用断路器阻断对故障服务的调用。 



断路器三种状态:关闭,打开,半开



服务重试及调用超时



上游调用者超时时间要大于下游调用者超时时间之和。





最重要的是需求





10.3 微服务网关的技术架构



基于网关的微服务架构





网关作用





微服务网关





网关管道技术



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





实现管道技术的责任链设计模式





Flower 异步网关与异步微服务框架



https://github.com/zhihuili/flower



利用Servlet3 实现异步网关



protected void doProcess(Object param, HttpServletRequest req) {



    if (req != null) {



}



public Web(AsyncContext context) {



    try {



    } catch (IOException e) {



}



public void print(String message) {



}



开放平台网关





  • API 接口:是开放平台暴露给合作者使用的一组 API,其形式可以是 RESTful、WebService、RPC 等各种形式 

  • 协议转换:将各种API 输入转换成内部服务可以识别的形式, 并将内部服务的返回封装成API 的格式。 

  • 安全:除了一般应用需要的身份识别、权限控制等安全手段, 开放平台还需要分级的访问带宽限制,保证平台资源被第 三方应用公平合理使用,也保护网站内部服务不会被外部应用拖垮。 

  • 审计:记录第三方应用的访问情况,并进行监控、计费等。 

  • 路由:将开放平台的各种访问路由映射到具体的内部服务。 

  • 流程:将一组离散的服务组织成一个上下文相关的新服务, 隐藏服务细节,提供统一接口供开发者调用。



开放授权协议







授权码授权



OAuth2.0 一共有四种授权方式,分别是授权码、隐式授权、资源所有者密码凭据和客户端凭据。 



目前互联网上使用最多也是最安全的的一种方式是授权码方式。





10.4 领域驱动设计DDD



为什么需要 DDD



很多项目的实际情况: 



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

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

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

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



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



事务脚本





领域模型





贫血模型 VS 充血模型



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



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



领域模型是合并了行为和数据的领域的对象模型。通过领域模型对象的交互完成业务逻辑的实现,也就是说,设计好了领域模型对象,也就设计好了业务逻辑实现。和事务脚本被称作贫血模型相对应的,领域模型也被称为充血模型



DDD 战略设计





领域是一个组织所做的事情以及其包含的一切,通俗地说,就是组织的业务范围和做事方式,也是软件开发的目标范围。 



领域驱动设计就是从领域出发,分析领域内模型及其关系,进而设计软件系统的方法。



子域



领域是一个组织所做的事情以及其包含的一切。这个范围就太大了,不知道该如何下手。所以通常的做法是把整个领域拆分成多个子域,比如用户、商品、订单、库存、物流、 发票等。 



  • 如何划分子域? 

  • 卖家提现功能是属于用户子域?订单子域?财务子域?还是直接设计一个提现子域?



限界上下文



在一个子域中,会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样边界便称为限界上下文。限界上下文和子域具有一对一的关系,用来控制子域的边界,保证子域内的概念统一性。 



通常限界上下文对应一个组件或者一个模块,或者一个微服务,一个子系统。





上下文映射图



不同的限界上下文,也就是不同的子系统或者模块之间会有各种的交互合作。DDD 使用上下文映射图来设计这种关联和交互。







实体



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



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



值对象



并不是领域内的对象都应该被设计为实体,DDD 推荐尽可能将对象设计为值对象。比如像住址这样的对象就是典型的值对象,也许建在住址上的房子可以被当做一个实体,但是住址仅仅是对房子的一个描述,像这样仅仅用来做度量或描述的对象应该被设计为值对象。 



值对象的一个特点是不变性,一个值对象创建以后就不能再改变了。如果地址改变了, 那就是一个新地址,而一个订单实体则可能会经历创建、待支付、已支付、代发货、已 发货、待签收、待评价等各种变化。



聚合



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



聚合根:将多个实体和值对象聚合在一起的实体。



DDD 分层架构



领域实体的组合调用和事务控制在应用层。





DDD 六边形架构



领域模型通过应用程序封装成一个相对比较独立的模块,而不同的外部系统则通过不同的适配器和领域模型交互,比如可以通过 HTTP 接口访问领域模型,也可以通过 Web Service 或者消息队列访问领域模型,只需要为这些不同的访问接口提供不同的适配器就可以了。





DDD 战略设计与战术设计



领域、子域、界限上下文、上下文映射图,这些是DDD 的战略设计。 



实体、值对象、聚合、CQRS、事件溯源,这些是DDD 战术设计。 



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



我经历的一个DDD 重构实践过程



当前系统设计与问题汇总讨论 



  • 架构与代码混乱

  • 需求迭代困难

  • 部署麻烦

  • bug 率逐渐升高 



针对问题分析具体原因 



  • 子系统 A 太庞大

  • 模块 B 和 C 职责不清

  • 业务理解不一致 



重新梳理业务规则和边界,明确业务术语:DDD 战略设计,领域建模 



技术框架选型与落地方案验证:DDD 战术设计,样例代码 



任务分解与持续重构:在不影响业务开发的前提下,按照战略与战术设计,将重构开发和业务迭代有机融合



如果一个工作多年的程序员,还是仅仅写一些跟他工作第一年差不多的 CRUD 代码。那么他迟早会遇到自己的职业危机。公司必然愿意用更年轻、更努力,当然也更低薪水的程序员来代替他。至于学习新技术的能力,其实多年工作经验也并没有太多帮助,有时候也许还是劣势。 



资深程序员真正有优势的是他在一个业务领域的多年积淀,对业务领域有更深刻的理解和认知。那么如何将这些业务沉淀和理解反映到工作中,体现在代码中呢?实践DDD 是一个不错的方式。 



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



10.5 软件组件设计原则



在没有变成语言的时候就已经有了软件组件





软件的复杂度和它的规模成指数关系



一个复杂度为 100 的软件系统,如果能拆分成两个互不相关、同等规模的子系统,那么每个子系统的复杂度应该是 25,而不是 50。软件开发这个行业很久之前就形成了一个共识,应该将复杂的软件系统进行拆分,拆成多个更低复杂度的子系统,子系统还可以继续拆分成更小粒度的组件。也就是说,软件需要进行模块化组件化设计。



组件内聚原则



组件内聚原则主要讨论哪些类应该聚合在同一个组件中,以便组件既能提供相对完整的功能,又不至于太过庞大。



  • 复用发布等同原则

  • 共同封闭原则

  • 共同复用原则



复用发布等同原则



复用发布等同原则是说,软件复用的最小粒度应该等同于其发布的最小粒度。也就是说, 如果你希望别人以怎样的粒度复用你的软件,你就应该以怎样的粒度发布你的软件。这其实就是组件的定义了,组件是软件复用和发布的最小粒度软件单元。这个粒度既是复用的粒度,也是发布的粒度。 



版本号约定建议: 



  • 版本号格式:主版本号. 次版本号. 修订号。比如1.3.12,在这个版本号中,主版本号是1, 次版本号是3,修订号是12。 

  • 主版本号升级,表示组件发生了不向前兼容的重大修订;

  • 次版本号升级,表示组件进行了重要的功能修订或者bug 修复,但是组件是向前兼容的; •

  • 修订号升级,表示组件进行了不重要的功能修订或者bug 修复。



共同封闭原则



共同封闭原则是说,我们应该将那些会同时修改,并且为了相同目的而修改的类放到同一个组件中。而将不会同时修改,并且不会为了相同目的而修改的类放到不同的组件中。



组件的目的虽然是为了复用,然而开发中常常引发问题的,恰恰在于组件本身的可维护性。如果组件在自己的生命周期中必须经历各种变更,那么最好不要涉及其他组件,相关的变更都在同一个组件中。这样,当变更发生的时候,只需要重新发布这个组件就可以了,而不是一大堆组件都受到牵连。



共同复用原则



共同复用原则是说,不要强迫一个组件的用户依赖他们不需要的东西。



这个原则一方面是说,我们应该将互相依赖,共同复用的类放在一个组件中。比如说, 一个数据结构容器组件,提供数组、Hash 表等各种数据结构容器,那么对数据结构遍历的类、排序的类也应该放在这个组件中,以使这个组件中的类共同对外提供服务。 



另一方面,这个原则也说明,如果不是被共同依赖的类,就不应该放在同一个组件中。 如果不被依赖的类发生变更,就会引起组件变更,进而引起使用组件的程序发生变更。 这样就会导致组件的使用者产生不必要的困扰,甚至讨厌使用这样的组件,也造成了组件复用的困难。



组件耦合原则



组件内聚原则讨论的是组件应该包含哪些功能和类,而组件耦合原则讨论组件之间的耦合关系应该如何设计。 



  • 无循环依赖原则

  • 稳定依赖原则

  • 稳定抽象原则



无循环依赖原则



无循环依赖原则说,组件依赖关系中不应该出现环。如果组件 A 依赖组件 B,组件 B 依 赖组件 C,组件 C 又依赖组件 A,就形成了循环依赖。 



很多时候,循环依赖是在组件的变更过程中逐渐形成的,组件 A 版本 1.0 依赖组件 B 版本 1.0,后来组件 B 升级到 1.1,升级的某个功能依赖组件 A 的1.0 版本,于是形成了循环依赖。如果组件设计的边界不清晰,组件开发设计缺乏评审,开发者只关注自己开发的组件,整个项目对组件依赖管理没有统一的规则,很有可能出现循环依赖。



稳定依赖原则



稳定依赖原则说,组件依赖关系必须指向更稳定的方向。较少变更的组件是稳定的,也就是说,经常变更的组件是不稳定的。根据稳定依赖原则,不稳定的组件应该依赖稳定的组件,而不是反过来。 



反过来说,如果一个组件被更多组件依赖,那么它需要相对是稳定的,因为想要变更一个被很多组件依赖的组件,本身就是一件困难的事。相对应的,如果一个组件依赖了很多的组件,那么它相对也是不稳定的,因为它依赖的任何组件变更,都可能导致自己的变更。 



稳定依赖原则通俗地说就是,组件不应该依赖一个比自己还不稳定的组件。



稳定抽象原则



稳定抽象原则说,一个组件的抽象化程度应该与其稳定性程度一致。也就是说,一个稳定的组件应该是抽象的,而不稳定的组件应该是具体的。 



这个原则对具体开发的指导意义就是:如果你设计的组件是具体的、不稳定的,那么可以为这个组件对外提供服务的类设计一组接口,并把这组接口封装在一个专门的组件中,那么这个组件相对就比较抽象、稳定。 



Java 中的 JDBC 就是这样一个例子,我们开发应用程序的时候只需要使用 JDBC 的接口编程就可以了。而发布应用的时候,我们指定具体的实现组件,可以是 MySQL 实现 的 JDBC 组件,也可以是 Oracle 实现的JDBC 组件。



组件的边界与依赖关系,不仅仅是技术问题



组件的边界与依赖关系划分,不仅需要考虑技术问题,也要考虑业务场景问题。易变与稳定,依赖与被依赖,都需要放在业务场景中去考察。有的时候,甚至不只是技术和业务的问题,还需要考虑人的问题,在一个复杂的组织中,组件的依赖与设计需要考虑人的因素,如果组件的功能划分涉及到部门的职责边界,甚至会和公司内的政治关联起来。



10.6 案例:用领域驱动设计驱动系统重构



系统重构是不可避免的



软件开发是一个过程,这个过程中相关方对软件系统的认知会不断改变,当系统现状和大家的认知有严重冲突的时候,不重构系统难以继续开发下去。 



在持续的需求迭代过程中,代码本身会逐渐腐坏,变得僵硬、脆弱、 难以维护,需求开发周期越来越长,bug却越来越多。



大多重构关注在代码上





而忽略了业务模块边界和关系的重构





微服务架构的关键是设计服务的职责与依赖关系





当微服务架构面临重构的挑战



如何随着业务不断发展,保持微服务的高内聚、低耦合?



用领域驱动设计驱动系统重构





统一领域知识分解领域问题



领域驱动设计关键术语



  • 领域:一个组织所做的事情以及其包含的一切,通俗地说,就是组织的业务范围和做事方式,也是软件开发的目标范围。

  • 子域:领域是一个组织所做的事情以及其包含的一切。这个范围就太大了,不知道该如何下手。所以通常的做法是把整个领域拆分成多个子域,比如用户、商品、订单、库存、物流、发票等。

  • 限界上下文:在一个子域中,会创建一个概念上的领域边界,在这个边界中,任何领域对象都只表示特定于该边界内部的确切含义。这样边界便称为限界上下文。限界上下文通常和子域具有一对一的关系,用来控制子域的边界,保证子域内的概念统一性。

  • 上下文映射图:不同的限界上下文,也就是不同子域之间会有各种的交互合作。DDD 使用上下文映射图来设计这种关联和交互。

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

  • 值对象:并不是领域内的对象都应该被设计为实体,DDD 推荐尽可能将对象设计为值对象。比如像住址这样的对象就是典型的值对象。

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



用限界上下文识别微服务的功能、边界和依赖关系





DDD 典型开发过程与关键产出





一个出行 App 的 DDD 重构历程



历时多年,多个团队转手,没有人了解最初的设计思路和设计目标。 



有个叫 Common 的微服务,承担了 60% 以上的功能,而且越来越大,大家开发新功能 的时候,越来越喜欢把功能放在 Common 里。 



开发迭代速度逐渐变慢,bug环比稳中有升。 



业务方准备进行一次较大规模的新业务尝试,新业务形式上有很大变化,不过核心业务 功能变化不大,但研发团队发现服务复用很困难。



重构背景与整体规划













基于领域知识的统一语言与概念模型











核心业务流程分析(泳道活动图、状态图)













基于流程分析的领域划分









基于领域划分的限界上下文设计















战术设计与架构落地















代码结构





用值对象重构成员变量













出行 App 重构过程总结



当前系统设计与问题汇总讨论



  • 架构与代码混乱

  • 需求迭代困难

  • 部署麻烦

  • bug 率逐渐升高



针对问题分析具体原因 



  • 微服务 A太庞大

  • 微服务 B 和 C 职责不清

  • 团队内业务理解不一致

  • 内部代码设计不良

  • 硬编码和耦合太多



重新梳理业务流程,明确业务术语,进行DDD战略设计



  • (泳道)活动图

  • 子域分解

  • 限界上下文设计 •



针对当前系统实现和DDD设计不匹配的地方设计微服务重构方案 



DDD战术设计与技术验证:聚合、实体、值对象设计,打样代码开发



任务分解与持续重构:在不影响业务开发的前提下,按照战略与战术设计,将重构开发和业务迭代有机融合



10.7 第十周课后练习



作业一:(至少完成一个)



根据微服务框架 Dubbo 的架构图,画出 Dubbo 进行一次微服务调用的时序图。





  • 关于微服务架构(中台架构、领域驱动设计、组件设计原则),你有什么样的思考和认识?



作业二:根据当周学习情况,完成一篇学习总结



发布于: 2020 年 11 月 29 日阅读数: 21
用户头像

escray

关注

Let's Go 2017.11.19 加入

大龄菜鸟项目经理

评论

发布
暂无评论
模块分解