消灭微服务的坏味道 之 循环依赖
生命以负熵为生。——薛定谔《生命是什么》
历史上有很多科学家为之背书的熵增定律,揭示了很多自然界现象的本质:任何孤立系统,在没有外力作用的情况下,其总混乱度(熵)会不断增大。
软件系统当然也不例外,随着软件系统的功能不断增加,系统的混乱度也在不断增大。为了降低软件系统混乱的速度,必须要对其施以外力(重构)。
重构系统和重构代码一样,首先要先识别系统的坏味道,Davide Taibi和Valentina Lenarduzzi在文章《On the Definition of Microservice Bad Smells》中定义了微服务的坏味道,今天我们就来聊聊如何消灭其中的一个坏味道——循环依赖。
循环依赖的危害
微服务之间的循环依赖类似于类之间的循环依赖,当依赖关系形成了环,会有很多危害:
首先是微服务之间的耦合性非常强,服务很难做到独立部署,这严重违反了微服务的初衷;这种情况往往是服务之间的调用没有约束导致的,为了方便取到或更新数据,服务之间可以随意的调用,以”微服务“为设计目标的系统会逐渐演变成一个分布式大单体,下图举例展示了三个服务之间的循环依赖关系:
另外循环依赖很可能导致一些循环调用或并发问题,造成一些复杂难以定位的问题,以上图中订单和客户的循环依赖为例:
订单服务依赖客户服务来获取客户信息,为了满足可以根据客户姓名查询订单的需求,除了记录客户的ID,同时将客户的姓名冗余到订单的数据中。当客户信息更新时,为保证订单上数据的准确,客户服务会调用订单服务更新订单上的客户信息。
循环依赖让事情变得复杂,不同的实现会有不同的问题表现,我们来看这样的场景:当订单状态变化时,客户的状态也要跟着变化,订单服务会更新客户的状态,而客户信息的变化反过来需要更新订单上冗余的客户信息;假设在订单上加了乐观锁,同一时刻的两次订单更新因为乐观锁会有一个失败,排查问题过程中通过现象会优先考虑并发的问题,然而真相却恰恰相反,参见下图时序:
循环依赖产生的原因
数据过度冗余
数据冗余是导致服务之间循环依赖的主要原因,为了保证多个服务之间数据的一致性,某个服务对数据的修改要同步到其他服务,保证其他服务冗余字段的信息准确。
上面订单上冗余客户姓名的场景就是个很好的例子,冗余数据在一定程度上增强了业务实现的简便性,但为了保证数据一致性,也增加了服务间不必要的调用。
缺失业务概念
和数据过度冗余相对的是在软件设计时缺少必要的业务概念,也同样会导致循环依赖的情况发生;典型的场景是上游系统的数据状态需要下游系统的数据进行计算得到,当需要对上游系统的数据状态时,上游服务会调用下游服务的接口来进行计算。
这种场景也很常见,继续用订单和客户的例子帮助理解:
如果一个客户的所有订单要么取消,要么已经完成开票结算流程,那么这个客户就是一个成功客户,否则是一个服务中客户
在软件设计时如果没有给客户定义一个状态来表达这个业务概念,那么每次需要拿到客户状态时只能通过订单服务来进行查询计算。
滥用同步调用
上面两种场景一旦产生循环依赖,往往也伴随着同步调用的滥用,然而也有一些场景既没有数据过度冗余,也没有缺少业务概念,但仍然可能会滥用同步调用。
上文图中用户离职更新客户所有者信息的场景就是一个很好的例子,虽然使用同步调用的方式从实现结果上来看是非常直接有效的方法,但长远来看,这种无脑的滥用同步调用让系统的复杂度变的非常高,当我们希望替换或重构某个服务,我们很难依据业务分析清楚到底有哪些服务会受到影响。
消灭循环依赖的方法
通过上面对循环依赖原因的分析(这里我们不考虑微服务划分不合理的场景),可以看出循环依赖通常是为了满足某种业务需求,在服务之间增加了不合理的调用。
要解决循环依赖,必须要在微服务之间建立一些原则来约束微服务之间的通信,定期通过这些原则来审视我们的系统,找到问题并进行重构,这些原则应该包括:
- 定义服务上下游关系,下游服务可以直接依赖上游服务,反之则不可
- 上游服务的变更对下游服务产生影响需要通过领域事件(异步)的方式来实现
- 服务之间要通过数据Id(或类Id,能够唯一代表数据且不变的属性)来进行关联,尽量不做过多的数据冗余
- 一旦需要上游服务调用下游服务才能完成业务时,要考虑是否上游服务缺少业务概念
- 为满足前端逻辑而导致的服务间交互逻辑要放到BFF(Backend for frontend)中,而不是增加服务间的调用
小结
微服务架构提倡把服务拆的小而独立,一个软件系统往往会有很多服务,大部分业务需求是需要多个服务一起配合才能完成;受限于交付压力,在对架构的理解不是很透彻的情况下,开发人员往往倾向于采用更容易的(熵增)方式实现功能,在这种情况下,循环依赖是一个非常容易发生的坏味道,对系统的健康具有巨大危害。
我们可以通过一些技术手段来发现系统中的循环依赖问题,比如通过链路追踪系统(如Zipkin)将服务间依赖关系可视化出来;也可以在发现问题时将有问题的流程时序图画出来,可视化的方式可以很容易帮我们找到系统中循环依赖的问题,然后我们就可以对症下药,消灭坏味道。