数据库内核那些事|PolarDB分布式版存储引擎之事务系统(下)
导读
云原生数据库PolarDB分布式版(PolarDB for Xscale,简称“PolarDB-X”)是阿里云自主设计研发的高性能云原生分布式数据库产品。作为PolarDB for Xscale分布式数据库产品存储引擎的核心技术模块, Lizard事务系统承担了重要角色。本系列文章将分为两部分对其进行深度解读,上篇介绍Lizard SCN单机事务系统,本篇将深入介绍Lizard GCN分布式事务系统。
PolarDB分布式版存储引擎,想要实现全局分布式事务的ACID特性,和全生态一致性,必须依赖一套分布式事务系统,而MySQL社区版本的InnoDB事务系统,存在诸多的弊端,详情参考《数据库内核那些事|PolarDB分布式版存储引擎之事务系统(上)》的介绍, 接下来,我们着重介绍在SCN单机事务系统的基础上,演化而来的分布式事务系统。
1.分布式事务模型
能否完整支持事务ACID是企业级分布式数据库最核心的特性。目前,主流的分布式事务模型有:
1. Percolator模型
Percolator是Google在 2010 年提出的一种分布式事务处理模型,它的设计目标是在大规模分布式系统中实现高效的事务处理。Percolator是一种乐观的事务模型,写写冲突被延迟到事务提交时才会进行检测。可见性与冲突检测依赖于事务的开始时间戳以及提交时间戳。Percolator模型,包括后续的Omid模型,其特点是:实现原理容易理解且易于工程实现,作为一种高效且直接的分布式事务处理模型,被广泛应用于主流分布式数据库。另外,由于其事务过程数据存放于内存中,事务大小会受到内存资源的限制。
2. Calvin模型
Calvin模型由Brown等人于2012年提出。它旨在提供高性能、高可用性和强一致性的分布式事务处理。Calvin 模型的核心思想是通过全局调度器,事先确定好各个调度节点的子事务执行顺序,从根源上规避掉并发事务的锁资源、缓存资源等资源的开销。为此,Calvin 模型还引入了一种称为 "transaction flow graph"(事务流图)的数据结构,用于描述事务的执行顺序和依赖关系。事务流图是一个有向无环图,其中节点表示子事务,边表示子事务之间的依赖关系。通过事务流图,Calvin 模型可以在分布式环境中实现事务的一致性和原子性。
尽管Calvin模型具有许多优点,但也存在一些缺点和挑战。在Calvin模型中,每个事务的执行都是独立的,并且在分布式环境中以并行方式执行。这种并行执行可能导致一些数据一致性问题,例如读取到过期或不一致的数据。虽然Calvin模型提供了一些机制来解决这些问题,如版本控制和冲突检测,但仍然无法完全消除数据一致性的风险。其次,Calvin模型需要一个全局调度器来协调和管理所有事务的执行。全局调度器需要考虑诸多因素,如事务的依赖关系、并发控制、负载均衡等,这增加了调度的复杂性。此外,全局调度器也可能成为系统的瓶颈,限制了整个系统的扩展性和可伸缩性。
3. XA模型
XA模型是一种用于管理分布式事务的标准接口规范。它定义了在分布式环境中进行事务处理所需的协议和操作。XA模型的名称来自于 X/Open 组织(现在是The Open Group),它制定了 XA 接口规范,以便不同的事务处理管理器(Transaction Manager)和资源管理器(Resource Manager)之间能够进行协作,实现数据的一致性。
在 XA 模型中,事务管理器(Transaction Manager)负责协调和管理事务的执行,而资源管理器(Resource Manager)则负责管理和操作特定的资源,XA 模型通过定义一组标准的接口和操作,使得事务管理器和资源管理器之间可以进行协作。
XA 模型的核心是两阶段提交(Two-Phase Commit,2PC)协议。在2PC协议中,事务管理器和资源管理器之间通过一系列的消息进行通信,以确保所有参与的资源管理器都在一个事务中执行相应的操作,并且最终要么全部提交,要么全部回滚。在这个过程中,事务管理器作为协调者(Coordinator),负责发起和管理2PC协议的执行。
XA模型的具体实现需要事务管理器和资源管理器都遵循XA接口规范。事务管理器需要实现事务的开始、提交、回滚等操作,以及协调2PC协议的执行。资源管理器需要实现参与2PC协议的相关操作,如准备(Prepare)、提交(Commit)、回滚(Rollback)等。
2.PolarDB for Xscale分布式事务模型
PolarDB分布式版作为一款企业级的分布式数据库,数据天然分片到不同的存储节点上,跨节点的修改和访问变成了常态,保障数据库的 ACID 特性,以及如何做到透明分布式也变成了一项有挑战的事情。
2.1 业务场景和挑战
1. 转账模型,如何保证ACID特性
如下图所示,一个经典的转账模型,在跨节点的情况下,如何保证事务的原子性,以及跨节点查询的一致性,单机的事务系统已经无法完成。
2. 多维度分区键,如何做到业务透明访问
在业务模型设计的时候,对应的业务访问,通常会涉及到多个维度,传统的本地索引无法多维度路由,这就需要引入全局二级索引来应对多维度的业务访问诉求,以便达到业务无感知,并高效的多维度路由访问方式,如何在跨节点的维护全局二级索引,同样需要分布式事务来保证。
2.2 Two-Phase Commit协议
为了实现 PolarDB-X 具有数据库的 ACID 特性和强一致性,其分布式事务模型采用了两阶段提交 (Two-Phase Commit,2PC) 协议,按照严格的 XA Spec 的定义,其中:
1. Transaction Manager
CN 节点承担事务管理的职责,作为协调器 (Coordinator) , 负责分布式事务状态的持久化和分布式事务的推进流转。
2. Resource Manager
DN 节点承担事务参与者的职责,作为参与方 (Participants),负责接受用户数据的修改和事务状态的变化。
3.GCN分布式事务系统架构
为了实现严格的 2PC 协议, PolarDB分布式版存储引擎在SCN单机事务系统的基础上,实现了 GCN ( Global Commit Number) 分布式事务系统,其架构图如下:
Lizard 事务系统的事务槽 (Transaction Slot) 在原有的基础上扩展了一个字段,用于保存一个GCN,即Global Commit Number,这个GCN来源于TSO发起号,至此,跨节点的事务将使用GCN来代替SCN为分布式事务全局定序。
3.1 事务原子性
跨节点的分布式事务,严格按照2PC协议的标准来实现,根据XA Spec定义的接口,CN节点通过以下的语法来操作DN节点。
XA {START|BEGIN} xid
XA END xid
XA PREPARE xid
XA COMMIT xid [ONE PHASE] $GCN
XA ROLLBACK xid $GCN
XA RECOVER
在XA COMMIT / ROLLBACK的时候,CN节点从GMS获取一个TSO,作为本次的外部提交号,也就是GCN,传给所有DN参与方,并持久化。
通过 2PC 协议, PolarDB-X 严格保证用户跨节点事务的原子性。
在XA COMMIT / ROLLBACK的时候,CN节点从GMS获取一个TSO,作为本次的外部提交号,也就是GCN,传给所有DN参与方,并持久化。
通过 2PC 协议, PolarDB-X 严格保证用户跨节点事务的原子性。
3.2 读强一致性
在用户的跨节点访问中,CN节点将获取一个TSO作为本次查询的MVCC视图,并通过 AS OF查询下发到 DN 存储节点,其语法如下:
SELECT ... FROM tablename
AS OF GCN expr;
在GCN事务系统中, 由于分布式事务由GCN定序,所以查询的可见性比较,也从单机的SCN比较转换成GCN比较,来保证全局的强一致性,由于外部定序和两阶段的提交过程, 在可见性比较的时候,对于无法决策的Prepare状态,PolarDB分布式版采用了等待的策略,等事务状态完结,再进行GCN的大小比较。
3.3 XA协调日志下沉
▶︎ 协调日志
分布式数据库中,两阶段提交协议(Two-Phase Commit,2PC)是一种经典的分布式事务处理协议。该协议分为两个阶段:准备阶段和提交阶段。在准备阶段,各个节点会将数据状态反馈给协调节点,协调节点再根据各节点的状态,决定是否进行提交。在提交阶段,协调节点通知各个节点进行提交或者回滚操作。
虽然2PC协议可以确保数据的一致性,但其在性能上存在较大问题。一个典型的问题是:两阶段提交过程中,多个节点需要进行通信:发起请求和等待应答。多节点之间的交互次数要比单机数据库要多得多,这导致了严重的延时问题,极大地限制了分布式数据库事务提交的性能和可扩展性。特别是对于OLTP型的业务,其大部分事务都是短平快类型,提交性能对于整个分布式数据库影响巨大。
其次,两阶段提交必须保证故障容灾,各个节点、各个环节都有可能发生异常。因此,协调者通常需要记录协调日志来保证最终各个节点事务的状态是一致的。
可以看到,协调者与协调日志是整个两阶段提交流程的关键,如何降低两阶段提交协议的交互次数,如何保证持久化协调日志,如何善后清理协调日志,如何保证协调日志高可用是分布式数据库设计中的核心问题之一。
▶︎ 日志下沉
在Lizard事务系统中,这些问题都将被妥善处理。其核心思想是:协调日志会被下沉到存储引擎上。当发生分布式事务时,其中一个参与方会被选为主分支。主分支负责持久化协调日志,并提供协调日志反查能力。主分支在事务启动时,会开辟一块事务槽空间;当事务提交或回滚时,相关事务状态会被持久化到事务槽上。其他节点在做故障恢复时,必须先找到主分支,并根据主分支的事务状态驱动本分支的事务完成提交或回滚。
其次,事务槽会被 Lizard 事务系统有目的的保留一段时间,直至事务状态信息不再被需要。随后,事务槽会随着数据库的清理系统自动被清理回收。也就是说,提交和回滚并不是事务的最终状态,当代表事务存在的事务槽信息被清理后,事务最终进入 Forget 状态。
同时,协调日志在内部会通过 X-Paxos 协议实现多副本,彻底保证了协调日志的可靠性。
▶︎ 故障恢复
当发生故障时,协调日志是故障恢复的重要环节。Lizard事务系统提供了协调日志回查能力,以协助故障恢复。
值得注意的是,XID 是 XA 分布式事务模型里对事务的唯一标识,是一个外部指定的事务标识号,区别于InnoDB 的 trx_id 以及MySQL的GTID。通过XID回查事务状态信息,对于原生MySQL是困难的,可能需要触发大量的 IO 操作,影响在线业务。Lizard事务系统却没有这个顾虑,原因在于:
1.乐观查找:在prepare阶段,会给CN节点返回事务槽地址的Hint信息,通过Hint信息乐观地查找事务槽,大部分情况下以最多一个IO的代价查找到相关的事务信息。
2.悲观查找:Lizard事务系统捆绑了XID与事务槽地址的映射关系,最坏情况下,只需要查找一组事务槽即可,而不需要对事务槽进行全量搜索。另外,为了防止HA后退化到全量搜索,这个映射关系会被X-Paxos协议广播到所有的副本。
▶︎ 日志下沉 VS 传统2PC
协调日志下沉到存储引擎对于传统 2PC 提交有显著的改进:
1. 协调日志跟随COMMIT / ROLLBACK操作,一起完成持久化,甚至是多副本同步,但并不会带来额外的开销;
2. 协调日志的清理跟随事务槽的清理,不需要额外复杂的清理机制;
3. 缩短了prepare的时间,提高了2PC提交的吞吐能力;
4. 降低了分布式读/单分片读因为读取到prepare事务的记录而被迫阻塞的可能性。
3.4 单分片优化
▶︎ 分布式事务的代价
分布式事务相比于单机事务,需要更多的代价:
1. 提交过程需要从 TSO 获取全局事务提交号;
2. 提交过程需要进行 2PC 提交,涉及到多个节点之间交互,通信开销显著提升。
目前,分布式事务需要完成一个完整的 2PC 提交,同步开销为 3 RT + 2 BINLOG。与之对比,单机事务的同步开销仅为 1 RT + 1 BINLOG。
▶︎ 单分片事务
如果数据的修改仅仅涉及到一个分片,在提交过程中,则完全可以采用一阶段提交来完成,从而节省掉 2PC 提交带来的开销。这样的单机(写)事务在PolarDB分布式版中被称为单分片(写)事务。
同样的,如果数据的查询仅仅涉及到单个分片,也可以不通过与 TSO 交互,直接在 DN 节点上完成查询操作。这样事务在 PolarDB-X 同样被称为单分片(读)事务。
单分片事务与纯粹的单机本地事务看起来很像,但内部逻辑是完全不同。最关键的区别是,单分片事务的可见性判断依赖于全局事务提交号GCN,而纯粹的单机本地事务的可见性仅仅取决于本地提交号SCN。
▶︎ 单分片事务提交序
单分片事务的提交号不能从TSO获取,因此如何确定单分片事务的提交号,是单分片事务最核心的问题。一个直观的理解是,单分片事务发生在本节点已提交的分布式事务之后,以及本节点已发起的分布式查询之后。即它的提交号必须比本节点所有已提交的分布式事务的提交号大,并且比分布式查询的快照号大。
为此,Lizard事务系统内部维护了本节点的全局GCN,该GCN被称为narrow_GCN(narrow GCN, 狭隘的全局提交号)。narrow_GCN在以下场景会被推高:
1. 分布式查询事务会推高 narrow_GCN
2. 分布式写事务会推高 narrow_GCN
当单分片(写)事务提交时,会获取本节点的 narrow_GCN 作为自己的全局提交号。
另外,单分片(写)事务之间也有提交顺序,该提交顺序仅仅由本地提交号 SCN 决定。
至此,单分片(写)事务的提交号可以确定为(narrow_GCN,SCN),该提交号反映了单分片事务的提交序。
▶︎ 单调递增 narrow_GCN
narrow_GCN 作为提交号,必须要满足单调递增永不回退。narrow_GCN 会被持久化到 Lizard 系统表空间中。然而,如果每次事务提交都需要持久化 narrow_GCN,则会形成严重的性能热点,限制了数据库的整体吞吐能力。
为此,Lizard事务系统优化了narrow_GCN的持久化性能。其核心思想是,对narrow_GCN的修改只记录重放日志,而不对实际的数据页进行修改。也就是说,数据页的实际修改会被延后,期间所有的修改都被合并为一次修改,大幅度改善了 narrow_GCN持久化的性能问题。
▶︎ 可见性
单分片事务与分布式事务并存时,可见性判断会面临巨大的挑战,因为同时会存在分布式读与分布式写,分布式读与单分片写,单分片读与分布式写,单分片读与单分片写的情况。一个典型的案例是:
1. 单分片读启动,并使用当时DN节点的最大 GCN(假设为95)为 Snapshot_GCN 作为视图;
2. 单分片写启动,账户A(转账前余额为1000)向账户B(转账前余额为1000)转账100块,并最终提交,取得提交号为 DN 节点的最大 GCN(同样为95)。
单分片读可能会读到一个不一致的状态:
1. 当单分片写未提交时,单分片读到账户A的余额为1000块。
2. 当单分片写提交后,单分片读此时才读取到账户B的余额,发现已提交,并且余额为1100
显然,这违反了事务的原子性以及一致性。类似的不一致性问题其它场景下也会发生。
可见性判断的关键在于定序,Lizard 事务系统通过全局提交号加本地提交号组合的方式准确给所有的事务确定先后顺序。更具体地说,可见性判断先依据全局提交号 GCN 来确定顺序,而当 GCN 已经无法确定先后顺序时,会进一步依据本地提交号 SCN 来确定先后顺序。
3.5 二级索引可见性
MySQL的二级索引的修改并不会产生UNDO,也就是说,二级索引没有多版本。MySQL原生事务系统对于二级索引可见性判断,主要依赖于数据页上的max_trx_id 字段,该字段表示了所有修改该二级索引数据页上的事务中,最大的事务ID号。
当 MySQL 查询生成视图时,同时会获取当时数据库里最小的活跃事务ID号。当读取二级索引时:
1. 如果数据页上的 max_trx_id 小于视图的最小的活跃事务ID号,则本数据页上所有的二级索引记录都可见;
2. 否则,无法判断出该二级索引的可见性,需要回到主键索引上进行可见性判断。这个过程一般称为回表。
回表需要回到主键上进行B+Tree的查找,直到找到对应的主键记录。显然,回表会带来巨大的查询开销,特别是可能会带来大量的随机IO。一个设计优秀的事务系统,应该尽量减少回表的次数。
MySQL的二级索引可见性判断是一个很自然的解决方案。然而在分布式查询场景,这个方案就不再可行,原因在于所有的分布式查询,都采用了 Flashback Query。在这种查询下,需要的不是现在的(最新的)系统里的最小的活跃事务ID号,而是当时的(历史的)最小的活跃事务ID号。
一个可靠的方案是:对于所有的二级索引查询都进行一次回表。然而,从上面分析可以获知回表的代价是很高的。
Lizard事务系统采用 TCS (Transaction Commit Snapshot) 方案彻底解决了分布式读查询二级索引总是需要回表的问题。其核心思想是,每间隔一段时间内部产生一个事务系统提交快照信息。该快照信息里保存了当时的事务系统状态,如 GCN, SCN, min_active_trx_id 等等。根据系统配置,保留一段时间内的所有事务系统提交快照信息。
当分布式查询构建视图时,需要拿着视图的 Snapshot_GCN,到 TCS 里查找,找到一个最贴近的min_active_trx_id来作为数据库在Snapshot_GCN时的近似 min_active_trx_id值,并用该值与二级索引数据页上的max_trx_id进行比较来决定二级索引的可见性。
测试结果显示,Lizard事务系统在二级索引可见性上的优化,相比全回表方案,提升在400%以上。
3.6 XA完整性
PolarDB分布式版的分布式事务依赖于XA模型。然而,MySQL XA事务在多副本策略下,容易导致主备不一致。经过测试,直到MySQL 8.0.32版本,经过多轮完善修改后,XA事务仍然有概率导致主备不一致。该问题的根源在于,BINLOG日志作为XA事务参与方之一,由于其本身为文本追加的格式,事实上并不支持回滚能力。这个问题在PolarDB分布式版的分布式模型中会被放大,原因在于PolarDB分布式版 DN 节点内部依赖 X-Paxos的多数派协议,BINLOG日志同时作为协议的载体,需要承担更多更复杂的状态流转逻辑。
Lizard 事务系统针对MySQL XA事务完整性问题,提出了基于GTID的全量事务日志回补方案,彻底解决了MySQL由来已久的XA事务完整性问题。其核心思想是,BINLOG 日志除了承担了内部协调日志功能外,还保存了全量的事务操作日志。在故障恢复中,存储引擎会提供 GTID executed 集合,BINLOG日志会根据GTID集合,回补在存储引擎中丢失的事务。
文章转载自公众号:阿里云瑶池数据库