作者:光气
背景
ACID 事务是关系型数据库一个重要的特性,也是 NewSQL 数据库最大的挑战之一。在 PolarDB-X 的架构中,Data Node(DN)是通过 Paxos 同步日志的,保证了事务的持久性(Durability),而原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation)需要通过合适的事务策略来保证。除此之外,在分布式场景下,由于数据被分散在不同节点,线性一致性(linearizability)也是事务策略的重要特性。
目前主流的分布式数据库都使用了基于两阶段提交(2PC)的策略,包括 Percolator 算法和 XA 协议。Percolator 是 Google 基于 BigTable 做的分布式系统,一个重要的设计就是使用了 Percolator 算法支持分布式事务。在 Percolator 算法中,最重要的参与者是客户端,更新被缓存在客户端,并在 Commit 时通过 2PC 提交到服务端。Percolator 算法的最大优势之一是主要状态都在客户端完成,服务端只需要支持简单的 CAS 就可以,不需要维护事务状态,也不需要引入额外的事务管理器。但 Percolator 也有一些已知缺陷:提交阶段延迟较高,仅支持乐观锁场景、冲突错误只能在提交时汇报等。XA 协议全名为 X/Open XA 协议,是一项通用的事务接口标准。XA 协议也基于两阶段提交的策略。
PolarDB-X 的事务策略
PolarDB-X 采用的是基于 XA 协议实现的分布式事务策略,其中又分为多种实现:
• 适用于 MySQL 5.6 的 BestEffort 事务
• 基于 InnoDB 的 XA 事务
• PolarDB-X 2.0 中实现的 TSO 事务
本文会重点介绍 XA 事务和 TSO 事务。
XA 事务
在 XA 协议的设计中,有两种角色:
• 事务管理器(Transaction Manager,TM):负责发起事务的提交,失败时处理事务异常,在 PolarDB-X 中这个角色由计算节点(CN) 承担
• 资源管理器(Resource Manger,RM):事务的参与方,如 MySQL 中的一个库,在 PolarDB-X 中这个角色由存储节点(DN)承担
XA 协议由 TM 向多个 RM 通过 XA PREPARE 和 XA COMMIT 两条命令完成两阶段提交。两阶段提交常常为人诟病的问题是 TM 的单点问题和 Commit 阶段发生异常可能导致的数据不一致问题。为此,在 PolarDB-X 的实现中,我们额外引入了事务日志表以及 COMMIT POINT 的概念。确认所有参与节点 Prepare 成功的情况下,我们向全局事务日志添加一条事务提交记录作为 COMMIT POINT。在 TM 发生异常的情况下,我们可以选择新的 TM 继续完成两阶段提交,新 TM 会根据主库中是否存在 COMMIT POINT 记录选择恢复事务状态或者回滚事务。
• 如果 COMMIT POINT 不存在,那么可以保证没有任何一个 RM 进入 Commit Phase,此时可以安全回滚所有 RM。
• 如果 COMMIT POINT 存在,那么可以保证所有 RM 都已经完成了 Prepare Phase,此时可以继续进行 Commit Phase。
XA 事务在并发行上有一定的局限性,在执行的过程中,我们必须使用类似 Spanner 锁定读写的事务策略,为所有 SELECT 操作带上 LOCK IN SHARE MODE ,造成读写相互阻塞。基于Lock模式的分布式事务在有单记录并发冲突时整体性能偏低,目前数据库业界的解法是事务多版本的MVCC策略,尽管我们的 DN 节点 InnoDB 本身支持基于 MVCC 的快照读,但我们却无法提供高效的快照读事务策略,原因是事务提交时不同 DN 执行 XA COMMIT 的时间不同,一个快照读请求请求不同分片的时间也不同,这意味着发起一个跨 DN 的快照读请求时,可能读到一个事务的部分已提交数据,无法得到全局的一致性视图(Snapshot)。
TSO 事务
造成 XA 事务无法提供快照读功能的核心原因是我们缺乏一个全局时钟来排序每个事务,每个事务和视图在不同的 DN 上可能是不同的顺序。因此在 PolarDB-X 2.0 中,我们引入了基于 XA 事务优化的 TSO 事务。TSO 事务需要一个生成全局单调递增 Timestamp 的策略,常见的策略有 True Time(Google Spanner)、HLC(CockroachDB)、TSO(TiDB),在我们目前的实现中,我们使用了 TSO 策略,由 GMS(元数据管理服务)作为一个高可用的单点服务承担生成 Timestamp 的任务。TSO 保证了正确的线性一致性和良好的性能,只是在跨全球机房部署的场景会带来较高的延迟。
原生的 InnoDB 引擎无法满足我们支持 TSO 事务的需求,因此我们修改了 InnoDB 引擎的提交逻辑和可见性判断逻辑,在 XA BEGIN 和 XA COMMIT 前插入了我们自定义的两个变量 SNAPSHOT_TS 和 COMMIT_TS。
• SNAPSHOT_TS 用于判断其他事务提交数据对当前事务的可见性,统一了一个分布式事务在每个分片上进行读取发生的时间。SNAPSHOT_TS 决定了当前事务的快照。
• COMMIT_TS 统一了一个分布式事务在每个分片上提交数据发生的时间,会被记录到 InnoDB 引擎中。COMMIT_TS 决定了当前事务在全局事务中的顺序。
在私有化的 InnoDB 中,我们会根据事务的 SNAPSHOT_TS 来决定可见性,同时我们也会让新事务的读请求在遇到处于 prepare 状态的数据时进行等待,避免处于 prepare 状态的事务 COMMIT_TS 比当前 SNAPSHOT_TS 更小导致 commit 前后数据不同的问题。
基于 TSO 事务,我们也做了一系列新功能和优化:
备库一致性读
由于 TSO 具备了全局快照的能力,我们可以将查询的任何一部分转发到任何分片的任何备份节点上进行读取,并不破坏事务语义。这个 feature 对我们实现面向 HTAP 的混合执行器非常重要,TP/AP 的资源分离以及 MPP 执行框架都基于这个 feature 来保证正确性。
一阶段提交优化
如果在提交阶段我们发现事务只涉及了一个分片,那么我们就会将其优化为一阶段提交,使用 XA COMMIT ONE PHASE 语句提交事务。对于正常的 TSO 事务,我们取了 SNAPSHOT_TS 和 COMMIT_TS 两个时间戳,而对于一阶段提交的事务来说,其实行为与单机事务类似,因此我们并不需要通过 TSO 获取 COMMIT_TS,而是可以直接由 InnoDB 计算出一个合适的 COMMIT_TS 来提交事务。具体的计算规则是:COMMIT_TS = MAX_SEQUENCE + 1,其中 MAX_SEQUENCE 为 InnoDB 本地维护的历史最大的 snapshot_ts。
只读连接优化
一个事务如果使用 START TRANSACTION READ ONLY 开启,那么我们就会将事务标记为只读事务。我们会直接通过多个 autocommit 的单语句获取需要的数据,避免长期持有连接和事务的开销。由于 TSO 的存在,我们只需要使用相同的 TS 就能保证读到相同的数据,因此我们通过私有协议支持在每个语句内置一个 SNAPSHOT_TS,保证了同一个事务内的多条单语句读到相同的数据。
通常,用户很少会主动使用 START TRANSACTION READ ONLY 开启事务,因此对于常规事务,我们也针对每个连接使用了延迟开启 XA 事务的策略。对于所有连接,默认以只读的形式不开启事务,直到第一个写请求或者 FOR UPDATE 读请求再进行正常的 XA 事务流程。
除了只读事务,这个优化的另一个场景是多分片读单分片写的事务,通过这个方案我们能将其优化为一阶段提交事务,在 TPCC 的测试结果上得到了 14% 的性能提升。
当前读事务优化
在上面的只读连接优化中,我们通过将仅使用快照读的连接摘出事务之外,来优化 COMMIT_TS 的获取。而如果是完全相反的情况 ——— 所有连接都是写操作或者带锁的当前读操作,那么我们完全不需要进行快照读。因此我们也做了这样一个优化:仅在第一次进行快照读时获取 SNAPSHOT_TS。这个优化针对的是一些对 Serializable 有很强需求的场景:
BEGIN;
SELECT balance FROM accounts WHERE id = 0 FOR UPDATE; # 检查余额,需要加锁
UPDATE accounts SET balance = balance - 1 WHERE id = 0;
UPDATE accounts SET balance = balance + 1 WHERE id = 1;
COMMIT;
上面的 SQL 执行的是一个典型的转账场景,将 1 元从 id 为 0 的账户转到 id 为 1 的账户,在整个事务中都没有使用到快照读,因此针对这种场景我们会省略 SNAPSHOT_TS 的获取。
单机多分片优化
如果分布式事务的多个分区位于同一个 DN 节点上时,实际上可以将它们共同视作一个单机事务。这种情况下,可以将多个 RPC 合并,一次性地完成多个分片的 PREPARE 和 COMMIT。如果所有分片均在同一个 DN 节点上,甚至可以将事务以 1PC 的方式提交。
异步提交优化
上述优化针对的依然是一部分特定场景,对于多分片的分布式事务,往往还是相比单机事务有较长的延迟。因此我们设计了异步提交方案,针对任何分布式事务,都可以在完成了 PREPARE 阶段后直接返回成功,达到接近单机事务的提交延迟(一次跨机房 RPC)且不影响数据可靠性和线性一致性。我们会在之后的文章中详细介绍这一方案,欢迎大家关注我们专栏。
总结
本文主要介绍了 PolarDB-X 2.0 中的 TSO 分布式事务的实现。相比于 PolarDB-X 1.0 (DRDS)默认的 XA 事务,TSO 事务性能更优:通过 MVCC 方式避免加读锁,同时通过一系列优化(例如异步提交)提升了性能。此外借助 TSO 事务还提供了备库一致性读的能力。