关于微服务架构中的数据一致性
我是日立制作所研究开发部门数据管理研究部的大越。在我们的研究部门中,我们致力于推动以数据管理技术为核心的研究开发工作。本文将着重从数据管理的角度,详细探讨近年备受关注的微服务领域中,服务间数据一致性所面临的问题。
在微服务中的数据一致性和Saga模式。
近年,一种被称为微服务架构(MSA)的设计方法或系统架构引起了广泛关注。在MSA中,通过构建独立部署的小型服务(微服务)的集合来实现可扩展性和敏捷性的提升。然而,由于MSA通常采用每个服务单独拥有数据库的架构(每个服务一个数据库),因此在跨多个微服务进行处理时,需要考虑服务之间数据的一致性。
作为一种在服务之间保持数据一致性的设计模式,Saga模式被广泛知晓。下图详细说明了Saga模式下的数据一致性维护。
Saga模式是一种设计模式,通过连接每个服务中的事务(本地事务)构建一种特定的工作流,以维护服务之间的数据一致性。根据图中的Saga(正常情况)所示,通过在微服务内部完成本地事务,并通过消息传播(关于传播方式的变化将在后文中作为Saga的调整方法进行描述),按顺序执行本地事务,从而维护服务之间的数据一致性。如果出现某种异常情况(通常是服务故障等),导致无法继续执行,则通过在已执行本地事务的各个服务中执行补偿事务来撤销本地事务,从而实现类似于事务处理中的回滚行为,以维护数据的一致性。
作为Saga的调整方法,存在着编排(Orchestration,左图)和协同编排(Choreography,右图)。编排是指由被称为编排器的特殊微服务充当整体的调停者,通过向每个微服务提出本地事务请求来管理Saga流程的执行。另一方面,协同编排是指各个微服务在没有调停者的情况下执行本地事务,并触发其他服务的本地事务。虽然各自都有其优缺点,但一般而言,对于复杂的Saga流程,编排更适合,而对于简单的Saga流程,协同编排更合适(参考:Saga分布式事务模式)。
2. 在Saga模式中保持数据一致性的挑战
在Saga模式中,通过使用执行的本地事务来取消事务(补偿事务),可以防止许多数据不一致。然而,仅通过补偿事务无法防止所有数据不一致。关于仅通过补偿事务无法防止的数据不一致,我想通过采用典型的Saga模式的系统示例(资金移动服务)进行更深入的挖掘。
这个图是资金转移服务的序列图。另外,Saga的调整方法是假设有编排。基本处理流程如下。
-
- 用户向资金移动服务(调度程序)发出资金移动请求。
-
- 资金移动服务(调度程序)向账户A(微服务A)发出提款请求,并进行提款处理。
-
- 资金移动服务(调度程序)向账户B(微服务B)发出存款请求,并进行存款处理。
- 用户从资金移动服务(调度程序)接收资金移动结果。
此外,还要看一下Saga模式下通过补偿事务来维护数据一致性。图示的是一个入金请求处理失败的案例,此时需要对已经完成的出金请求进行取消操作,即通过补偿事务来与出金请求的本地事务相对应。通过这样的操作,账户A和账户B都回到了资金移动请求之前的状态,从而保持了数据的一致性。
接下来,让我们将情况再复杂化一些。资金转移服务决定为了应对自身的服务故障,在必要时实施状态持久化。此时,顺序图将如下所示。
稍微细致一些,我们将在资金转移服务(协调员)每次发生状态转换时进行持久化。这样一来,即使在图中所示的从接收取款结果到发送存款请求之间发生故障并重新启动的情况下,(只要持久化的数据是安全的),就可以继续处理。
我們現在要討論的是資金轉移服務(包含持續化)的情況下可能發生的一些典型障礙模式。
2.1 第一种不一致发生案例
这是一个资金移动服务的取款请求发生故障并进行持久化的案例。协调器在进行取款请求后立即发生故障,重新启动而无法接收持久化和取款结果。最后一次持久化状态是在接收到资金移动请求后,所以从这个状态重新开始处理,但正如图所示,由于取款请求没有被持久化,导致了重复执行取款请求。
2.2. 第二个不一致发生案例
假设由于网络故障等原因,入金请求未能发送到B账户。在这种情况下,资金转移服务将等待入金结果的响应,但是由于B账户没有收到入金请求,无法进行处理。虽然可以考虑重新发送入金请求,但如果无法确定B账户的接收状态,可能会因为无法判断是否存在重复请求而难以进行发送判断。
3. 维护数据一致性所需的设计模式的必要性
即使是相对简单的系统,在考虑持久化和故障恢复时,也需要考虑到许多数据不一致的发生,并需要进行系统设计。在这里,我们将介绍防止这些问题发生的代表性设计模式。
3.1 交易信息(对于非集成情况1的解决方案)
首先,让我们来看看对于不一致性案例1的解决方案,即事务性消息。事务性消息是一组设计模式,主要用于像事务一样执行消息发送和内部状态更改。具体而言,包括事务性发件箱、事务性日志追踪、轮询发布者等设计模式(详细信息请参考Microservices.io的各个页面)。不一致性案例1的原因是提款请求和其持久化处理被分开进行,由于在持久化之前发生了故障,导致处理状态变得不确定。事务性消息就是将这种消息发送和内部状态更新类似于一个事务的设计模式。下图展示了使用这些典型系统的示例。
Transactional messaging具备以下两种设计模式:Transactional outbox模式将状态持久化和消息写入作为本地事务写入数据库,而Outbox表(专用于写入消息的表)用于获取已写入的消息并发送给其他服务或消息总线等,这是一种Transactional log tailing/Polling publisher模式。通过组合这些设计模式,可以将状态持久化和消息发送处理得像是事务一样。
作为实现,使用RDB/Debezium/Kafka的架构(用于验证基于Kafka的微服务Saga模式)是典型的选择。
3.2 通信方式+附加(处理与情况2不匹配的处方)
接下来,我们将看一下不一致性案例2的处方方案,即通信风格(Communication style)。之所以添加“+α”,是因为仅仅使用Communication style中定义的设计模式(详细信息请参考Microservices.io的各个页面),对于不一致性案例2来说是不足够的。通信风格是一系列与通信主要相关的设计模式,如消息传递(Messaging)、远程过程调用(Remote procedure invocation)、领域特定协议(Domain specific protocol)等。它们主要定义了微服务之间如何实现通信,而不是直接应对数据不一致性的设计模式。在不一致性案例2中,主要需要使用与前述设计模式相关的一组设计模式,如重试(Retry)、幂等发布者/消费者(Idempotent Publisher/Consumer)等(详细信息将在后面介绍)。
在重新审视案例2时,我们发现,在资金转移服务的处理过程中,存款请求消息在中途消失,导致无法继续进行处理。在这种情况下,需要重新发送消息,并采用Retry模式来保证重新发送的处理。然而,如果我们简单粗暴地进行Retry,考虑到B账户的状态,
(1)我已收到入金请求,并且已成功处理。
(2)我收到了入金请求,但处理失败了
(3)我还没有收到入金请求。
有可能考虑到多种状态。因此,可以考虑一种简单通用的解决方案,即在成功之前不断重试,成功后立即终止处理。然而,简单的重试会导致相同的请求处理(这里是存款请求)被发送多次,因此需要保证”即使处理请求发送多次,也能收敛到相同的状态”(即幂等性)。实现这一目标的设计模式是幂等的发布者/消费者。下面将以幂等ID为例来解释幂等的发布者/消费者的构建示例。
在此示例中,如果无法接收到发送的消息,将会使用重试模式进行再发送;在发送消息时,会附加幂等ID以应用幂等发布者模式;对于具有相同幂等ID的消息,会使用幂等消费者模式进行丢弃。具体的实现取决于应用程序,但通常可以考虑以下实现方案。
-
- Retry:試行回数をパラメータとして指定し,エラー時に規定回数の試行(Retry)を実施する。
-
- Idempotent Publisher:メッセージ送信時に冪等IDをメッセージに付与する。なお,Retry時は同じIDを再利用する。
- Idempotent Consumer:冪等IDと処理状態を保存したデータベースを用いて冪等性を実現する。具体的には,未処理の冪等IDを含むメッセージを受信した際は,当該冪等IDをデータベースに追加し,処理状態を更新した後,応答メッセージを生成しデータベースに保存するとともに応答メッセージを送信する。なお,処理済みの場合は内部処理をスキップし,過去の応答メッセージを再送する。
通过这些应用,即使在传输失败时也会进行重试,并且通过幂等发布者/消费者来确保即使进行了多次消息的发送和接收,也能收敛到相同的状态。
根据实施的情况来看,似乎在Eventuate中有一个使用消息ID实现幂等性的实例。
4. 总结
在本文中,我们介绍了用于维持微服务数据一致性的设计模式——Saga模式,以及其他与数据一致性维护相关的设计模式。当然,这些设计模式本身并不足够,还需要进一步扩展和系统化。另一方面,我们还需要深入思考Saga执行中可能遗留的数据不一致以及并行执行可能导致的数据不一致等问题。
填补
这篇文章中提到的公司名称、产品名称或专有名词都是各自公司的商号、商标或注册商标。