微服务的灾难-最终一致

现在的架构师总喜欢把最终一致挂在嘴上,好像最终一致是解决分布式场景下数据一致问题的金科玉律。事实上又怎么样呢?

事实上的这些人嘴里的最终一致,往往都是最终不一致。在多个系统之间进行数据传递时,无非通过 RPC 或者异步消息。RPC 能保证一致性么?当 B 系统需要 A 系统提供数据时,想要达到一致的效果,那么在 A call B 时发生失败,那么必须让 A 中的逻辑终止。这样才能够使 B 中的状态或数据与 A 中的完全一致。这样实际上需要让 A 和 B 成为生死共同体,B 挂了,那 A 也得挂。可能么?在中大型规模的互联网公司的业务系统中,其下游系统往往有几十个,因此实际的场景是 A call B -> A call C -> A call D .... -> A call Z。这种情况下,你想让所有系统中的状态都一致,那是不可能的。

有的架构师又拿出 saga pattern 来说事,我如果有写数据的逻辑,那么我自然会有一套回滚逻辑,只要在中间发生错误,那么我就对之前的所有调用执行回滚逻辑即可。然而回滚是需要开发量的。我所有下游系统那都得支持回滚才行啊,你觉得做得到么? saga pattern 的异常处理就更扯蛋了:回滚过程中发生失败的话,那需要人工介入,人肉处理。显然人肉是处理不过来的,机房网络抖动实在太正常了,可能一天两天的就会有一次,每次抖动都造成 bad case,研发人员不用干别的事情了,都去处理 bad case 好了。

当然上面这种情况比较极端,一般公司内有靠谱的 MQ 方案的话,会选用 MQ 对这种数据同步的场景进行解耦。之前我做的一些总结也都提到过,只要往 MQ 发一条消息,在字段上尽量满足下游系统,那么我就不用挨个儿去调用他们了,可以很好地进行解耦。从设计的角度上来讲,这确实是比较好的解耦形式。但是你要考虑,逻辑执行和消息发送这两步操作并不具备原子性,除非 MQ 支持事务消息,我才能完成两个操作同时成功或者失败,何况逻辑执行内部可能还有更多的子操作,这件事情远没有打打嘴炮那么简单。

也有的公司会将发送失败的消息进行落盘,比如落进 MySQL 或者写入到磁盘,在发送失败之后,由后台线程在合适的时间进行重发,以让消息能够最终发出。一些简单的场景,这样确实算是解决了问题。如果下游对于消息本身有顺序要求呢?比如订单的状态流转,如果顺序错了,那状态机最终的状态都错乱了。又是一个麻烦的问题。

在当前的开发环境下,想要达到最终一致的效果需要上下游同时进行很多工作,例如上面说的异步消息的场景,上游至少需要做失败落盘和后台发送。而下游需要在状态机的正常状态流转之外,处理各种麻烦的乱序问题。这种乱序处理基本和业务是强相关的,并没有通用方案。即使是同一套状态机,针对不同的业务场景可能还需要定制不相同的业务逻辑。

除了网络抖动,数据不一致的问题可能还会因为模块上线导致。有些公司(比如我司)为了简单 MQ 的消费逻辑,提供了一套由 MQ 平台消费,然后通过 http post 来将消息发送给业务系统的逻辑,降低了业务系统的消息消费开发成本(这样就不用使用 MQ 的 client)了。这种情况下如果模块发生上线的话,即使在 MQ 平台侧有 post 重试,但在模块上线时,还是有概率发生消息丢失。如果有一些状态机流转强依赖于这些消息,那也会造成一部分 bad case。而且这种 bad case 查起来真是没什么意思。之后的数据修复也基本只能靠研发人员自行修复。

这种恶劣的场景下,也有一些人想到了一种方法,我在业务模块中插入多个桩,只要可以每过一段时间触发状态的全量更新,那么我就找一个其它模块来持续地刷新我系统中的数据状态。从而达到“最终一致”。只要这些最终一致的数据没有暴露给用户,没人看得见,那就是最终一致。倒确实是个可用的方案。但架构师们在吹牛逼的时候,对于这种恶心的逻辑一定是绝口不提的。

大多数公司的架构师嘴里的最终一致,依靠的都是人肉而非技术。