极客时间架构师训练营 1 期 - 第 10 周总结

用户头像
Kaven
关注
发布于: 2020 年 11 月 29 日

巨无霸应用系统

面临问题

编译、部署困难

代码分支管理困难

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

数据库连接耗尽

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

新增业务困难

过度的复杂度,牵一发动全身;新增一个业务,需要将与其相关的功能全面衡量,开发周期长,测试周期长,交付周期更长;无法目前快速发展的业务;

应用的弹性伸缩性差, 难以扩展。

解决办法

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

  • 纵向拆分:将一个大应用拆分为多个小应用,如果新增业务较为独立,那么就直接将其设计部 署为一个独立的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)架构



ServiceMesh服务网格

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





ServiceMesh的Sidecar模式







微服务架构实践

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

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

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

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

微服务架构落地

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

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

  • 更清晰的领域模型

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

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

事件溯源

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

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

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

断路器

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

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





服务重试及调用超时

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





最重要的是需求








微服务网关

基于网关的微服务架构





网关作用:

统一接入

高性能,高并发,高可靠,负载均衡

流量管控与容器

限流,降级,熔断

协议适配

http,dubbo,jsf

安全防护

防刷控制,黑白名单

微服务网关





网关管道技术

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





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





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





https://github.com/zhihuili/flower

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

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

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

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

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

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





开放授权协议OAuth2.0







OAuth2.0 一共有四种授权方式, 分别是授权码、隐式授权、资源所有者密码凭据和客户端凭据。 目前互联网上使用多也是安全的的一种方式是授权码方式

授权码授权





授权码通过使用授权服务器作为客户端与资源所有者的中介而获得。客户端不是直接从资源所有者请求授权,而是引导资源所有者至授权服务器授权服务器之后引导资源所有者带着授权码回到客户端。在引导资源所有者携带授权码返回客户端前,授权服务器会鉴定资源所有者身份并获得其授权。由于资源所有者只与授权服务器进行身份验证,所以资源所有者的凭据不需要与客户端分享。授权码提供了一些重要的安全益处,例如验证客户端身份的能力,以及向客户端直接的访问令牌的传输而非通过资源所有者的用户代理来传送它而潜在暴露给他人(包括资源所有者)。

隐式许可:在隐式许可流程中,不再给客户端颁发授权码,取而代之的是客户端直接被颁发一个访问令牌(作为资源所有者的授权),授权服务器不对客户端进行身份验证。这种许可类型是隐式的,因为没有中间凭据(如授权码)被颁发(之后用于获取访问令牌)。

资源所有者密码凭据:资源所有者密码凭据(即用户名和密码),可以直接作为获取访问令牌的授权许可。这种凭据只能应该 当资源所有者和客户端之间具有高度信任时(例如,客户端是设备的操作系统的一部分,或者是一个高度特权应用程序),以及当其他授权许可类型(例如授权码)不可用时被使用。

客户端凭据:当授权范围限于客户端控制下的受保护资源或事先与授权服务器商定的受保护资源时客户端凭据可以被 用作为一种授权许可。典型的当客户端代表自己(客户端也是资源所有者)或者基于与授权服务器事先商定的授权请求对受保护资源的访问权限时,客户端凭据被用作为授权许可。




领域设计

为什么需要DDD

很多项目的实际情况:

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

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

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

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

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

事务脚本

领域模型

贫血模型VS充血模型

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

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

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




DDD战略设计

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

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

子域

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

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

限界上下文

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

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

上下文映射图

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




DDD战术设计

实体

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

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

值对象

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

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

聚合

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

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

DDD分层架构

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





DDD六边形架构

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





DDD战略设计与战术设计

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

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

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

DDD实践参考

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

架构与代码混乱,需求迭代困难,部署麻烦,bug率逐渐升高

针对问题分析具体原因

• 子系统A太庞大,模块B和C职责不清,业务理解不一致

重新梳理业务规则和边界,明确业务术语

• DDD战略设计,领域建模

技术框架选型与落地方案验证

• DDD战术设计,样例代码

任务分解与持续重构

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

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

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

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




组件设计原则

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

一个复杂度为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组件。

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

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




案例总结

系统重构是不可避免的

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

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

大多重构关注在代码上而忽略了业务模块边界和关系的重构





微服务架构的关键是设计服务的职责与依赖关系
当微服务架构面临重构的挑战

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

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





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





领域驱动设计关键术语

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

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

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

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

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

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

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

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





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





用户头像

Kaven

关注

还未添加个人签名 2019.04.13 加入

还未添加个人简介

评论

发布
暂无评论
极客时间架构师训练营 1 期 - 第 10 周总结