背景
软件架构设计是一个工程问题,设计的目的是为了能以最低的成本来满足构建和运维系统的需求。其实几乎所有的软件系统在设计之初都被要求是可以扩展的。对于一套持续运行的系统来说,运维的成本往往是开发成本的10倍以上。设计一套可扩展的软件系统去支撑多变的业务,已经成为了软件设计的一个主导原则。
设计可扩展的软件系统,有两个基本的条件,首先我们需要知道哪里可能需要扩展,其次就是如何设计去实现对扩展的支持。知道哪里是稳定的,哪里是变化的,本质上是需要我们在需求分析的过程中能够识别出哪些需求是稳定的,哪些是可能会发生改变的。识别可能会发生改变的地方,其实是一种预测。这种能力,往往需要一定的经验。即便是经验丰富的架构师,也有可能预测错误。
常见的扩展场景分类
下面将实际软件开发过程中经常遇到的有扩展性诉求的场景作一个分类,基本所有的扩展性诉求都逃不出这几类。
-
主体业务流程是稳定不变的,流程节点中的处理逻辑是变化的
典型的场景就是以前做过的一个物流场景下的分拣实操系统,对包裹进行入库、分拣、出库是一套分拣实操系统的基本功能,有SOP工序。包裹到达分拣中心后,先操作收货入库,再根据流程分拣成堆,然后装车出库。每个节点,针对不同的包裹(正向、逆向、拦截等)在处理逻辑上会有各自的特色定制逻辑。这时容易变化的点就是根据包裹类型处理入库、分拣及出库逻辑。如果不作设计,代码中就会出现很多if/else分支,一个方法上千行,改啥都要全量回归。 -
主体业务流程是变化的,但每个业务环节的处理逻辑是不变的
典型的场景就是OA系统,以请假审批流为例,这个审批流的流程是可变的,但每个审批节点要做的动作是不变的(同意/拒绝/加签)。例如,对于年休假,先团队负责人批,然后总监批,批完还要本人确认。对于婚假病假这类请假,先团队负责人批,然后总监批,然后要HR批,批完最后本人确认。而且请假审批流必须是可变化的,除了按假期类型修订审批流以外,还可能会根据请假天数的不同有不同的审批流,例如配置只有超过3天的假期,才需要总监审批。审批流中的动作,就是一些文本资料的保存(例如上一个审批人的留言或备注),虽然单个节点的处理逻辑是不变的,但总体流程是可定制的,如果一开始就把主流程用硬编码写死了,一旦主流程发生变化就要重新改,费时费力。 -
主体业务流程是变化的,流程节点的处理逻辑也是变化的,甚至底层数据模型也是变化的
这类场景多半在To B的SaaS系统中会遇到,企业级需求是变幻莫测的,几乎是全定制的,也是比较难运维的。 -
技术组件变化
例如,数据库从Oracle改成MySQL -
不改变核心的抽象流程,需要扩展新的功能
这类是最常见的,相对来说处理起来也是最容易的,例如,开发好了一个限流组件,现在要为该组件新增一种限流算法。 -
多租户需求增加,需要具有多租户隔离能力
随着组织管理的发展,组织隔离的诉求不断增加,需要系统具有多租户能力,租户之间需要进行数据层面的隔离,进而要求租户之间能够做到业务定制与业务隔离。
扩展性解决方案思考
面向对象的设计方法有6大设计原则,其中开闭原则是面向对象设计中最基础的原则,其他的5个原则是为完成这个最终的原则服务的,这个原则是目标,其他原则是手段,当然也是我们业务代码追求的最终目标。开闭原则很简单,对扩展开放,对修改关闭,即在添加新功能时,不能或者尽量少得修改老功能,而只添加新功能的代码。
当然,完全做到开闭原则很难,也基本没意义,我们需要尽量做到开闭原则的基础上支持新功能的开发。面向对象设计原则中最重要的就是封装变化点(将稳定的部分与变化的部分分离)这一原则,通过各种设计模式、设计思想、开发框架等方式,将变化的部分与稳定的部分分离,分别维护,是提高代码可维护性、扩展性的一大良方。
稳定的领域模型设计(DDD)
在思考如何将软件中稳定的部分与变化的部分分离的过程中,我发现,对于业务系统中某一个具体的领域,稳定的部分只能是稳定的领域模型。虽然领域模型也是不断变化的,但这种变化与多变的需求是不一样的,领域模型的变化是随着认知的升级,对领域理解的提升而变化,不是随着需求的变化而变化,也就是说领域模型是独立于需求的,是业务系统中比较稳定的内核部分。
谈到DDD,就包含聚合根、实体、值对象、限界上下文、防腐层等概念,在此处就不详细展开了,通过这一系列DDD手段,我们能够形成一个相对稳定的内核,通过领域事件将跨领域的耦合降到最低,通过界限上下文、防腐层再辅以依赖倒置等方式,可以解决跨领域的调用问题。最终目标是形成一个个相对稳定,对外能提供稳定服务的,不依赖基础设施层的领域。
当然,要实现这一目标并不简单,过程中会遇到各种各样的问题,尤其是针对老系统的重构,由于存在各种各样的系统间依赖、跨领域依赖,以及如何确定合适的限界上下文等等,都是需要不断讨论的。
基于事件机制的依赖解耦
对于系统间的依赖,如果跨了微服务,那通常是采用RPC调用或者异步消息通知这两种方式,再辅以合适的防腐层,基本能将系统间的依赖降到很低的程度。
但是针对单一系统内部,不同领域之间的依赖,尤其是之前没有建立稳定领域模型的业务系统,系统内的模块间调用,通常都是同步的,并且通常是经由基础设施层直接访问DB来解决的。这种方式在系统开发初期、系统逻辑较简单的情况下是非常高效的,不用额外设计异步消息发送、消费框架,不用考虑诸如分布式锁、分布式事务、最终一致性、幂等性等问题。然而随着业务的发展,系统越来越复杂,这种附着在正常业务代码之外的,跟领域事件相关的逻辑会越来越多,这时就需要一种统一的基于事件的异步处理机制(EventBus事件总线就是一种较好的方式)。通过这种方式,可以保持领域之间的隔离性,领域内部只关注跟本领域相关的领域事件,并在合适的时机发送本领域的事件到领域外部。领域之间可以尽量减少同步的调用,彻底断绝跨领域的直接依赖。
插件/业务扩展点模式
在应对多变的业务需求时,如果仅仅是采用if-else式的处理方式是无法支持无穷无尽的业务需求的,需要我们找到一种复合开闭原则的业务个性化需求定制方式。
参考NBF(盒马的新零售服务开放框架)以及COLA等框架或者解决方案,我们需要一种支持多维度扩展点的方式,在业务节点的适当位置,为节点提供各种扩展点,同一类型的扩展点可以针对不同的业务身份、维度有不同的业务逻辑。这种扩展点可以用于校验验证,也可以用在额外的追加逻辑上、额外的业务逻辑上等等,甚至可以提供一种简单的插件机制,为业务的扩展提供无限可能。
例如Spring框架在Bean的生命周期全程中都留了足够多的扩展点,供应用层实现,从而在Bean的生命周期各个节点织入业务逻辑。当然,合适的技术选型很重要,这种框架通常会对业务代码有较大的入侵性,一旦选定,后期替换的成本会比较大,需要慎重。
采用这种方式,常用于解决上一节中的第一种场景——主体流程稳定但单个环节的细节多变,如果设置足够多的扩展点,就可以基本做到开闭原则,有新的定制需求,无需修改原有逻辑,只新增新的逻辑即可,系统稳定性大大提升。
流程编排/规则引擎
流程编排/规则引擎对于重流程/工作流是场景非常适合的,这种模式可以用来解决上一节的第二种场景——主要的变化集中在整体规则和流程上。
通过稳定的Domain,来保证节点内部逻辑的稳定性,高内聚性;节点间的顺序、前置条件等完全依赖领域模型的上层例如应用层、业务流层。这一步骤做的好,可以在领域层,甚至应用层形成一系列可以服用的基础能力,发展起来就是所谓的中台能力。通过业务编排,可以在较短的开发周期内,支持一套全新的业务流程,当然这依赖于节点间的解耦程度,如果节点间是绝对的低耦合高内聚,那相信这种需求还是不难的。
微内核插件化架构
插件化架构是一种面向功能拆分的可扩展架构,其内核部分功能比较稳定,不会因为业务功能扩展而不断修改,插件模块可以根据业务功能的需要不断地扩展,微内核的架构本质就是将变化部分封装在插件里面,从而达到快速灵活扩展的目的,而又不影响整体系统的稳定,这种架构可用来解决上一节中的第五种场景。
插件的依赖管理需遵循依赖倒置原则,即内核部分应该只依赖接口或抽象类而不应该依赖具体实现类,这样才能在代码运行过程中选择具体的实现类。关系型数据库MySQL就是采用插件化架构,server层定义增删改查的接口,存储引擎去实现,存储引擎的加载、卸载、升级与server层解耦。
元数据驱动引擎
元数据引擎模式用来解决上一节的第三种场景——业务流程及流程中的细节都是变化的,元数据引擎常见用来解决数据模型的扩展,也会涉及到一些流程和逻辑的变化。SaaS化租户的核心诉求是数据、模型、业务逻辑隔离,灵活扩展。传统的可扩展多租户架构是基于关系数据驱动的设计,即不同的租户共享的底层同一套物理表结构,比如订单表、商品表、库存表以及对应的核心模型。这些不同租户都依赖同一个模型,系统对模型的抽象程度就决定了其对不同租户的扩展能力,包括需求扩展和底层数据结构扩展,一旦底层模型抽象的不够好,就需要通过增加属性的方式来解决,久而久之就造成的模型的腐化和流程的糅杂。
但是,如果我连业务领域是什么还不知道,能不能做数据扩展呢? 可以的,Salesforce的force.com就是这么做的,其底层数据存储完全是元数据驱动的(metadata-driven),它用一张有500个匿名字段的表,去支撑所有的SaaS业务,value0到value500都是预留的业务字段,具体代表什么意思,由metadata去定义,把不同租户对模型的抽象放到了元数据层。说实话,这种实现方式的确是一个很有想法,很大胆的设计,也的确支撑了上面数以千计的SaaS应用和Salesforce千亿美金的市值。
我们以前依赖具体关系型数据库的功能都要转变成依赖元数据层,Salesforce将其元数据管理实现方案以论文的形式公开了,文章洋洋洒洒太长了,网络上有许多课代表的读后总结。Salesforce元数据驱动架构最大的特点是实现了一套通用的数据存储方案,做业务数据隔离的同时,扩展性极好,能快速满足用户的个性化需求,而且不需要考虑底层数据模型的羁绊。
但是,现实生活中,软件所需解决问题都是确定的,不需要像Salesforce那样提供“无边际”的扩展能力。