作者 |  陈璐 

在软件开发领域,解耦这个词相信大家都不陌生。在面向对象的语境下,我们会应用SOLID原则来构建高内聚低耦合的应用,实现模块间的解耦;在复杂业务系统分析和建模时,会通过DDD的战略和战术设计帮助划分领域并实现分布式系统中服务的解耦;当我们在组织大型敏捷开发团队协同工作时,通过组建自治团队来减少摩擦,从而实现团队级别的解耦。

可以看到解耦无处不在,并且以此为目的投入,大家都会觉得是无比的政治正确,因为实现了解耦,我们的系统和应用就能更快速的扩展和演进,我们的团队就能更顺畅的合作并能更加快速的实现业务价值。

但是,当我们暂时抛开将得到的种种好处,思考要如何去实现它时,却发现解耦这个词表达的意义过于抽象和模糊,它既没有描述最终的状态也没有提供实现的方法。那当我们谈解耦的时候,具体内容是什么呢?

从字面上理解的所谓耦合,通常是指两个或两个以上的物体或者体系之间相互作用彼此影响,对应到软件研发的以上场景,我们可以转换成是指两个或两个以上的模块/系统/团队之间相互作用彼此影响。

在软件需要解决的业务问题越来复杂的今天,单个的系统或者团队很难在不依赖外部的情况下去实现业务目标,所以我理解的解耦并不是要消除耦合(彼此的作用和影响/依赖),而是指我们应该如何通过一定的方式和规则,来设计和管理以上提到的多个元素之间的依赖,降低耦合程度来使整个系统有序顺畅的运转。

本文将从服务间上下游的思维来讨论如何在系统架构演进过程中,持续的保持服务间的松耦合,实现解耦的目标。

上下游思维定义

关于服务的上下游的定义,在DDD建模方法中,在确定了限界上下文(bounded context)后通过在上下文映射(context mapping)中使用上下游来表示上下文依赖的方向,其确定的依据是下游需要了解上游的领域知识实现业务,反之则不会。引申的含义就是上游的业务能力可以不用关心下游业务的存在,下游业务的开展依赖于上游提供的业务能力。下图是限界上下文映射的一个例子:

图片

图片出处:https://www.oreilly.com/library/view/what-is-domain-driven/9781492057802/ch04.html

当我们基于以上的限界上下文设计领域模型并落地时,理想的情况是一个限界上下文对应一个应用服务。参考限界上下文的上下游关系,我把上下游思维定义为:上游服务不受下游服务的业务能力和可用性影响,反之则相反。我们会发现服务间的上下游关系比限界上下文中领域知识的上下游关系更复杂,而且上下游关系也会随着集成方式的不同而变化。

基于上下游思维的耦合级别

基于服务上下游的思维,我把服务间依赖按以下维度进行耦合度分级:

  • Level4: 领域知识互为上下游,业务可用性互为上下游

  • Level3: 领域知识互为上下游, 业务可用性为单向上下游

  • Level2: 领域知识为单向上下游,业务可用性互为上下游

  • Level1: 领域知识为单向上下游,业务可用性为单向上下游

由于松耦合的业务模型利于松耦合的架构设计和业务的演进,同时松耦合的架构也利于组建松耦合的团队结构。业务模型作为松耦合设计的基础,以上的级别依据于这个思路定义的。

一种常见的Level4级别的情况是处于伙伴关系的上下文。比如订单服务与派送服务之间通过同步API的方式进行通信,用户订单下单成功,通知派送服务,派送服务完成,更新订单状态。两个服务通过API进行集成,服务需要相互知道对方的部分领域知识来完成API的调用以实现功能,同时业务的可用性互相关联,一方服务不可用,导致整个业务的中断。

如果希望耦合度向level3演进,不希望服务的可用性产生直接的依赖,我们通常会通过引入消息中间件来进行解耦,服务间通过消息的方式进行集成,由于某些原因,它们都按照对方的领域模型定义的消息结构进行通信。那么这种情况下,服务间的领域知识相互耦合,业务可用性与具体的服务解耦,与消息中间件的可用性耦合,我们需要关注如何提高消息中间件的可用性来保障业务的高可用。

Level2级别的耦合度是建立在清晰的领域限界上下文边界基础上的,在上面包含的订单服务和派送服务的业务中,派送服务作为上游在完成派送进行订单更新这个业务时,它将派送更新的内容发送至订单服务,订单服务再解析派送更新内容并更新关联的订单状态。那么在通过API的方式进行集成时,它们就处于领域知识的单向上下游和业务可用性互为上下游的状态。具体构建服务时,根据团队的组织结构和话语权的大小,又可以通过不同的方式来进行服务的集成。上游服务通常使用Open Host Service(OHS) / Published Language(PL)来提供业务能力,下游服务通过遵循上游的领域模型或者通过防腐层(Anti Cruption Layer - ACL)来完成领域模型的转换。处于这个级别耦合度的上下游服务在开放主机接口不变的情况下可以独立的进行迭代更新,否则需要通知下游服务评估影响并同步进行变更。

接下来可以更近一步,我们通常会通过引入消息中间件来对服务可用性依赖进行解耦来达到Level1的级别。处在这一级别的服务之间,由于有明确的上下文边界和依赖关系,消息的结构也是上游系统来定义和维护的。那么如何基于业务场景来设计消息结构、集成规则,以及支持兼容性的消息格式更新方式是这一级别需要关注的问题。

四种耦合级别中,从高到低对团队的业务建模和技术能力要求越来越高,也随着耦合度的减轻对新业务的适应能力越来越强。

通过耦合级别来做出架构上的权衡

那么基于上述耦合级别的区分,如何在设计架构时进行取舍呢?

对于处在level4级别的系统,如果服务都在团队的职责范围内,在保证高可用的前提下,在业务需求变化不频繁的情况下,它暂时可以工作。如果系统由不同的团队维护,或者需求变更频繁的情况下,需要对业务模型进行优化,通过定义清楚的上下游关系以达到level2级别以增强架构的适应性。

对于处在level3级别的系统,由于领域知识的耦合,服务都需要有其它领域的知识来完成自己的业务能力,随着服务的增多很容易退化成网状的依赖,通常新的业务变更需要同时修改多个服务,异步的集成方式也增加了扩展和维护的难度。处在这一层级的系统,优先级还是通过优化业务模型,定义清楚的上下游关系,至于是否需要使用异步的方式集成,需要综合权衡业务的实时性和一致性要求来进行权衡是过度到level2还是level1。

对于处在level2级别的系统,由于系统的上下游关系相对清晰,重点可以放在采用合适的方式来完成上下游系统的集成上以实现。一般上游系统通过OHS/PL在保证发布语言不变化的情况下,可以独立的进行迭代更新;下游系统是通过跟随或者添加防腐层来屏蔽上游业务模型变化带来的影响,取决于业务模型变化的频繁程度和添加新一层的成本。通常在绿地项目中,由于能从零开始进行业务建模和组建开发团队,在统一业务语言和明确上下游团队遵从关系的基础上,采用新的服务构建技术和实践,在上下游服务间同时使用OHS/PL和ACL会比较好的隔离相互之间的影响。上游服务专注于领域能力的迭代并通过OHS/PL来发布功能,下游服务通过ACL来隔离上游变化对自身领域模型的影响,同时也可以按需来使用上游提供的新的功能。

对于处在level1级别的系统,在业务和技术上都具备了松耦合的基础,但是此时需要警惕一种新的依赖产生。由于上游系统在消息格式的设计时没有按照使用场景来设计,或者消息格式不能很好的在向前兼容的情况下进行更新,这带来的后果是上游系统会成为下游新增业务的强依赖,因为任何的新需求可能需要上游系统定义新的消息格式来支持,上游系统会成为响应变化的瓶颈。如果服务在不同的团队中进行维护,那么带来的后果就是团队间的冲突。在这个级别的依赖关系中,合理的消息模式以及兼容性设计是迭代演进的关键。

消息集成通常分为两种风格Event Notification和Event-carried State Transfer,具体又可扩展为以下几种模式:

  • 消息体包含领域事件发生后领域模型的最新状态和变更内容

  • 消息体包含领域事件发生后领域模型的最新状态

  • 消息体包含领域事件发生后领域模型的变更内容

  • 消息体只包含领域事件发生后领域模型的标识,需要消费者按需通过API来获取相关信息

最后

以上是对于分布式系统中关于服务解耦的一些思考,希望上下游的思维能够在做设计和系统开发时给大家提供对照参考,帮助我们实现松耦合的目标,同时也有助于减小团队之间的依赖和摩擦。