在 “软件设计要素初探”[1] 一文,尝试从软件设计的整体角度,综合讨论了软件设计的各种要素。本文探讨确定系统整体结构的架构模式。
概述
「架构模式是系统组件及组件交互的模式,决定了处理数据和领域对象的全局控制结构。」
架构模式包含三个要素:
- 划分方式:是在技术层面划分,还是在领域层面划分。
- 核心组件:包含哪些核心组件,组件之间如何交互。
- 设计重点:每一种架构模式都有其核心概念和设计重点。
架构模式评价
架构模式评价是指了解各种架构风格的优势和劣势,从而为业务选择适合的架构。
- 简单性:理解、构建和维护系统的简单性。越简单越好。
- 模块化:模块内内聚的程度(或模块之间的耦合程度)。核心架构概念。《Fundamentals of Software Architecture》第三章专门探讨了模块化。后续单独写一篇文章。
- 性能: 架构支持缓存、异步、并发、批量等性能手段的能力。
- 高可用:故障恢复时长,不可用时间、不停机服务。出现局部故障时能否不影响服务可用性或者减少影响范围。不可用时长越少越好,影响范围越小越好。这很考验互联网企业的技术水平。最近两年互联网企业发生的几起大规模不可用事件,也能给予人很好的启发。支持高可用的技术主要有复制、冗余、负载均衡、异地多活等。
- 可靠性:考虑各组件可靠性及网络拓扑对整体系统可靠性的影响。如果任一组件失效都会导致系统失效,则可靠性为组件可靠性之积;如果所有组件失效才会导致系统失效,则可靠性为组件可靠性的最低值。此外,网络流量大的系统要达到高可靠性需要付出更多的努力。
- 可扩展:扩展新功能且对现有系统不影响的改动成本。越少越好。
- 可伸缩:通过水平方式扩展系统能力(性能和容量)。越简单越好。
- 容错:错误发生后影响系统服务的程度。越少越好。
- 易测性:mock,单一模块测试,自动化测试的难易程度。越容易越好。
- 易部署性:整体系统部署成本及部署风险;改动后的部署成本和部署风险等。
架构模式清单
分层模式
图片
分层架构是一种基于职责的单体架构。体现了康威定律:组织结构决定系统设计和开发。当还没有确定架构模式时,分层模式是一个好的起点。网络栈协议是分层模式的典型应用。
将应用划分为多层,定义各层的接口、职责,以及各层之间的通信与交互。业务系统通常会划分为表现层、业务逻辑层、持久层、数据层。业务逻辑层还可以分离出领域层。应用分层模式时,为确保系统可维护性,通常第j+1层只依赖于第j层的接口和服务。在性能场合下,可能会有跨层依赖的情况;应对特殊场景的设计中,会有“第j层的服务依赖于第j+1层的服务”的反向依赖关系。但这是个反模式,最好不要这么做。
分层体现了关注点分离原则。设计重点在于层的开闭与隔离(是否允许跨层访问)。开闭是指某一层的上一层能否直接绕过本层访问其下一层。开是能够,闭是不能。层的闭可以起到隔离作用,即层的下一层改动不会影响到层的上一层,从而起到解耦的作用。如果跨层访问非常多,可能意味着分层模式不太适合。
分层模式的最大优点是简单易用。定义好各层的职责和边界,就可以愉快地编码了。
设计要素:
- 划分方式:Technical-partitioned。
- 核心组件:Layer
- 设计重点:层的开闭。
- 优点:单体应用;简单,容易理解与构建。
- 缺点:规模扩大后简单性优点被削弱;少量代码改动就需要整体重新编译署和回归测试,部署风险较高,可测试性低,可伸缩性低,容错性低;需要手动添加负载均衡、多线程、缓存等方式来提升性能和高可用。
Pipeline模式
图片
管道-过滤器链是一种基于流程的可扩展的单体架构。一个请求沿着管道连接的链路,依次由链上的过滤器进行处理。过滤器可以有多种类型:数据转换、数据过滤、数据收集、数据存储、数据显示等。
- Shell 基于管道的命令组合是 Pipeline 模式的范例。
- JavaWeb应用的Servlet架构模式是“过滤器-处理器链”的典型应用;
- Java stream 也是 Pipeline 模式的应用。
- 数据同步也适合采用 Pipeline 模式,
Pipeline 模式的最大优点是简单性与可扩展性。只要在链路上添加新的过滤器,即可扩展链路的处理能力。
设计要素:
- 划分方式:Technical-partitioned。
- 核心组件:Filter, Pipeline,Context。
- 设计重点:Filter接口交互。
- 优点:单体应用,细粒度的模块化;简单,容易理解、构建和部署;可扩展性良好(添加新的Filter即可);可伸缩性良好(可以针对Filter做水平扩展);可测试性良好(可针对Filter独立测试)。
- 缺点:少量代码改动就需要整体重新编译部署,部署风险高,容错性中(一个 Filter 失败可能无影响,可能会导致整体流程失败);需要手动添加负载均衡、多线程、缓存等方式来提升性能和高可用。
微内核模式
图片
亦称“插件模式”。是一种基于功能组合的可扩展可定制的单体架构设计。
微内核架构主要由两种组件组成:
- 核心系统组件:提供最小可用功能集及插件的注册、加载和管理;
- 插件:用于扩展和定制功能。插件通常是相互独立的。插件可以是基于运行时或编译时。运行时部署的插件的优点是灵活装载与卸载,插件变化核心系统不需要重新编译部署,缺点是不太好管理插件。编译时部署的插件是容易管理,但插件有变化则要整体重新编译部署。
核心系统组件和插件,既可以通过接口(同步)也可以通过消息中间件(异步)来交互。通过同步来通信的优点是可靠,能及时处理插件失败,缺点是核心系统可能受某个插件运行的影响从而影响整体。通过异步来通信的优点在于解耦,缺点在于插件如果失败的话,很难快速通知到核心组件。
微内核的最主要特点是可扩展性和可定制。通过插件实现功能的解耦。其设计重点是定义良好的插件接口及插件交互机制,开发者只要遵循规范编写和调试具体的插件并融入到系统,即可为系统增加新功能和新特性。
使用微内核架构的系统通常是产品型系统,有 Eclipse, Emacs, Firefox, Chrome 等。
- 划分方式:Technical-partitioned,domain-partitioned。
- 核心组件:Core-System, Plug-in Components
- 设计重点:最小功能集设计、插件加载与管理。
- 优点:单体应用,细粒度的模块化;简单,容易理解、构建和部署;定制性良好;运行时插件可动态装载或卸载;可测试性良好(针对插件单独测试);容错性中等(某个插件失败不影响整体,除非Core-System失败)。
- 缺点:可伸缩性中;需要手动添加负载均衡、多线程、缓存等方式来提升性能和高可用。
MVC模式
MVC ,“模型-视图-控制”, 经典的 WebUI 架构模式。控制器处理请求从而更新模型和返回视图,模型更新驱动视图更新,视图请求控制器处理。
经典的 JavaMVC 框架有 Struts2, SpringMVC, 前端 MVC 框架有Extjs4。
设计要素:
- 划分方式:Technical-partitioned。
- 核心组件:Model, Viewer, Controller。
- 设计重点:model, controller
- 优点:单体应用,细粒度的模块化,简单,容易理解、构建和部署;前后端分离。
- 缺点:可靠性较低,可伸缩性低。
微服务模式
图片
微服务模式是一种基于领域服务的高度解耦的可扩展可伸缩的分布式架构。
将单体应用分解为多个具有明确领域定义的业务子域,将每个相对独立的业务子域实现成单独的微服务。微服务独立管理各自子域的问题,采用不同的架构和方案来适配自身领域的问题,最终所有微服务集成起来完成整体应用功能。实现独立自治和发展、模块化、分工协作等。微服务解决的是基础服务和数据层的复用问题。
- 领域与代码复用。
- 同一数据源的统一访问。
- 单独扩容某个服务容易。
微服务适用于中大型互联网应用。不过微服务也有一定的复杂性。微服务面临的问题是服务治理。主要包括:限流/熔断降级、配置管理、日志中心、监控预警、链路跟踪、故障隔离、动态扩容、分流发布、全链路压测、中间件支撑、团队组织架构适配与管理。微服务意味着要搭建一整套成熟的技术体系。
微服务要面对的若干问题:
- 拆分粒度:容易拆得太小。需要迭代出好的粒度。
- 通信:让某一个领域服务成为 Mediator 或 单独构建一个 Mediator 服务。
- 跨服务事务:增大服务粒度,使事务变成单体的;rollback;undo 机制。
设计要素:
- 划分方式:Domain-partitioned。
- 核心组件:Domain, Bounded-context
- 设计重点:拆分粒度、数据隔离、边界划分。
- 优点:分布式系统,细粒度的模块化;可伸缩性、可扩展、高可用、容错性佳。
- 缺点:较为复杂,实施成本较高,性能方面中等(有很多网络调用);可靠性中等(占用网络带宽、网络延迟)。
事件驱动模式
图片
事件驱动是一种基于消息的可扩展的分布式架构。
在系统内定义一系列的组件、事件及监听器,组件发生变化时触发事件,通知相应的监听器处理事件更新组件,进而触发新的事件,如此循环直至手动终止系统或系统崩溃。GUI 应用是事件驱动模式的典型范例。事件驱动模式通过消息进行解耦。从事件驱动模式可以衍生出订阅-消费模式。大型互联网应用中几乎都存在订阅和消费业务表更新或业务消息推送的子系统。需要高稳定可用的消息中间件,并仔细评估消息延迟对用户活动造成的影响。新品消息推送、商品消费订阅、发货提醒等,我们正处于一个“消息/通知的订阅-推送-被消费”的移动互联网时代里。
从事件驱动模式还可以衍生出 Actor 模式。基于事件驱动的分布式的、异步并发的、可伸缩的、有故障恢复能力的大型消息处理架构。一个简单例子可参见:“混合使用ForkJoin+Actor+Future实现一千万个不重复整数的排序(Scala示例)”[2]
设计要素:
- 划分方式:Technical-partitioned。
- 核心组件:Events, Queues(Channels), Event-processors 。
- 设计重点:广播机制、通知机制。
- 优点:单体或分布式系统,细粒度的模块化;解耦良好、性能优(异步+并行处理)、可扩展性好(容易添加新事件处理器)、可伸缩性好(负载均衡)、容错性好(异步响应、重试、补偿、最终一致性)。
- 缺点:较为复杂、可测性低(异步,执行路径不确定性、事件树流图的组合非常多)。
规则-工作流模式
将系统分析成一系列的工作流节点以及规则的解析匹配,使用规则引擎来控制和运行,通过添加规则及规则流,实现可扩展性和可配置性。规则-工作流模式实现了逻辑表达与执行的分离,计算组件的复用。优点在于可扩展、可推导、自解释性,面向业务人员使用。可参阅使用规则引擎计算工资的简单例子:“Java Drools5.1 规则流基础【示例】”[3],“基于规则和规则引擎的系统”[4] 。
设计要素:
- 划分方式:Domain-partitioned。
- 核心组件:Rule, Workflow。
- 设计重点:规则、规则引擎,工作流引擎。
- 优点:单体或分布式,细粒度的模块化;容易理解和维护;可扩展性好;易于部署和使用。
- 缺点:可测性中等,独立规则容易测试,但组合规则可能出现非预期结果;性能取决于引擎性能。
Restful 模式
适合资源构建与共享的规范的可伸缩的架构模式,适合于构建API接口。
- 全局规范一致的资源逻辑命名、寻址、返回码定义,关注资源与数据而非行为或服务。
- 无状态的声明式的请求;对客户端隐藏实现细节。
- 通用的数据返回格式(JSON),跨语言与平台。
- 使用名词而非动词作为路径名。
设计要素:
- 划分方式:Domain-partitioned。
- 核心组件:Rule, Workflow。
- 设计重点:规则、规则引擎,工作流引擎。
- 优点:单体或分布式,细粒度的模块化;容易理解和维护;可扩展性好;易于部署和使用。
- 缺点:可测性中等,独立规则容易测试,但组合规则可能出现非预期结果;性能取决于引擎性能。
参考资料
- 《Fundamentals of Software Architecture》[5]
- 《架构之美》第5章:“Web:面向资源的架构”。
Reference
[1]“软件设计要素初探”:https://www.cnblogs.com/lovesqcc/p/7572682.html
[2]“混合使用ForkJoin+Actor+Future实现一千万个不重复整数的排序(Scala示例)”:http://www.cnblogs.com/lovesqcc/p/5540415.html
[3]“Java Drools5.1 规则流基础【示例】”:http://www.cnblogs.com/lovesqcc/archive/2011/01/18/4037863.html
[4]“基于规则和规则引擎的系统”:http://www.cnblogs.com/lovesqcc/archive/2012/05/16/4037817.html
[5]《Fundamentals of Software Architecture》:https://book.douban.com/subject/35306892/