图解分布式理论与实践

611 阅读12分钟

image.png

The world grows to be distributed ~

正文前

本文是基于过去一年工作的总结和思考,对各种分布式方案和业务思考的一些实践,欢迎阅读,Comment指正和讨论:) 阅读本文需要注意以下问题:

  • 本文的核心是图,图承载了全部核心信息和认知框架,如果你看图不太理解可以再辅助阅读文字;
  • 分布式事务是指广义的事务,包括柔性事务,而不是指狭义的基于DB的事务;
  • 方案对比对一些细节不严格计较,注重核心思路和关键设计,比如2pc和事务消息的思路完全不同,但2pc和3pc的细节不展开;
  • 本文的前置知识最好是对分布式事务相关的概念有简单的了解;

认知框架

image.png

关于事务的概念很多,有区别有联系,目前我自己的构建的认知框架分为三层:指导思想层(Idea),具体的实现层(Protocol),一些核心的设计元素(Element);

Basic Idea

BASE,ACID与SAGA

image.png

在BASE明确提出之前,分布式事务在向数据库内部事务的ACID标准对标,但是其实SAGA(1987年提出 )已经开始向最终一致和事件驱动的方向演进了。BASE 2008年提出,在这个方向之上提出了更为明确的框架和抽象,最终BASE成为一种理论模型和ACID并列,而SAGA更多是是下一层的实现模型。 悲观和乐观的假设

ACID和BASE的整体假设的态度不同:BASE整体是乐观的,认为组成分布式事务的每一个local-unit最终一定会成功;ACID整体是悲观的,认为在同步执行时确认保证成功才可能成功。理解这个核心的假设其实很重要:

  • check假设是否合意:因为在使用不同思路的时候要思考假设在应用场景是否合适,当你的场景中local-unit非常容易失败,使用BASE是非常麻烦的;
  • 理解假设后的mechanics:理解了这个思路,就比较容易理解ACID思路的模型和BASE思路的模型在执行层面为什么要这么设计了;

BASE方案

image.png ACID尽人皆知,这里来讨论一下BASE做了什么。 BASE的方案里一个核心设定是放松对达成一致性的达成时刻的要求,采用类似准实时的Eventually consistent;并把在一致性达成之前的数据状态定义成是一种Soft state,soft state是一种暂时性的状态,分布式事务完成之后数据会进入consistent/integrity的状态。 当争取到了这片刻之间,就有很多操作空间来达成一致性了,比如说中间用queue链接。Queue的出现就让一致性的保障和业务的请求时数据流解耦,增加了控制的空间。当然如果不用queue,进行data scan是另虽然简单粗暴但是却很容易控制的方式。 这片刻之间的操作无论是用queue的方式处理,还是用data scan的处理,都需要一个标示soft state的信号(marker):queue中随locally trx发出的信号是一种方式,事务消息在走这个方向的思路;db里提前记录一个init状态来识别,也是一种方式。 除此之外,类似单机数据库引擎也需要考虑的:分布式事务Id(TrxId),多个消息的并行处理(秉并行处理器),幂等也都需要实现才能保证BASE模型的可靠。

Mode and Protocol

第二个层级总结为具体的模式和协议,这些协议详细定义了如何具体的实现一个分布式事务。这里还是按大方向分成两组介绍,第一组的核心思路是2Phase:包括XA和TCC。

2Phase:XA 和 TCC

image.png

2Phase中第一阶段保证了操作的可执行,第二阶段对可执行的操作做标识或者对不可提交的操作回滚。第一阶段其实不仅仅是一个简单的check阶段,可以理解成类似预执行的阶段,这一阶段的成功往往代表这个本地任务可以执行。比如这一步是扣库存,这一步往往是直接预先锁定库存,注意这里是落盘的满足Duration,而不是简单的check数据。基于这个设定,第一阶段各个环节如果都成功了,那么即使all重启,也可以认为这个事务是成功的,这也是这个算法进行恢复的一个思路。

XA和TCC的核心要义都是两阶段思路,XA注重在DB层实现,TCC主要在微服务的接口层实现;两种方式的本质思路相同,但是根据其实现导致的使用场景不同。从业务开发角度讲TCC的用法会非常多,因为微服务调用往往会自己管理DB,更倾向于通过提供rpc接口实现调用。

image.png

上图是一个常见的TCC实现分布式事务的业务系统架构方案:这个方案的好处是绿色的独立业务模块只要focus自己所在业务单元的try-confirm-cancel接口如何实现就可以了。经过调研,XX金融服务和我们公司的财经的转账提现等实现均有采用这个方案。 这种方案的问题先不去计较tcc三个接口的开发繁琐的问题,如果No-2是个热点数据,如果No-2并不需要实时成功且不想被No-2当下是否稳定影响。那么业务往往可以有不同的选择。 顺便讲一下Seata的AT方案,AT方案试图通过记录本地事务的sql语句并自己生成undo-lock,在数据库上层来做二次开发进行rollback,并通过global lock和local lock的方式防止一个事务中两阶段的混乱和被其他更改干扰(这个是非常值得参考和借鉴的)。这个方案的好处是不block-db,通过框架的实现对业务的影响小,但是对于并发情况下的更新,cascading rollback的处理会比较复杂,暂时没有深入研究。

One By One:SAGA,事务消息

对应2Phase,把基于Base的思路命名为One By One(ObO,我自己命名的)强调是一个个执行本地的事务。这里忽略rollback,私以为这种串联模式不再适合大批量需要rollback的场景,最好是在逻辑上一定能保证成功。 这里首先列举了两种模式,两种模式的本质在于异构的基础组建如何实现事务,也就是如何确保本地事务和消息一起成功。第一种方式依赖DB本身发出的变更通知,依赖binlog;第二种方式是上层的基建架构设计,依赖了分布式事务的协议half-message,commit/cancel,recheck。这两种方式是不是很熟悉,类似XA和TCC的区别。 当没有支持事务消息的中间件可以使用的时候,一个折中的方法是记录一个marker在数据库里自己实现二次check的方案。

基于DB的方案:

image.png

基于事务消息的方案:

image.png

Quick Review

image.png

写到这里我们可以快速review一下两种思路,第一种思路是类似单机的模式每个子任务节点一次性一个个准备好,然后进行commit或者rollback;另一种思路是子任务一个个来,但要保证子任务之间的通知是可靠的,然后最终完成所有子任务。这两种思路的一个关键点,是判定事务成功的逻辑,并基于这种认定的逻辑进行异常处理。两种思路的选择逻辑有两条:1)检查是否符合乐观悲观假设;2)检查是否对实时性要求很高。

这两种思路有各种不同的实现形成了各种mode和实践方案,TCC和事务消息是常见的模式。不同的实践常常用到的关键设计要点有trx-id,lock,幂等等。这就形成了我们本章第一部分的一张图的认知框架。

业务实践

活动中奖

需求分析

活动的需求场景是:根据用户行为判断是否中奖,如果中奖就记录发奖。根据需求我们来拆解一下技术分析到关键设计如下:

  • 多:行为导致的发奖不能重复发放:【must-have】否则会引起资损;
  • 少:也不能少发放:【must-have】否则客诉;
  • 用户感知和实时性角度:
    • 用户实时感知:感知中奖结果是实时要求非常高的,对用户有露出;
    • 发放用户体验最好是prefer的场景是用户能实时收到奖品,但可以接受的稍微延迟的方案;

方案设计

image.png

基于上面分析,不重复发放部分:发奖行为用户的请求业务id做了幂等,计算得奖的unit进行了lock。在一致性方案上,这个场景非常适合用one-by-one的设计: 1)发奖流程是一定会成功的,这个场景下并没有资源限制的问题,这就满足了乐观的假设; 2)用户对延迟情况有一定的接受程度;

那么我们看one-by-one的pattern在这个场景下关键元素具体对应了什么: 1)什么时候标识这个事务认为可以完成:第一个local-trx-1成功完成的时候。这样满足的用户对得奖结果的认知,并且持久化了这次数据的结果; 2)全局的事务id和记录是什么:事务id其实就是db-trx-1的记录id,同时这个记录的状态标示了事务是不是有完成最终一致性(从to-reward变更成reward-sent),所以db-trx-1一定要确认事务的时候就进行持久化; 3)如何传递信息:信息的记录和标记to-reward这个soft state的落库有了个持久的标志,这个状态并不向外暴露是个内部的一致性保障状态;这个标示方法非常好用,这样在不用事务消息的时候也有一个识别的状态可以扫描方便重试;这里扩展讲一下事务消息的在业务中的接入有一些成本而且公司暂时没有支持,所以在需要扫描的是异常数据(数量级别不大),这种pattern在某种程度上更通用; 4)在用户中奖的时候会实时调用发奖如果调用失败会通过重新扫描这个状态的记录来重试;也因此下游的实现都根据db-trx-id的来记录幂等; 这个方案其实也做了灵活变动,会在用户请求时尝试调用trx-2,毕竟大部分请求都是可以实时成功的。但是trx-2的失败并不会反馈给用户,这样对用户的个感知和系统的一致做了平衡。

交易下单

需求分析

下单购买是一个非常常见的考题:下单时需要扣库存,扣营销资源等。私以为这个方案在下单当时是不适合用one-by-one的BASE模型的: 1)首先,Optimist的假设就不能满足。因为库存和营销资源是稀有品没法保障一定会扣除成功,所以用最终一致性模型是非常容易发生需要rollback的情况的,尤其是营销资源优惠券等非常容易被薅羊毛。 2)其次用户对下单的感知要求是非常实时的,用户和app的交互场景不太可能要求用户等在这里;相反发货的场景就有一些最终一致性的操作空间,即使是30分钟快速到达的实时配送也是如此;

方案设计

image.png

其实看了前面TCC的模型,应该对这个方案已经非常熟悉了,这个系统完全可以用TCC的模型复用(参考第一部分TCC图)。但在没有分布式框架的情况下,严格按TCC的框架一步步来比较冗余;业务往往会轻量级实现TCC的核心思想,上图就提供了一个实现方案。

这个方案的核心是合并了全局事务记录和订单,通过订单的内部状态来实现全局事务的记录和处理: 1)这样订单id作为全局事务的trx-Id来实现幂等和关联记录 2)同时通过订单的内部状态来记录事务,内部状态有prepare和innerclosed两种对用户不可见,prepare是内部的soft-status是一种临时状态,下单接口正常返回前应该根据phase-1的结果把这个状态改成inner-closed或者wait-to-pay标示完成phase-2的commit/rollback; 3)当程序出现问题,恢复线程也会根据这个prepared的暂时性状态发现问题,进行恢复; 当然如果有非常方便的TCC微服务框架直接做了集成,这个场景也可以直接使用TCC的方案进行实现。

实践选型的原则

上面两个例子一个是适合使用BASE的思路,另一个适合使用2Phase的思路,而且两个方案都根据业务情况做了灵活的变通和简化。所以框架其实是输出思想,工程师可以灵活变通和应用的。判断的标准和原则其实阅读前文已经比较清楚了,我们在此继续明确一下,一些条件,可以思考这个条件满足不满足分别应该用什么样的思路:

  • 是不是很大概率成功
  • 哪些环节用户可以感知
  • 哪一个环节有热点
  • 哪一个环节有允许异步慢慢执行
  • 哪一个环节容易出错且耗时高
  • 是不是有mixed的情况