【51CTO.com快译】在微服务架构中,单个服务内的事务通常使用ACID事务来提供数据一致性。
介绍
本文描述了在微服务系统中管理分布式和长时间运行的事务的架构和概念框架。作者发表此文旨在与开发社区分享经验,表达对事件驱动架构的热情,并促进对复杂事件处理分布式系统的讨论兴趣。
概述
微服务在其真正的场景中是一个分布式系统。一个事务被分发到多个服务,这些服务被顺序或并行调用以完成整个事务。在微服务架构中,单个服务中的事务使用ACID事务来提供数据一致性。然而,面临挑战在于处理跨多个服务的事务,在某些情况下需要很长时间才能完成。在这种情况下,应用程序必须使用复杂的机制来管理事务。而ACID是指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务是正确可靠的,所必须具备的四个特性:原子性、一致性、隔离性、持久性。
设想
考虑一个使用微服务架构实现的航空公司航班座位预订简单的场景。在这个场景中,一个微服务来锁定预订座位,另一个微服务接受付款,还有一个微服务在付款之后解锁并分配席位,每个微服务都实现一个本地事务。旅客要成功完成航班预订流程,必须完成所有三个步骤。如果任何一个步骤失败,则必须回滚之前完成的所有步骤。由于整体事务的边界跨越多个服务和数据库,因此被认为是一个分布式事务。
考虑通过微服务方法实现另一个订单实现场景。工作流事务从订单服务开始,首先创建订单,随后采用另一个服务进行付款,接下来为交易创建发票,然后进行发货,最后交付订单并完成工作流,并循环执行每个本地事务。这里的订单处理本质上是分布式的,完成工作流程可能需要几天到几周的时间。这样的事务可以称为长时间运行的事务,因为不能使用传统的ACID事务语义一次性执行所有步骤。
挑战
随着微服务架构的出现,分布式事务管理存在两个关键问题:
- 原子性:原子性意味着事务中的所有步骤都必须获得成功,或者如果一个步骤失败,则应该回滚之前完成的所有步骤。但是在微服务架构中,一个事务可以由不同微服务处理的多个本地事务组成。因此,如果其中一个本地事务失败,那么如何回滚之前成功完成的事务?
- 隔离性:事务隔离指定对事务中的语句可见的数据量,特别是当多个服务调用同时访问同一数据源时。如果来自任何一个微服务的对象持久地保存在数据库中,而另一个请求同时读取同一个对象,那么该服务应该提交旧数据还是新数据?
为了解决这些问题并提供有效的事务管理能力,可以采取两种方法:一是两阶段提交(2PC) ;二是Saga。
(1)两阶段提交(2PC)
保持跨多个服务的数据一致性的传统方法是使用分布式事务,其事实标准是两阶段提交(2PC)。两阶段提交(2PC) 确保事务中的所有参与者或者提交或者回滚。它分为两个阶段工作;阶段1称为准备阶段,控制节点询问所有参与节点是否准备好提交;阶段2称为提交阶段,如果所有节点都回答是肯定的,则控制节点要求它们提交,否则回滚。
尽管两阶段提交(2PC)可以帮助在分布式系统中提供事务管理,但它也会成为单点故障,因为事务的责任落在了协调器上,并且这种协调器的典型实现本质上是同步的,这会导致减少未来的吞吐量。因此,两阶段提交(2PC)还存在以下不足:
- MongoDB和Cassandra等现代NoSQL数据库不提供支持。
- Apache Kafka等现代消息代理不提供支持。
- 同步IPC降低了可用性。
- 所有参与者都必须在场。
(2)Saga
为了解决在微服务架构中维护数据一致性这一更复杂的问题,应用程序必须使用基于松耦合异步服务概念的不同机制。这就是Saga发挥重要作用的地方。
Saga是一种架构模式,它提供了一种优雅的方法来实现跨多个服务的事务,在本质上是异步和反应式的。因此,Saga可以定义为事件驱动的本地事务序列,其中每个本地事务更新数据库,并发布命令或事件以触发Saga中的下一个本地事务。如果本地事务因为违反业务规则而失败,那么Saga将执行一系列补偿事务,这些补偿事务将撤消先前本地事务所做的更改。
Saga实现确保执行所有事务或撤消所有更改,从而提供原子性保证。将Saga设计为状态机模型将提供处理隔离的对策。
Saga模式如何提供帮助
使用微服务架构,单个业务流程将多个微服务聚合在一起以提供整体解决方案。使用微服务架构实现ACID(原子性、一致性、隔离性、持久性)事务非常困难,并且在某些情况下是不可能的。
例如,在以上提到的航班座位预订场景中,具有预订座位功能的微服务无法实现支付数据库的锁定,因为它在大多数情况下可能是外部服务。但是仍然需要某种形式的事务管理,这种事务被称为BASE事务:基本可用、软状态和最终一致。
必须采取补偿措施来恢复作为事务一部分发生的任何事情。以下是Saga如何为航班预订座位场景图:
补偿事务
当Saga的一个步骤由于违反业务规则而失败时,Saga必须通过执行补偿事务撤消先前步骤所做的更新。假设Saga的第(n+1)个交易失败,则必须撤销之前n个事务的影响。
从概念上来说,每个步骤Ti都有一个相应的补偿事务Ci,它可以消除Ti的影响。为了消除前n个步骤的影响,Saga必须以相反的顺序执行每个Ci。如图所示,其步骤顺序为T1…Tn、Cn…C1。
在这一示例中,Tn+1步骤失败,这需要撤消T1…Tn步骤。Saga以与远期事务相反的顺序执行补偿事务:Cn…C1。Cis测序的机制与Tis测序没有任何区别。Ci的完成必须触发Ci-1的执行。
Pivot事务和Retryable事务
下表显示了Saga在航班座位预订中每个步骤的补偿事务,其三个步骤被称为补偿事务,因为它们之后是可能失败的步骤。在此需要注意的是,并非所有步骤都需要补偿事务。
Saga模式中还有另外两种事务类型;一个是Pivot事务,就像Saga中的一个成功/失败点。如果Pivot事务提交,则Saga将一直运行到结束。另一个是Retryable事务,跟随Pivot事务并保证事务成功。
Saga保证
Saga保证以下两种结果之一: Saga中的所有请求或者都成功完成,或者执行一部分请求及其补偿请求。而请求和补偿请求都需要遵循一定的原则:
- 单个事务可以中止,并且必须是幂等的。
- 补偿事务必须是幂等的、可交换的,并且不能中止(必须无限期重试或在必要时通过人工干预解决)。
Saga协调策略
Saga执行协调器(SEC)是实现成功的Saga流程的核心组件。Saga协调可在以下方面实施:
- 编排(choreography)——在Saga参与者之间分配决策和排序。换句话说,参与者在没有集中控制点的情况下交换事件,每个本地事务都会发布触发其他服务中的本地事务的域事件。
尽管Saga编排是简单且可靠的基于事件的通信,但它只能处理简单用例,并且存在一些限制,这使其无法成为管理分布式事务的理想选择。而基于编排的Saga难以理解,经常会产生循环依赖,并且Saga参与者之间存在紧密耦合的风险。
- 协调(orchestration)——将Saga的协调逻辑集中在Saga协调器类中。Saga协调器向Saga 参与者发送命令并对事件的结果采取行动。协调器执行Saga请求,存储和解释每个任务的状态,并通过补偿事务处理故障恢复。基于协调器的Saga更适合复杂的事件处理,并使它们成为管理分布式事务的一种理想选择。
Saga协调器(Saga Orchestrator)
正如Saga“协调”模式所暗示的那样,有一个单独的协调器组件负责管理整个流程工作流。在使用编制时可以定义一个协调器类,它的唯一职责是告诉Saga参与者要做什么。Saga协调器使用命令/异步回复式交互与参与者进行通信。为了执行Saga步骤,它会向参与者发送命令消息,告诉它要执行什么操作。在Saga参与者执行操作后,它会向协调器发送回复消息。然后协调器处理这一消息并确定下一步要执行的Saga步骤。
上图显示了航班座位预订采用Saga的基于协调器的过程。Saga由Saga协调器组件编排,该组件使用异步请求/响应调用Saga参与者。Saga协调器跟踪进程并通过命令组件向Saga参与者发送命令操作,例如座位锁定服务(Seat Blocking Service)和付款服务(Payment Service),并通过事件处理器从其回复通道读取回复消息,然后确定下一个步骤,采用Saga协调器预订航班座位的步骤如下:
(1)FrontEnd UI向Saga协调器发送座位预订请求。
(2)Saga协调器启动一个新的工作流并向座位锁定服务(Seat Blocking Service)发送一个座位锁定命令(Seat Blocking Command)。
(3)座位锁定服务(Seat Blocking Service)处理命令并回复一个座位锁定事件(Seat Blocked Event)。
(4)Saga协调器触发工作流中的下一个动作,并向付款服务(Payment Service)发送付款请求命令(Payment Request Command)。
(5)付款服务(Payment Service)回复付款成功事件(Payment Success Event)。
(6)Saga协调器然后向座位分配服务(Seat Allocation Service)发送一个座位分配命令(Seat Allocation Command)。
(7)座位分配服务(Seat Allocation Service)回复一个座位分配事件(Seat Allocated Event)。
(8)Saga协调器结束事务并完成工作流。
但是,如果座位锁定服务、付款服务或座位分配服务中任何一个步骤失败,Saga航班预订的场景可能会失败。为了有效地管理工作流并处理故障,建议将Saga建模为状态机,因为它描述了所有可能的场景,并让协调器确定需要执行的操作。
作为状态机的Saga
将Saga协调器建模为状态机是一种有效的方式,不仅可以管理分布式事务,还可以支持长时间运行的事务。状态机由一组状态和一组由事件触发的状态之间的转换组成。每个转换都可以有一个动作,对于Saga来说,它是一个Saga参与者的调用。
状态之间的转换由Saga参与者执行的本地事务的完成触发。当前状态和本地事务的特定结果决定了状态转换以及要执行的操作。因此,使用状态机模型可以更轻松地设计、实现和测试Sagas。
上图突出显示了Saga航班预订的状态机模型。该状态机由许多状态和转换组成,其中包括以下内容:
- 开放订单(Order Open)——初始状态。Saga在工作流开始时设置这一状态。
- 锁定座位(Blocking Seat)——当处于这种状态时,Saga正在等待座位锁定服务(Seat Blocking Service)来阻止预订座位。
- 授权付款(Authorizing Payment)——Saga正在等待来自付款服务(Payment Service)的支付授权命令的回复。
- 分配座位(Allocating Seat)——在支付成功后等待座位分配服务(Seat Allocation Service)分配座位。
- 反向付款(Reverse Payment)——如果座位分配失败,Saga将发送付款退款请求。
- 解锁座位(Unblock Seat)——如果支付授权失败,Saga将发送一个失败事件来解除对座位的封定。
- 订单完成(Order Completed)——表示Saga成功完成的最终状态。
- 订单被拒绝(Order Rejected)——表示订单被其中一位参与者拒绝的最终状态。
最后,Saga工作流可以重新设计为Saga状态机。Saga协调器链接到一个状态机,它负责通过状态管理器API管理事务状态。除此之外,它还负责将事务状态存储在持久数据存储设备中,以确保发生系统故障时的恢复。
因此,Saga状态机有责任或者完成所有事务,或者使系统处于已知状态,以便它可以确定可能执行下一个动作状态或补偿活动的顺序,无论发生的事务是分布的还是长期的。
好处和潜在用例
- 更简单的依赖关系——Saga协调器调用Saga参与者,但参与者不调用协调器。因此,协调器依赖于参与者,反之则不然,因此不存在循环依赖关系。
- 减少耦合——每个服务都实现了一个由协调器调用的API,因此它不需要知道Saga参与者发布的事件。
- 关注点分离——Saga协调逻辑在Saga协调器中实现本地化。域对象更简单,并且不知道它们参与的Saga。
- 数据一致性——跨多个微服务保持数据一致性,无需紧密耦合。
- 开发人员体验——设计允许开发人员只关注Saga参与者的业务逻辑,并简化Saga协调器上有状态工作流的实现。
可以执行此类实现的几个潜在用例:
(1)订单管理系统
- 电子商务
- 送餐
- 机票预订
- 酒店/出租车预订
(2)结算交易。
指南和建议
如果组织正在设计和构建编排器驱动的Saga以支持分布式和长时间运行的事务,则建议遵循以下准则:
- 协调器应该只负责管理事务和状态,此处不应添加任何业务逻辑。而业务逻辑应该在各个服务参与者中定义。
- 进出协调器的所有事件和命令都应承载事务数据,而不是引用数据。
- 使用异步样式消息在服务之间进行通信。
- 如果使用消息代理(如Kafka),则实施幂等性和状态检查以提高弹性。
- 适用于在CQRS和Event Sourcing架构中设计命令端(写入模型)。
原文标题:Modeling Saga as a State Machine,作者:Rohit Singh
【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】