设计演进
演进式设计
向微服务演进
我们的一个项目,采用了简单分割的分布式架构,Front End面向客户的用户,而Office End则面向业务人员和系统管理者。随着需求增多,功能越来越复杂,系统各个模块的边界开始变得越模糊,形成了一个逻辑散乱的庞大代码库。重复代码与重复数据俯拾皆是,而Front End与Office End之间的集成也非常复杂。负责开发这两个模块的团队虽然属于同一个项目,但团队之间存在极大的技术和业务壁垒,团队成员对整个系统缺乏整体认识,知识没有能够在团队之间传递起来。
图:分为Front End与Office End的分布式架构
新的业务服务如约而至,项目面临抉择:究竟是得过且过,然后看着代码库逐渐庞大,复杂而僵化以至于难以维护;抑或风险驱动,通过演进改进系统架构,同时进行新需求开发?
智者,自然会选择后者,毋庸置疑。
观察这个架构,它是Monolithic System的典型体现,体现为系统复杂,没有形成松散耦合的模块结构,没有清晰的层次和边界划分,进而导致复杂的集成。因而,我们选择以划定清晰边界为系统演进过程的起点。考虑重用和统一接口的因素,我们以REST服务为重用粒度。
识别服务时,可以运用Bounded Context来确定服务的边界。在《领域驱动设计》中,Eric Evans将Bounded Context定义为[1]:
应该显式地定义某个模型所应用的上下文。还应该在团队组织、应用中特定部分的使用以及像代码库和数据库样式等物理表现显式地设定边界。要保持边界中模型的严格一致,不受外界问题的影响与干扰。
Bounded Context之间应尽可能地保持解耦,即使互相之间存在依赖关系,也尽可能依赖暴露在边界外的接口。这恰好也符合REST服务的要求。由于Bounded Context体现的是领域概念,因而我们也可以将其定义为领域服务。
当我们把之前相对混乱和模糊的功能模块全部演进为REST服务后,对外公开了统一的服务接口,UI的展现逻辑就与领域逻辑完全分离了。UI Applications是一个薄薄的展现层,以RESTful的方式调用服务,也使得服务在保证接口不变的前提下能够单独演化。每个服务都是独立的,甚至可以采用微服务的风格,将每个服务部署为Standalone的形式。这就使得服务不仅保证了逻辑边界的隔离,甚至还隔离了物理边界,因而可以针对服务建立特性团队(Feature Team)。服务的重用性和可扩展性也有了更好的保障,服务与UI之间的集成变得更简单,整个架构更加清晰了。
图:基于领域服务的架构
演进的技术要素
REST服务
整个架构演进的中心思想是建立服务级别的重用粒度。我们希望服务遵循统一且定义良好的契约,形成能够独立演进的自治服务,并降低服务与客户端之间的集成复杂度。自治服务的体现是它具有定义清晰的服务边界。服务提供的功能是最小完整的,并以尽量降低对外部的依赖为设计准则。自治服务能够自我履行消费者发起的请求,针对请求执行相应动作,并在可能的情况下完成状态之间的迁移。
REST服务完全符合自治服务的特征,它提供了一组架构约束,当作为一个整体来应用时,强调组件交互的可伸缩性、接口的通用性、组件的独立部署、以及用来减少交互延迟、增强安全性、封装遗留系统的中间组件。REST采用了现有的Internet标准,包括HTTP、XML和TCP/IP,能够更好地利用HTTP去查询或操作服务资源。
Roy Fielding在其论文中描述了REST的特征[2]:
Web是旨在成为一个Internet规模的分布式超媒体系统,这意味着它的内涵远远不只仅仅是地理上的分布。Internet是跨越组织边界互相连接的信息网络。信息服务的提供商必须有能力应对无法控制(anarchic)的可伸缩性的需求和软件组件的独立部署。通过将动作控制(action controls)内嵌在从远程站点获取到的信息的表述之中,分布式超媒体为访问服务提供了一种统一的方法。因此 Web的架构必须在如下环境中进行设计,即跨越高延迟的网络和多个可信任的边界,以大粒度的(large-grain)数据对象进行通信。
Consumber Driven Contract
针对服务演进,Ian Robinson提出了Consumer-Driven Contracts模式。
一方面,为了保证迁移遗留系统的正确性,另一方面也为了保障软件的质量,我们针对这些暴露的接口开展了自动化测试,即所谓的Consumer Driven Contract Test。
图:Consumer Driven Contract
图片来自Ian Robinson的文章Consumer-Driven Contracts: A Service Evolution Pattern
持续交付
这些自动化测试还需要与持续交付结合起来,形成一个自动部署的流水线。
演进的文化要素
康威定律(Conway's Law)
康威定律(Conway’s law)认为:一个组织的设计成果,其结构往往对应于这个组织中的沟通结构(organizations which design systems ... are constrained to produce designs which are copies of the communication structures of these organizations)。
图:企业组织结构对康威定律的形象表现
图片来自Manu Cornet:Manu Cornet’s visual interpretation of Cornet’s law
演进前的架构自然而然会催生出关注Front End与Office End的两个组件团队(component team)。这样的组件团队缺乏对整个系统的整体认识,团队之间存在严重的技术与业务壁垒。当我们利用Bounded Context识别出诸如account,calender等领域服务后,我们可以为这些相对独立完整的领域服务分别建立特性团队(feature team)。一个特性团队就是一个针对端对端(End to End)的完整团队,包括了需求分析、设计开发与测试、运维等职责。至于跨特性团队之间的合作,则可以通过建立CoP(Community of Practice),形成需求分析、开发与测试的纵向社区,以利于各个角色跨团队的沟通与协作。例如通过PO(Product Owner)协调和识别需求的优先级,通过TL(Tech Leader)识别技术风险、技术债务,并作出技术决策,通过TM(Test Manager)来确定测试战略,确定测试计划。在项目层次,还需要项目经理协调多个团队的Leader一起召开计划会议、演示会议等,保证迭代的正常进行。
引入客户
演进式设计的最大风险在于客户的参与度。没有客户的参与,设计的功能就无法及时地获得反馈。演进式设计的最大特征是设计以迭代形式进行,由于每个迭代都能交付一些可用的功能,这为客户交流建立了可靠的基础。
设计即交流
我们希望设计并不以文档的形式存在,或者说,设计的目的并非为了产生文档,而是为了更好地交流。设计无论多么优良完美,如果团队人员都不能理解此设计,又或者设计从一开始就偏离了客户的需求,这样的设计始终是糟糕的。
交流的手段有很多,在设计领域,我更期望以可视化的方式去展现自己的设计思想,并辅之以简单直接的工具,例如白板、即时贴等,有效地构筑交流的渠道。许多设计方案也都可以通过可视化的方式展现出来,例如前面提及的Bounded Context,Context Map,以及我观察采用的六边形架构。如下图所示:
图:Bounded Context
图:Context Map
图:六边形架构
这种可视化的手段不仅仅是绘图,而是充分调动团队成员的参与度,利用白板、即时贴等工具进行头脑风暴,又或者是以Workshop的形式驱动出整个系统的设计。一旦驱动出这样的设计,再以清晰直观的可视化形式展现出来,又可以进一步促进团队的交流。每当设计面临重构或者进一步演进时,都可以站在白板前,就展现出来的设计图进行讨论,轻而易举地对设计的草案进行修改。
例如,我通过运用六边形架构模式有效地识别出一个电子商务系统的内外边界,以及模块之间(或者子系统之间)的集成方式,通过确定通信的Port与Adapter,就可以进一步获得系统的应用逻辑架构与物理架构。
图:电子商务系统的六边形架构
上图直观地展现了电子商务系统如何与外部的支付系统以及物流系统的集成。例如,图中展现的Port实际上为防腐层(ACL)。之所以要建立一个防腐层呢,原因在于:支付与物流常常存在多个供应商,因而需要解除对供应商的绑定,并避免供应商系统的变化造成对电子商务系统的腐蚀。这是切合实际的决策。
这个电子商务系统需要与仓库管理系统集成。《面向模式的软件架构》卷四给出了一个仓库管理流程控制系统的案例。书中描述的非功能性需求,即所谓质量属性包括:
分布性。仓库管理流程控制系统天生就是分布式的。
性能。仓库管理流程控制系统不是一个“绝对的”实时系统,但性能仍与业务息息相关。对系统有整体的吞吐量要求,因此系统必须确保所有的运输指令能够被及时而有效地运行。
可伸缩性。不同仓库其大小可能会有很大的不同,因此仓库管理流程控制系统必须能既支持只有几千个箱子的小仓库,又要支持超过一百万个箱子的大仓库。
可用性。许多仓库操作采用三班倒的24/7模式工作,因此可用性是仓库管理流程控制系统对业务案例支持的关键因素。
假设要设计这样的系统以支持这些质量属性。对于分布式而言,书中提出的解决方案是传统的分布式系统解决方案,即引入Broker模式,在本地建立对远程对象的代理。而对于支持并发的领域对象访问而言,则采用了Active Object模式,并引入Leader/Followers并发模型来获得可扩展。然而通过对质量属性的思考,以及观察系统间的集成方式,则可以考虑引入消息队列与消息路由来实现系统的分布式。这其中当然会用到经典的Publisher-Subscriber模式。通过对领域逻辑进行识别,将整个仓库管理流程控制系统的领域逻辑分为三个Bounded Context。
- 库存管理
- 物流控制
- 拓扑管理
整个架构如下图所示:
对于库存管理而言,我认为它主要支持商品存放信息的数据管理,即获得商品数量、存放位置以及更新这些信息。对于该上下文而言,操作本身比较简单,且耗时较短。若出现大规模并发,其瓶颈也不在于获取或更新仓库信息(当然需要通过测试数据验证),而在于客户下订单后向仓库管理流程控制系统发起的发货请求。
我将发货请求放到了物流控制上下文中,除此之外,它还包括收货以及订单管理等。同时,对于物流控制与拓扑管理功能,基本上与具体的仓库形成了一一对应关系。此外,对于发货请求(或收货请求),并不要求很强的实时性,这使得对这些请求的异步处理成为可能。
物流控制由于牵涉到收货和运货,需要控制仓库的相关设备,并按照仓库的拓扑结构设定设备的路由。这说明物流控制与拓扑控制存在上下游关系,拓扑控制是上游。这两个上下文可以是Customer-Provider的关系。但它们之间不应该存在物理边界。因此,我将这两个上下文放到了同一个六边形中,而将库存管理放到了另一个单独的六边形中,以便于它们各自独立的可伸缩。
在库存管理与物流控制六边形之间,我引入消息队列来应对从库存管理子系统中转发而来的发货请求(发货请求实则又来自于E-Commerce的订单请求)。原则上,我针对一个物理的仓库建立一个单独的消息队列,因此库存管理在发送发货请求时,会根据商品的存放位置以及用户请求的IP地址,获得最优的仓库信息,然后通过Router将消息转发到正确的消息队列中。
一旦收到消息,物流控制系统作为消息队列的订阅者(或侦听器)就可以即使处理信息,进行后续的处理。
针对库存管理而言,我认为它是一个独立的物理边界,因此在可视化手段中,我展现为一个单独的库存管理六边形,如下图所示:
- 建立了针对REST服务的端口,对应的适配器为Controller,其目的是支持E-Commerce系统。事实上,我们对E-Commerce系统进行过分析,获得的六边形架构正好与此对接。
- 建立了针对DB的端口,对应的适配器为DB Gateway,它负责访问库存管理自身的数据库。数据库持久化的消息包括商品的基本信息如SKU、商品名、数量等,以及商品存放的仓库名。
- 建立了针对Queue的端口,对应的适配器为Message Router,负责将发货请求消息路由到正确的消息队列。
物流控制与拓扑管理放在同一个边界中,它是高度可伸缩的独立系统,为展现它的可伸缩性以及它与库存管理之间的集成,我在可视化手段中,展现出两个独立的六边形,如下图所示:
- 针对Queue的侦听器端口,对应的适配器为Message Handler。若有必要,如为了更好的支持并发,也可以在此引入Active Object甚至Leader/Followers。
- 提供了针对REST的端口,对应适配器为Controller。它主要是为了支持移动终端设备、Web应用,以便于相关人员直接发出发货或收货请求。
- 提供了DB的端口。这个数据库是对应仓库的专有数据库,与库存管理数据库无关。
- 提供了针对设备(指仓库的设备,如叉车,箱子,运输车等)的端口,对应适配器为South Gateway。
- 提供了针对配置文件的端口,对应适配器为Configurer。此功能是为了支持拓扑信息的动态配置。
- 提供了针对外部物流系统的端口,这里为其建立了Shipping的防腐层,使其能够更好地支持各个不同的物流供应商。
参考文献 [1]Eric Evans,领域驱动设计
[2]Roy Thomas Fielding: 《架构风格与基于网络的软件架构设计》