【整理】系统可扩展性的设计与实现

总体思路

系统可扩展性是指能够低成本、高质量地在现有系统中添加新功能和优化现有功能。

可扩展设计的核心原则是:开闭原则。对新增开放,对修改关闭。也就是说,后续有新的需求,只需要新增组件,而不需要修改现有组件或逻辑(除非实现有BUG)。

要保证一个大的业务模块的可扩展性,有效的策略是拆分和分层。

  • 拆分: 将大的业务模块拆分为多个子模块,针对每个子模块进行可扩展设计;
  • 分层: 自下而上进行可扩展设计 → 底层数据模型的可扩展性、存储的可扩展性、业务流程的可扩展性;代码实现层面的可扩展性。

实现系统可扩展性,可从以下方面着手:

  • 选择合适的架构
  • 使用微服务来分解系统
  • 使用消息系统进行服务解耦
  • 使用代理和负载均衡来确保服务可用性
  • 使用组件和配置化来提升功能的扩展性
  • 使用易用的 API 来构建功能的可集成性
  • 使用柔性编程来实现代码的扩展性
  • 通过CI/CD 和容器提升部署的扩展性
  • 预测业务变化

实现可扩展性的前提:

  • 模块化: 将系统分解成多个可装卸的、高内聚、低耦合的模块;
  • 组件化: 将功能实现成可复用的组件,组合组件来构成模块;
  • 接口化: 将功能抽象成接口,提供接口的不同实现;
  • 配置化: 通过配置来定制和选择功能的实现。

选择合适的架构

使用可扩展的架构设计:

  • 建模存储:数据模型、数据 Schema 、完整性和一致性约束定义。
  • 领域驱动:DDD 设计,稳定的精炼的可持续演进的领域模型,六边形架构,聚合根,充血模型。
  • 架构模式:分层、微内核+插件、PipeLine 、事件驱动、订阅-推送、Actor、AKF 等。

架构模式见: “软件设计要素初探:架构模式”


微服务

  • 单体应用的问题:直接通过访问 DB 来共用数据库,数据管理容易缺乏明确 owner 而混乱,DB 设计变更难度高;缺乏明确的域的划分,功能边界不清晰,重复功能实现和重复代码难以维护,业务耦合,依赖关系混乱;协作成本高,系统整体稳定性差。
  • 微服务:将单体应用分解为多个具有明确领域定义的业务子域,将每个相对独立的业务子域实现成单独的微服务,微服务独立管理各自子域的问题,采用不同的架构和方案来适配自身领域的问题,最终所有微服务集成起来完成整体应用功能。实现独立自治和发展、模块化、分工协作等。
  • 微服务要解决的问题是服务治理。主要包括:限流/熔断降级、配置管理、日志中心、监控预警、链路跟踪、故障隔离、动态扩容、分流发布、全链路压测、中间件支撑、团队组织架构适配与管理。基本解决方案: RPC 框架和统一应用框架接入。
  • 一个较为棘手的问题:应用一致性。解决方案有:分布式事务、消息补偿、对账和自动修复。

消息系统

代理

代理的目标是性能、路由、安全、透明、迟加载、隐藏复杂实现细节。

网络代理技术

网络代理技术主要有四层代理(IP+Port,LVS)、七层代理(IP+Port+Application,Ngnix)。四层代理性能更高,七层代理更灵活。

代理模式

设计模式中的代理模式:静态代理、动态代理。

  • 参与者: 目标实例、代理实例、代理逻辑。 目标实例是已知的,需要代理的逻辑需要指定,代理实例需要生成。
  • 静态代理: 编写和目标实例具有相同行为的代理实例,并将对目标实例的请求转发给这个代理实例上。由于总是需要为目标对象手动编写代理实例,因此称为静态代理。静态代理容易理解,但不够灵活。
  • 动态代理:动态生成和目标实例具有相同行为的代理实例,并将对目标实例的请求转发给这个代理实例上。动态体现在可以为不同行为的目标对象生成相应的代理实例,而不是手动去编写代理实现。常用动态代理有 JDK 代理和 CGBLIB 代理。
  • JDK 代理:通过 java.lang.reflect.Proxy.newProxyInstance + 反射机制实现。适合对接口代理。代理逻辑通过 InvocationHandler 接口定义,Proxy 将实现 InvocationHandler 的实例传入构造器,生成动态代理实例。代理实例的类继承自 Proxy 。Proxy 通过proxyClassCache 来管理 ProxyClass 和 ProxyFactory ,并在 getProxyClass0 的时候去缓存 ProxyClass 的信息。

HTTP代理

  • HTTP 代理就像客户端与服务器之间的拦截器。既充当客户端的角色,又充当服务器的角色。代理可以级联,组合使用。可以通过 Trace 方法和 响应头的 Via 首部来追踪报文途径的网关和代理(Via 有安全与隐私问题)。
  • HTTP 代理的作用:过滤(不宜内容)、访问控制与审计追踪(安全)、安全防火墙(安全)、流量监控(安全)、缓存(性能,降低网络开销和拥塞)、反向代理(性能)、内容路由器(增值服务)、转码与压缩(国际化与性能)、匿名(安全与隐私)、路由与负载均衡(稳定性)。
  • HTTP 代理的部署: 出口(LAN 出网点,过滤、安全)、入口(缓存与性能)、边缘(反向代理)、对等交换点(缓存与安全)。
  • 使用 HTTP 代理的方式: 浏览器配置、交换或路由设备拦截、修改 DNS 、重定向。客户端代理配置 -- PAC 文件(提供一个URI, 指向用 JS 写的代理自动配置文件,会动态计算适合的代理配置);自动代理发现 -- WPAD ,按顺序尝试 DHCP(动态主机配置协议)、 SLP(服务定位协议)、DNS Known Hosts、DNS SRV 等技术,自动发现合适的 PAC 文件。
  • HTTP 代理的问题及方案:客户端发给代理的 HTTP 请求报文里应当是包含主机名的完整 URI。但客户端并不总是知道对方是代理,或者并不知道代理的存在。因此通用代理需要进行“缺失主机名的部分 URI 补全”,拿到主机名拼成完整的 URI(没有代理时浏览器也会做类似的事情)。某些代理会对 URI 做细微修改,影响互操作性。代理的容错机制(解析到的主机是已停用服务器时)。
  • 代理认证:客户端发送请求,代理发现没有认证,会返回 407 响应码,客户端拿到 407 后搜索和拿到证书,重发请求,代理认证通过。

组件和配置化

  • 组件化: 配置化的基本前提。组件需要定义良好的行为规范和接口规范。
  • 流程的组件编排:将整个流程划分为若干阶段,定义每个阶段的行为和目标,将每个阶段实现为组件,然后通过编排配置将组件连接成完整的流程。
  • 动态语言脚本。比如订单导出使用 Groovy 脚本配置报表字段逻辑。 脚本注意做成缓存对象,避免可能的内存泄漏。
  • 选项参数。选项参数的原型是命令行参数。用户可以通过选项参数来选择策略、调节性能等。
  • 规则引擎。 将业务逻辑表达为若干条规则,然后用工作流将规则集合串联起来。

组件与配置化实践可阅: “有赞订单导出的配置化实践”“事件处理业务的简易组件编排框架”“基于规则和规则引擎的系统”

API组合的扩展性

将系统和模块的功能通过若干清晰、易用、正交的、易组合的API公开出来,不仅能够构建灵活的外部应用,还能发掘出系统原来具备但并未提供的功能。

此外,公开的API 能够增强系统与其它系统的集成性,避免成为一个系统孤岛。

柔性编程

落实到编程层面,即是:

  • 组件化编程:将系统功能分组、分类、模块化,提炼成可复用的组件。
  • 基于接口编程:抽象对象行为,定义扩展点接口,通过接口来交互。
  • 设计原则与模式:应用设计原则(SOLID, KISS)指导,使用设计模式(策略模式、组合模式、装饰器模式等)实现。
  • 建立代码关联:建立代码的关联关系,通过关联关系自动传递改动。
  • 占位符思想:规范、识别、注册、使用。
  • 持续小幅重构。

可阅:“基于接口编程:使用收集器模式使数据获取流程更加清晰可配置”“设计模式之模板方法模式:实现可扩展性设计(Java示例) ”“由一次重构引发的对可扩展性的思考”


代码技巧

实现可扩展代码的四个基本步骤:

  • 识别变化;
  • 抽离共性,定义接口;
  • 实现子类;
  • 注入子类实现,实现处理框架。

可阅: “实现可扩展代码的四步曲”

CI/CD 和容器化部署

  • 持续集成,快速迭代功能到系统中。
  • 分流发布:灰度发布、蓝绿发布。小批量验证。分流系数可动态配置和生效。
  • 容器化部署,提升系统部署的可伸缩性。

预测业务变化

实现扩展性的前提是能够预测业务变化。既不过度设计,也不是完全不考虑扩展。

业务变化的方向:

  • 相似需求:比如来了一个检测流程 A,然后又来一个检测流程 B,B 的流程 与 A 基本相同,仅有少量差异;
  • 不同场景的相似功能:画了 CPU 的利用率曲线,也要画内存的利用率曲线;
  • 流程中的环节增加:比如在原有检测流程中增加一个新的检测子环节;
  • 流程中的分支增加:需要针对不同条件做判断和逻辑;
  • 列表扩充:功能实现中有一个列表,列表元素会不断扩充;
  • 变化频度: 现在变化频繁,将来变化也会频繁。

posted @ 2023-04-17 12:56  琴水玉  阅读(1082)  评论(0编辑  收藏  举报