<SOFA:Channel/>,有趣实用的分布式架构频道。
本文根据 SOFAChannel#10 直播分享整理,主题:分布式事务 Seata 长事务解决方案 Saga 模式详解。
回顾视频以及 PPT 查看地址见文末。欢迎加入直播互动钉钉群:23372465,不错过每场直播。
大家好,我是陈龙,花名: 屹远(long187@github),是蚂蚁金服分布式事务核心研发,也是 Seata Committer。今天分享的主题是《分布式事务 Seata 长事务解决方案 Saga 模式详解》,将从金融分布式应用开发的痛点出发,结合 Saga 分布式事务的理论和使用场景,讲解如何使用 Seata Saga 状态机来进行服务编排和分布式事务处理,构建更有弹性的金融应用,同时也会从架构、原理、设计、高可用、最佳实践等方面剖析 Saga 状态机的实现。
Seata:https://github.com/seata/seata
金融分布式应用的痛点
分布式系统有一个比较明显的问题就是,一个业务流程需要组合一组服务。这样的事情在微服务下就更为明显了,因为这需要业务上的一致性的保证。也就是说,如果一个步骤失败了,那么要么回滚到以前的服务调用,要么不断重试保证所有的步骤都成功。---《左耳听风-弹力设计之“补偿事务”》
而在金融领域微服务架构下的业务流程往往会更复杂,流程很长,比如一个互联网微贷业务流程调十几个服务很正常,再加上异常处理的流程那就更复杂了,做过金融业务开发的同学会很有体感。
所以在金融分布式应用开发过程中我们面临一些痛点:
业务一致性难以保障
我们接触到的大多数业务(比如在渠道层、产品层、集成层的系统),为了保障业务最终一致性,往往会采用“补偿”的方式来做,如果没有一个协调器来支持,开发难度是比较大的,每一步都要在 catch 里去处理前面所有的“回滚”操作,这将会形成“箭头形”的代码,可读性及维护性差。或者重试异常的操作,如果重试不成功可能要转异步重试,甚至最后转人工处理。这些都给开发人员带来极大的负担,开发效率低,且容易出错。
业务状态难以管理
业务实体很多、实体的状态也很多,往往做完一个业务活动后就将实体的状态更新到了数据库里,没有一个状态机来管理整个状态的变迁过程,不直观,容易出错,造成业务进入一个不正确的状态。
业务监控运维难
业务的执行情况监控一般通过打印日志,再基于日志监控平台查看,大多数情况是没有问题的,但是如果业务出错,这些监控缺乏当时的业务上下文,对排查问题不友好,往往需要再去数据库里查。同时日志的打印也依赖于开发,容易遗漏。
缺乏统一的差错守护能力
对于补偿事务往往需要有“差错守护触发补偿”、“人工触发补偿”操作,没有统一的差错守护和处理规范,这些都要开发者逐个开发,负担沉重。
理论基础
对于事务我们都知道 ACID,也很熟悉 CAP 理论最多只能满足其中两个,所以,为了提高性能,出现了 ACID 的一个变种 BASE。ACID 强调的是一致性(CAP 中的 C),而 BASE 强调的是可用性(CAP 中的 A)。在很多情况下,我们是无法做到强一致性的 ACID 的。特别是我们需要跨多个系统的时候,而且这些系统还不是由一个公司所提供的。BASE 的系统倾向于设计出更加有弹力的系统,在短时间内,就算是有数据不同步的风险,我们也应该允许新的交易可以发生,而后面我们在业务上将可能出现问题的事务通过补偿的方式处理掉,以保证最终的一致性。
所以我们在实际开发中会进行取舍,对于更多的金融核心以上的业务系统可以采用补偿事务,补偿事务处理方面在30多年前就提出了 Saga 理论,随着微服务的发展,近些年才逐步受到大家的关注。目前业界比较也公认 Saga 是作为长事务的解决方案。
https://github.com/aphyr/dist-sagas/blob/master/sagas.pdf[1]
http://microservices.io/patterns/data/saga.html[2]
Saga 模式用一种非常纯朴的方式来处理一致性:补偿。上图左侧是正常的事务流程,当执行到 T3 时发生了错误,则开始执行右边的事务补偿流程,返向执行T3、T2、T1 的补偿服务,其中 C3 是 T3 的补偿服务、C2 是 T2 的补偿服务、C1 是 T1 的补偿服务,将T3、T2、T1 已经修改的数据补偿掉。
使用场景
一些场景下,我们对数据有强一致性的需求时,会采用在业务层上需要使用“两阶段提交”这样的分布式事务方案。而在另外一些场景下,我们并不需要这么强的一致性,那就只需要保证最终一致性就可以了。
例如蚂蚁金服目前在金融核心系统使用的就是 TCC 模式,金融核心系统的特点是一致性要求高(业务上的隔离性)、短流程、并发高。
而在很多金融核心以上的业务(比如在渠道层、产品层、集成层的系统),这些系统的特点是最终一致即可、流程多、流程长、还可能要调用其它公司的服务(如金融网络)。这是如果每个服务都开发 Try、Confirm、Cancel 三个方法成本高。如果事务中有其它公司的服务,也无法要求其它公司的服务也遵循 TCC 这种开发模式。同时流程长,事务边界太长,加锁时间长,会影响并发性能。
所以 Saga 模式的适用场景是:
业务流程长、业务流程多;
参与者包含其它公司或遗留系统服务,无法提供 TCC 模式要求的三个接口;
典型业务系统:如金融网路(与外部机构对接)、互联网微贷、渠道整合、分布式架构下服务集成等业务系统;
银行业金融机构使用广泛;
其优势:
一阶段提交本地事务,无锁,高性能;
参与者可异步执行,高吞吐;
补偿服务易于实现,因为一个更新操作的反向操作是比较容易理解的;
其缺点:
不保证隔离性,后面我们会讲到如何应对隔离性的缺失。
基于状态机引擎的 Saga 实现
基于状态机引擎的 Saga 实现的基本原理:
通过状态图来定义服务调用的流程并生成 json 定义文件;
状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点(虚线关联的节点);
状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚;
异常发生时是否进行补偿也可由用户自定义决定;
可以实现服务编排需求,路由、异步、重试、参数转换、参数映射、服务执行状态判断、异常捕获等功能 ;
Seata 目前的 Saga 模式采用了状态机+DSL 方案来实现,原因有以下几个:
状态机+DSL 方案在实际生产中应用更广泛;
可以使用 Actor 模型或 SEDA 架构等异步处理引擎来执行,提高整体吞吐量;
通常在核心系统以上层的业务系统会伴随有“服务编排”的需求,而服务编排又有事务最终一致性要求,两者很难分割开,状态机+DSL 方案可以同时满足这两个需求;
由于 Saga 模式在理论上是不保证隔离性的,在极端情况下可能由于脏写无法完成回滚操作,比如举一个极端的例子,分布式事务内先给用户 A 充值,然后给用户 B 扣减余额,如果在给A用户充值成功,在事务提交以前,A 用户把线消费掉了,如果事务发生回滚,这时则没有办法进行补偿了,有些业务场景可以允许让业务最终成功,在回滚不了的情况下可以继续重试完成后面的流程,状态机+DSL的方案可以实现“向前”恢复上下文继续执行的能力, 让业务最终执行成功,达到最终一致性的目的。
状态定义语言(Seata State Language)
通过状态图来定义服务调用的流程并生成 JSON 状态语言定义文件;
状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点;
状态类型支持单项选择、并发、异步、子状态机、参数转换、参数映射、服务执行状态判断、异常捕获等;
JSON 定义相对于 XML(如 BPMN、BPEL 等)更加简洁易读、学习成本低;
状态机 JSON 示例:
"状态机" 属性说明:
Name:表示状态机的名称,必须唯一;
Comment:状态机的描述;
Version:状态机定义版本;
StartState:启动时运行的第一个"状态";
States:状态列表,是一个 map 结构,key 是"状态"的名称,在状态机内必须唯一;
"状态" 属性说明:
Type:"状态" 的类型,比如有:
ServiceTask:执行调用服务任务;
Choice:单条件选择路由;
CompensationTrigger:触发补偿流程;
Succeed:状态机正常结束;
Fail:状态机异常结束;
SubStateMachine:调用子状态机;
ServiceName:服务名称,通常是服务的beanId;
ServiceMethod:服务方法名称;
CompensateState:该"状态"的补偿"状态";
Input:调用服务的输入参数列表;
Output:将服务返回的参数赋值到状态机上下文中;
Status:服务执行状态映射,框架定义了三个状态,SU 成功、FA 失败、UN 未知,我们需要把服务执行的状态映射成这三个状态,帮助框架判断整个事务的一致性;
Catch:捕获到异常后的路由;
Retry:服务调用重试策略;
Nex:服务执行完成后下一个执行的"状态"。
更多详细的状态语言解释请看《Seata Saga 官网文档》[3]。
状态机设计器
Seata Saga 提供了一个可视化的状态机设计器方便用户使用,代码和运行指南请参考:
https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer[4]
状态机设计器截图:
状态机设计器演示地址:
http://seata.io/saga_designer/index.html[5]
状态机引擎原理
图中的状态图是先执行 stateA,再执行 stataB,然后执行 stateC;
"状态"的执行是基于事件驱动的模型,stataA 执行完成后,会产生路由消息放入 EventQueue,事件消费端从 EventQueue 取出消息,执行 stateB;
在整个状态机启动时会调用 Seata Server 开启分布式事务,并生产 xid, 然后记录"状态机实例"启动事件到本地数据库;
当执行到一个"状态"时会调用 Seata Server 注册分支事务,并生产 branchId, 然后记录"状态实例"开始执行事件到本地数据库;
当一个"状态"执行完成后会记录"状态实例"执行结束事件到本地数据库, 然后调用 Seata Server 上报分支事务的状态;
当整个状态机执行完成,会记录"状态机实例"执行完成事件到本地数据库, 然后调用 Seata Server 提交或回滚分布式事务。
状态机引擎设计
状态机引擎的设计主要分成三层, 上层依赖下层,从下往上分别是:
Eventing 层:
实现事件驱动架构, 可以压入事件, 并由消费端消费事件, 本层不关心事件是什么消费端执行什么,由上层实现;
ProcessController 层:
由于上层的 Eventing 驱动一个“空”流程执行的执行,"state"的行为和路由都未实现,由上层实现;
基于以上两层理论上可以自定义扩展任何"流程"引擎。这两层的设计是参考了内部金融网络平台的设计。
StateMachineEngine 层:
实现状态机引擎每种 state 的行为和路由逻辑;
提供 API、状态机语言仓库;
状态机引擎高可用
状态机引擎是无状态的,它是内嵌在应用中。
当应用正常运行时:
状态机引擎会上报状态到Seata Server;
状态机执行日志存储在业务的数据库中;
当一台应用实例宕机时:
Seata Server 会感知到,并发送事务恢复请求到还存活的应用实例;
状态机引擎收到事务恢复请求后,从数据库里装载日志,并恢复状态机上下文继续执行;
Saga 模式下服务设计的实践经验
下面是实践中总结的在 Saga 模式下微服务设计的一些经验。
Seata Saga 模式对微服务的接口参数没有任何要求,这使得 Saga 模式可用于集成遗留系统或外部机构的服务。
允许空补偿
空补偿:原服务未执行,补偿服务执行了;
出现原因:
原服务超时(丢包);
Saga 事务触发回滚;
未收到原服务请求,先收到补偿请求;
所以服务设计时需要允许空补偿,即没有找到要补偿的业务主键时返回补偿成功并将原业务主键记录下来。
防悬挂控制
悬挂:补偿服务比原服务先执行;
出现原因:
原服务超时(拥堵);
Saga 事务回滚,触发回滚;
拥堵的原服务到达;
所以要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝服务的执行。
幂等控制
原服务与补偿服务都需要保证幂等性, 由于网络可能超时,可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新。
对于一些老系统的服务可能没有实现幂等,也有“绕过”方案:可以不设置重试策略,让状态机不要重试服务调用,然后通过“反查”或者“人工订正”服务的执行状态,然后再恢复状态机继续执行。
缺乏隔离性的应对
由于 Saga 事务不保证隔离性,在极端情况下可能由于脏写无法完成回滚操作,比如举一个极端的例子,分布式事务内先给用户A充值,然后给用户B扣减余额,如果在给A用户充值成功,在事务提交以前,A用户把余额消费掉了,如果事务发生回滚,这时则没有办法进行补偿了。
这就是缺乏隔离性造成的典型的问题,实践中一般的应对方法是:
业务流程设计时遵循“宁可长款,不可短款”的原则,长款意思是客户少了钱机构多了钱,以机构信誉可以给客户退款,反之则是短款,少的钱可能追不回来了,所以在业务流程设计上一定是先扣款;
有些业务场景可以允许让业务最终成功,在回滚不了的情况下可以继续重试完成后面的流程,所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力,让业务最终执行成功,达到最终一致性的目的;
Seata Saga 优势
我们在实践中发现,长流程的业务场景,往往有服务编排的需求,同时又要保证服务之间的数据一致性。
目前开源社区也有一些 Saga 事务框架,如:Apache Camel Saga、Eventuate Tram Saga、Apache ServiceComb Saga 等等。也有一些服务编排的框架,如 uber cadence、netflix conductor、zeebe-io zeebe、ing-bank Baker、AWS Step Functions 等等。
但是它们要么只有 Saga 事务处理能力、要么只有服务编排能力,Seata Saga 是将这两者能力非常优雅的结合在一起,为用户提供一个简化研发、降低异常处理难度、高性能事件驱动的产品。
基于注解拦截器的 Saga 实现(规划中)
还有一种 Saga 的实现是基于注解+拦截器的实现,Seata 目前没有实现,可以看上面的伪代码来理解一下,one 方法上定义了 @SagaCompensable 的注解,用于定义 one 方法的补偿方法是 compensateOne 方法。然后在业务流程代码 processA 方法上定义 @SagaTransactional 注解,启动 Saga 分布式事务,通过拦截器拦截每个正向方法当出现异常的时候触发回滚操作,调用正向方法的补偿方法。
两种 Saga 实现优劣对比
状态机引擎的最大优势是可以通过事件驱动的方法异步执行提高系统吞吐,可以实现服务编排需求,在 Saga 模式缺乏隔离性的情况下,可以多一种“向前重试”的事情恢复策略,从而提高系统容错能力,缺点是业务入侵高。
注解加拦截器的的最大优势是,开发简单、学习成本低,缺点是无法做到“事后向前重试”,因为无法恢复线程上下文(事后是指:当出现异常后重试多次没有成功,然后由守护任务,如 Seata Server 继续发起重试),在缺乏隔离性的情况下,缺少一种事务处理手段,会增加一定的运维成本。
总结
很多时候我们不需要强调强一性,我们基于 BASE 和 Saga 理论去设计更有弹性的系统,在分布式架构下获得更好的性能和容错能力。分布式架构没有银弹,只有适合特定场景的方案,事实上 Seata Saga 是一个具备“服务编排”和“Saga 分布式事务”能力的产品,总结下来它的适用场景是:
适用于微服务架构下的“长事务”处理;
适用于微服务架构下的“服务编排”需求;
适用于金融核心系统以上的有大量组合服务的业务系统(比如在渠道层、产品层、集成层的系统);
适用于业务流程中需要集成遗留系统或外部机构提供的服务的场景(这些服务不可变不能对其提出改造要求);
以上就是本次分享的全部内容,如果大家对于 Seata 还想要有更多的了解,欢迎在官网浏览相关文章 ,或者在项目中查看具体代码。
文中涉及相关链接
[1] https://github.com/aphyr/dist-sagas/blob/master/sagas.pdf
[2] http://microservices.io/patterns/data/saga.html
[3]《Seata Saga 官网文档》:
http://seata.io/zh-cn/docs/user/saga.html
[4] Seata Saga可视化状态机设计器:
https://github.com/seata/seata/tree/develop/saga/seata-saga-statemachine-designer
[5] 状态机设计器演示地址:
http://seata.io/saga_designer/index.html
[6] Seata:
https://github.com/seata/seata
[7] Seata 相关文章:
https://www.sofastack.tech/tags/seata/
本期视频回顾以及 PPT 查看地址
https://tech.antfin.com/community/live/1076/data/968
本文归档在 sofastack.tech。
???? 奖励支持 SOFAStack 的你~
* 点下右下角“在看”
* 到公众号对话框发送“包”,试试手气~
* 本期互动奖品“13寸 With Ant 电脑内胆包”