在单体应用向微服务架构转型的过程中,本地事务已不再满足系统一致性需求,为了解决这一问题,前人在对性能和数据一致性反复权衡的过程中总结了许多典型的协议和算法,各有优劣。本文我们将深入探讨 Freewheel 如何实现无单点故障的可扩展分布式事务实现模型。当应用程序有严格的数据一致性要求时,ACID 事务是必须的,如果一个事务涉及的所有操作能够放在一个服务内部,且共用一个数据库,那么只用在一个方法里同一个事务下操作数据库即可。然而为了提升系统整体的可靠性,方便各个模块独立演化,系统从单体应用演进为微服务架构。随着数据体量的增长,数据源也从 MySQL 扩展到关系型数据库 Amazon Aurora 和 NoSQL 数据库 (Amazon DynamoDB),基于多样化索引和查询数据的需求,引入了搜素引擎 (ApacheSolr 和 ElasticSearch ) ,多服务交互、多数据源并存产生了分布式事务。
Freewheel 分布式事务应用场景有三个:
综合考虑 Freewheel 的业务需求后,我们实现了多引擎数据库分布式事务。
Freewheel 分布式事务方案主要设计目标如下:
数据强一致性: 确保该事务范围内的所有操作都可以全部成功或者全部失败,事务具有原子性、一致性、隔离性、持久性 4 个特性。
系统高可用: 遵循“design for failure”的设计原则,硬件层面,采用服务节点多 region 多 AZ 部署和节点故障快速自恢复的策略来保证系统的高可用。软件层面,设计 failover 机制应对服务异常。
可扩展: 应用 Auto Scaling 服务,它会基于设定的负载压力,自动进行扩展和缩容,来保证服务正常运行。
易用性: 分布式事务应用 API:易学,易懂,易记,系统设计上无业务侵入,没有额外的编码或测试工作。
常见分布式事务解决方案对照表:
结合 Freewheel 强一致性业务需求,多数据源分布式事务将由 XA、2PC 和 Seata 这些解决方案组合而成。
Seata 框架设计思想
基于 XA 的 Aurora 分支事务
基于 2PC 的 DynamoDB 分支事务
Freewheel 分布式事务依托在 Freewheel 数据访问层中间件 (DAL) 上,这个中间件是由 Freewheel 平台团队自主研发的,它的目标是为上游应用提供更好的数据访问,为下游数据源提供更好的保护。为了方便描述,下文均用 DAL 来作为 Freewheel 数据访问层中间件的简称。
分布式事务由这三个组件来协商处理:
Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
Resource Manager (RM): 控制分支事务,负责分支注册、并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。
为了预防死锁,并且减少 DAL RM 和 TC 之间的 交互,降低对 TC 的依赖,同一个分布式事务操作放在同一个 DAL 节点,由此,DAL RM 可以方便的在单节点控制和协调分支事务,完成全局事务的提交和回滚。
以多服务,不同数据源 (Aurora 与 DynamoDB) 为例,描述 Freewheel 分布式事务过程。
- A Service TM 向 TC 申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID。
- TM 向 TC 发起针对 XID 的全局提交或回滚决议。
DAL RM 对 XID 下管辖的全部分支事务完成提交或回滚请求。
基于业务需求,DAL 分布式事务支持的数据源为 MySQL 和 AWS DynamoDB,下面章节阐述了这两个数据源 ACID 技术实现。
分布式事务设计中新建了事务控制表、事务记录表、索引表及业务镜像表:
采用 MySQL XA 2PC 来保证 ACID,原因如下:
这个时序图描述了 RM 对 MySQL 事务的工作流程:
一个事务操作,由同一个 DAL RM 处理,相同 DB 下业务事务处理,放在一个 XA 操作里:
SQL CRUD 语句应该使用触发行锁的索引操作,否则会触发表锁,影响系统吞吐量。
DynamoDB 提供了本地事务接口 TransactGetItems 和 TransactWriteItems, 它等效于 MySQL 批量操作,对于相互间有上下文或者依赖的操作并不可用,这限制了它在应用中的使用场景,详细信息请参考 TransactGetItems 和 TransactWriteItems。
DynamoDB 本身没有分布式事务机制,DAL 结合 DynamoDB 功能属性,对提供的插入、更新、删除和查询接口,设计 2PC 机制 来满足 DynamoDB 的 事务属性。
下表显示了分布式事务操作 (DisTxDAL) 和其他操作之间的隔离级别。
更新接口实现方法
一阶段
二阶段
插入接口实现方法
一阶段
应用本地事务原子地备份事务记录及备份索引
插入带有事务属性信息的业务记录
二阶段
删除接口实现方法
一阶段
应用本地事务原子地备份事物记录及备份索引
更新带有事务属性信息的业务记录, 标注为删除操作
二阶段
查询接口实现方法
事务进行中的数据含有事务属性信息,xid 表示事务全局事务 ID, operation 表示事务操作接口 create、update、delete,这里 item 表示业务数据元素。
基于业务数据变更表及写接口实现方法,实现了在读提交与读未提及查询方法:
- 判断记录是否含有事务属性,如果无,返回记录,否则到步骤 2
- 判读隔离级别,如果读提交,步骤 3,如果读未提交,步骤 5
- 记录事务操作是 create, 返回空,否则如果是 delete,去除事物属性信息,然后返回,否则步骤 4
- 本地事务原子地读取镜像表与业务表,如果镜像表值存在,返回,否则返回业务表值,都不存在返回空
如果记录事务操作是 delete,返回空,否则返回记录
为了方便用户使用,分布式事务 API 里封装了与事务协调器及 DAL 资源管理器的交互过程,交互过程对应用是透明的,下面是分布式事务 API:
type DistributedTransApi interface {
DisTxDAL(ctx context.Context, fn TranFunc) error
}
type TranFunc func(ctx context.Context) error
微服务之间,微服务与数据库访问层之间采用 google rpc 调用,服务之间关键数据都是基于 context metadata,如果经过两层服务交互, 就会导致 context metadata 丢失。举例来说,A 服务调用 B 服务,B 服务调用 C 服务,那么 C 服务就会缺失 A 服务 context metadata,针对这种情况,DAL 提供了通用函数用于提取 DAL 相关的元数据,供应用方按需添加。
func ExtractDalMetadata(ctx context.Context) (metadata.MD, error)
协调器主要功能点:
全局分配唯一事务 ID,供 DAL TM 获取,此数据需要在同一事务的业务服务间传输共享。
维护全局事务的运行状态,负责协调和驱动全局事务的提交或回滚。
DAL TC 是 HA 多节点实例,引入 ETCD leader 选举机制来保证只有一个 TC 实例承担 failover 功能,详细信息在系统高可用章节软件层面 failover。
为了预防硬件故障对高可用的影响,DAL,TC 和 ETCD 服务均是多 Region 多 AZ 部署,并且基于服务的特性配置了相应的服务策略:
ETCD 采用自恢复策略
-
DAL 和 TC 服务采用了弹性伸缩策略
美东美西均部署相应服务作为服务灾备策略,下图是美东地区的解释图(美西类似)。
在 DAL 应用或者业务应用遇到异常退出时,软件层面 Failover 机制是为了能不发生死锁,并且继续处理未完成分布式事务,实现方法如下:
Freewheel 强一致性分布式事务未来会支撑更多的数据源,如 Redis、Solr 和 ElasticSearch 等,目前的数据库访问层 API 是基于 gRPC,这对数据库访问层使用方带来了一定技术语言限制,未来会探究 GraphQL 在数据库访问层分布式事务应用的可行性。
作者简介:
李长城,Lead Software Engineer,目前就职于 Comcast FreeWheel 架构平台团队。研究方向为微服务架构、数据库中间件、云计算等领域。
你也「在看」吗?👇