暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB-X技术解读系列|Lizard,创新经典算法,性能直升20%

PolarDB 2024-11-06
276

早在上世纪 70 年代,针对分布式计算中的一致性问题,两阶段提交算法(Two-Phase Commit Protocol,2PC)作为一个概念进入到了当时学者们的视野中。后来随着分布式数据库和分布式系统的兴起,2PC 在分布式事务处理中的重要性迅速提升。到了现代,随着云计算的兴起,分布式计算面临了新的挑战,出现了更多复杂的算法和协议,如 Paxos、三阶段提交(3PC)、XA 协议等等,这些算法大都能找到 2PC 的影子。时至今日,两阶段提交算法的基础概念仍然是理解分布式一致性问题的重要起点。

经典两阶段提交算法角色


上图展示了在分布式数据库领域里,一个经典且成熟的两阶段提交算法。其中各个组件的概念如下所示:

  1. AP (Application Program)。应用程序等数据库服务使用者。

  2. TM (Transaction Manager) 。充当 2PC 算法里的协调者角色。

  3. RM (Resource Manger) 。充当 2PC 算法里的参与者角色,单次分布式事务可能涉及多个 RM,每个 RM 里可能存在多个分支事务。所有的分支事务共同组成了一个完整的分布式事务。

  4. GMS (Global Metadata Service)。在分布式数据库领域中,为了提供 MVCC 能力,需要借助 GMS 服务为所有的分布式事务进行定序。

经典两阶段提交算法协议

当 AP 对一个分布式事务发起提交请求后:

  1. PREPARE 阶段

TM 对所有的参与方 RM 下发 PREPARE 指令,当所有 RM 均响应请求并返回成功的消息后,PREPARE 阶段完成。

  1. GMS 定序阶段

TM 访问 GMS 服务,获取全局提交号 GCN (Global Commit Number),为该分布式事务定序。

值得注意的是,该阶段并非 2PC 算法里必须要求的阶段。但对于数据库领域,通过全局定序来实现读一致性,仍然是目前业内久经考验且被认可的成熟方案。

  1. COMMIT 阶段

TM 对所有的参与方 RM 下发 COMMIT 指令,当所有 RM 均响应请求并返回成功的消息后,该分布式事务提交成功。

经典两阶段提交算法的困境

两阶段提交算法作为分布式计算里最古老的算法之一,按理说应该已经没有什么值得讨论的空间,但在我们实际工程化中,却发现了经典两阶段提交算法在分布式数据库领域中的三大困境。

困境一:事务日志流转成本高昂

回顾经典两阶段提交算法,PREPARE(以及 GMS 定序)完成后,TM 需要驱动 RM 的分支事务进行事务提交。

为了确保事务满足原子性(如多个分支事务最终全提交或全回滚),TM 通常需要在 COMMIT 阶段前,先持久化一份事务日志(Transaction Log, TLOG)。以保证意外崩溃恢复后,TM 仍然能够驱动悬而未决的分支事务(以下简称为悬挂事务)进行提交或回滚。

通常 TLOG 日志结构化抽象表示如下所示:


分布式事务标识

事务完结动作

事务提交号

总参与方

....

GTRID_1

COMMIT

225208

5

....

GTRID_2

ROLLBACK

/

3

....

TLOG 是两阶段提交的核心日志,如果 TLOG 日志不可用或发生丢失:

  • 资源无法释放:悬挂事务无法完结,占有的锁资源、UNDO 资源等都无法被释放

  • 破坏事务特性:丢失 TLOG 日志会导致分布式事务失去一致性、原子性


因此如何高效且可靠地使用 TLOG 是两阶段提交算法中最关键的部分之一。通常,TLOG 在工程化中有如下难点:

  1. TLOG 存储成本

TLOG 天然是一份结构化的数据,天然适合采用表形态存储在 RM 上。对其产生的修改需要一个完整的事务,即在一个 RM 上提交分支事务,需要额外进行一次 TLOG 表的事务操作。我们知道,一次事务操作需要非常高昂的成本。

  1. TLOG 管理成本

TLOG 日志存储内容不能无限制地膨胀,即需要 TM 定期将不需要的 TLOG 日志清理,这带来了额外的管理开销,以及一定的风险。极端场景下甚至会对用户的业务产生影响。

  1. TLOG 高可用成本

TLOG 日志是两阶段提交的核心日志,其可用性需要得到最高级别的保障。如在 PolarDB-X 这样的金融级数据库中,TLOG 日志会被要求保存两个副本以上,以确保其可用性。

当然,这并非没有代价,我们知道基于副本的容灾设计,往往意味着高昂的通信、存储、同步等待成本。特别是这些代价发生在两阶段提交过程之中。

困境二:单 RM 多分支无法并行

事务拆分以及并行化是分布式数据库相比于单机数据库具备性能优势的关键奥秘之一。事务拆分落实到存储引擎上,业界产品(如 Oracle、PolarDB-X 等)有多种解决方案。以 PolarDB-X 为例,分布式事务会被拆分成多个分支事务,在 RM 上并行执行:

  1. 对于 RM 而言,每个分支事务都是一个独立的事务,需要满足事务 ACID 特性。

  2. 对于 TM 而言,多个分支组成了一个完整的分布式事务,理应表现出一个完整事务的特性。


这种矛盾性主要体现在两个方面:

  1. 写分支事务割裂

同一个 RM 下多个并发的分支事务虽然同属于一个分布式事务,但彼此的修改相互不可见。这是因为两阶段提交里的分支事务对于 RM 而言是一个独立事务,只有事务提交后其修改才能被其它事务可见。这大大限制了分支事务并行化的适用范围。

  1. 读查询无法读取一致状态

在传统两阶段提交算法中,同一个 RM 下多个分支事务提交总是有先后顺序,因此查询无法保证同时看到或同时看不到两个分支事务。也就是说对于 AP,可能会发现查询只看到了一个分布式事务的一部分修改,而看不到另一部分修改。

困境三:多 RTT 导致性能恶化

对于本地单机事务而言,提交阶段通常只需要一个 RTT。然而,相比于本地单机事务,经典两阶段提交算法在分布式数据库应用中,通常需要 3 个 RTT:

RTT-1: PREPARE 阶段,驱动分支进行两阶段提交流程。

RTT-2: GMS 定序阶段,通过 GMS 为该分布式事务定序。

RTT-3: COMMIT 阶段,驱动各个分支进行事务的完结状态。


然而,RTT 对于系统整体的吞吐能力影响是显而易见的:

  1. 写入性能:每次 RTT 都涉及通信代价,如跨节点的网络延迟、或本节点的总线延时等。由于两阶段提交阶段是阻塞同步等待,过多的延时会恶化数据库写入性能

  2. 查询性能:两阶段提交过程中由于未进行全局定序,对事务所修改的数据无法判断可见性,通常需要阻塞查询,以保证查询一致性。当提交过程延时较大时会恶化数据库的查询性能。



  PolarDB-X 的解决方案:Lizard 


针对上述问题,PolarDB-X  通过多个模块的深度协同,提供了完整的解决方案:Lizard,Lizard 是 PolarDB-X 存储引擎的新型事务引擎,区别于 InnoDB 这样的单机事务系统,其设计之初就是为了提供分布式场景下的事务引擎解决方案。

本文主要着重介绍存储引擎的相关思考与设计,但在这之前,首先简单介绍 PolarDB-X 的整体架构,以作铺垫。

PolarDB-X 产品能力

PolarDB-X 是阿里云面向高吞吐、大存储、低延时、易扩展和超高可用的云时代数据库使用需求自主设计研发的高性能云原生分布式数据库产品。PolarDB-X 作为云原生时代的产品,其特点可以由 5 个词来表达:金融级高可用透明分布式集分一体HTAP 一体化开源与多云安全与稳定PolarDB-X 的架构如下图所示。


各个模块如下:

  • 元数据服务(Global Meta Service,GMS),主要提供分布式的元数据,提供全局授时服务(TSO)、维护 Table/Schema、Statistic 等 Meta 信息。承担了 2PC 中分布式事务全局定序的角色

  • 计算节点(Compute Node,CN),主要提供分布式 SQL 引擎,包含核心的优化器和执行器。基于无状态的 SQL 引擎提供分布式路由和计算,解决分布式事务 2PC 协调、分布式 DDL 执行、全局索引维护等。承担了 2PC 中 TM 的角色。

  • 存储节点 (Data Node,DN),主要通过自研的 Lizard 事务引擎,提供了分布式场景下高效且高可靠地存储、检索和处理海量数据的能力;以及通过分布式共识协议提供数据高可用保障。承担了 2PC 中 RM 的角色

  • 日志节点(Change Data Capture,CDC),主要提供兼容 MySQL 生态的主备复制协议,兼容 BINLOG 协议和数据格式、支持主备复制 Replication 的协议和交互。

  • 列存节点(Columnar), 主要提供持久化列存索引,实时消费分布式事务的 BINLOG 日志,基于对象存储介质构建列存索引,能满足实时更新的需求、以及结合计算节点可提供列存的快照一致性查询能力。

Lizard 两阶段提交算法

针对经典两阶段提交算法目前遇到的三大困境,Lizard 事务系统有三大“法宝”:

TLOG 日志下沉

TLOG 日志是两阶段提交算法里的核心模块。Lizard 事务系统通过引入 Lizard 事务槽机制彻底彻底解决了 TLOG 日志流转成本过高的问题。

Lizard 事务槽设计

Lizard 事务系统会为所有事务分配一个事务槽,事务槽维护了事务所有的状态信息。一个典型的事务槽如下所示:

事务标识

事务状态

事务提交号

XA 成员信息

XA 事务组信息

drds-1ef@23425

COMMIT

225208

{drds-1ef...}

{236029, 112899..}

drds-2cb@34856

ROLLBACK

225209

{drds-2cb...}

{258019, 164115..}

其中典型的事务信息如下所述:

  1. XA 事务标识:分支事务的全局唯一标识

  2. XA 事务状态:分支事务状态,包括:ACTIVE、PREPARE、COMMIT、FORGET 等等

  3. XA 事务提交号:分支事务提交号,用于定序,包括外部(全局)提交号以及内部提交号(表格未展示)

  4. XA 事务成员信息:分布式事务参与方信息,包含了全局参与方与局部参与方信息

  5. XA 事务组信息:分布式事务多个参与方作为一个事务组的关联关系

可以看到,TLOG 日志从上层的逻辑表结构下沉到存储引擎,由 Lizard 事务槽接管。

Lizard 事务槽管理

Lizard 事务槽的生命周期由 Lizard 事务系统管理,其生命周期可以大致分为如下阶段:

  1. ACTIVE: 事务启动后会向 Lizard 事务系统分配一个事务槽,该事务槽处于 ACTIVE 区域,以便事务系统能够迅速访问和更新。

  2. FINISH: 事务通过两阶段提交后进入完结阶段,事务槽适时维护相关的 XA 事务信息,并从 ACTIVE 区域挪移到 HISTORY 区域。在该区域事务槽会依据策略保留足够长的时间。

  3. FORGET: 在确保不再被需要后,Lizard 事务槽被 Lizard 清理系统及时清理,并回收空间以备下次使用。

Lizard 事务槽副本

为了提供金融级的可用性,Lizard 事务槽会被同步到其它节点。目前 PolarDB-X 采用逻辑日志的方式同步到 Standby 节点。Standby 节点会根据逻辑日志重建一个 Lizard 事务槽的逻辑副本。

Lizard 事务槽优势

相比于基于逻辑表的 TLOG 日志方案,Lizard 基于事务槽的 TLOG 日志下沉方案有显著的优势:

  1. 事务槽存储成本低:事务槽跟随原事务进行流转,无需额外的事务对象来维护。这意味着所付出的成本不过是多产生了一些 REDO 日志以及少量的空间资源,而没有增加任何的同步等待开销。

  2. 事务槽自治:事务槽的生命周期维护由 Lizard 事务系统负责,并确保需要的事务槽会被保留,而不再需要的事务槽会被平滑地清理,以避免空间膨胀。

  3. 事务槽高可用成本低:事务槽随着本事务的复制日志同步到 Standby 节点,没有增加额外的 RTT 开销以及同步等待开销,大幅度降低了高可用成本。

分支事务并行化

如何让分支事务充分并行是分布式数据库存储引擎的核心问题之一。基于此,Lizard 事务系统提供了丰富的事务策略以让 TM 可以对分支事务如臂使指。其中,某些策略看似在突破数据库 ACID 理论,实则在分布式场景下确是自然且必要的。

打破「次元壁」,未提交修改也可见

倘若有人告诉笔者事务未提交修改也要被看见,那笔者可能要翻出 ACID 理论与之论述。然而,这种怪异的现象在分布式场景下却可能是必须的。

其根本原因在于,一个 RM 上多个分支事务虽然借用了单机的事务对象结构,但对外是一个完整的分布式事务,这意味着该(分布式)事务内的修改理应在本(分布式)事务内可见。

Lizard 事务系统通过引入事务组概念,将同属于一个事务组内的事务之间的「次元壁」打破,让彼此的修改相互可见,这为分支事务并行化提供了充分的可能性与灵活性。

这也是 PolarDB-X 集分一体理念的充分体现:

  • 需要的时候(集中式),单机事务与单机事务之间像是隔了一道天河,永远存在隔阂

  • 不需要的时候(分布式),分支事务与分支事务之间像是拿走一张纸一样,轻而易举地搭起了一座鹊桥

共享提交状态,仿若一个原子事务

多个分支事务在同一个 RM 上总是有先后顺序,存储引擎也无法阻止某些查询在分支事务提交的间隙发起,这意味着查询可以看到部分分支事务的修改,而看不到另外分支事务的修改。

为了让多分支事务看起来就像一个事务,Lizard 事务系统会在事务组里标识出主分支与附属分支,主分支与附属分支之间会构建出关联关系。通过这种关联关系,同属于一个事务组的分支事务共享同一个提交状态。因此,当外部查询发起时,多分支事务看起来就像是一个原子事务。

异步提交

通过上述介绍,PolarDB-X 分布式事务的 MVCC 方案采用了全局授时时间戳方案(TSO)。

在 PolarDB-X 存储引擎中,TSO(由 GMS 服务承担)产生的定序号,被称为 GCN(Global Commit Number)

TSO 方案参与两阶段提交的核心思想可概括为:

  • 第一步:“锁住”提交中的记录不允许被查询(PREPARE)

  • 第二步:获取分布式事务的提交号 GCN(GMS 定序)

  • 第三步:释放第一步的“锁”,并带着第二步产生的 GCN 驱动事务进入完结状态(COMMIT)

通过上述分析我们知道,传统的基于 TSO 的两阶段提交方案带来了非常高昂的代价,整个流程涉及到 3 个 RTT。针对这个问题,PolarDB-X 采用了异步提交方案,大幅地降低了提交代价。

其中,PolarDB-X Lizard 存储引擎将 XA 分支事务升级为 AC 事务(Async Commit 事务),以支撑 PolarDB-X 达到极致的提交性能。以下从 PolarDB-X 存储引擎视角介绍 AC 事务。

AC 事务提交

如果上图所示,AC 事务的提交流程可以简化为:

  1. RTT-1:GMS 预定序

TM 从 GMS 获取一个预提交号 PRE_GCN。

  1. RTT-2:AC PREPARE

TM 带着 PRE_GCN 到各个 RM 上发起 AC PREPARE 请求,并根据所有的响应结果,获知所有参与 RM 中已经取到的最大外部号,并将该最大外部号作为该分布式事务的最终提交号 GCN。

当所有的 RM 都完成 PREPARE 的状态流转并响应请求后,至此,该分布式事务的提交状态(包括提交号)已经确定。

随后 TM 可以给 AP 返回 OK 报文表示事务已经提交。整个过程仅使用了 2 个 RTT

  1. 异步:AC COMMIT

TM 通过 AC COMMIT 驱动事务进入完结状态。该过程是一个异步过程,不在用户的流程中。

AC 事务恢复

当分布式事务在两阶段提交过程中发生异常崩溃,TM 需要在恢复流程里处理未决的悬挂事务,并最终驱动所有的分支事务达成一致的完结状态。

为此 Lizard 事务引擎通过事务槽维护了 TLOG 日志以方便回查事务状态;以及提供了 AC 交互能力以便 TM 驱动事务状态流转。

  1. AC 事务状态查询

Lizard 事务系统提供了 XA 事务状态查询能力,其核心模块是由 Lizard 事务槽所维护的 TLOG 日志。Lizard TLOG 日志具有许多优势,但对于查询而言,由于其采用了链式结构维护,索引查询效率并没有逻辑表这样的索引结构高效。为此 Lizard 事务系统引入了两种查询方式:

(1)、乐观查询:Lizard TLOG 日志允许根据物理位置直接定位,进行一次乐观查询

(2)、悲观查询:Lizard TLOG 日志根据事务标识信息被映射到特定了数据段,也大幅度优化了查询效率

通过两种查询模式结合使用,Lizard 事务系统能够以较少的代价查询 XA 事务状态。

  1. AC 事务状态流转

Lizard 事务系统为 XA 事务定义了事务恢复所需的状态信息,以便 TM 根据分支事务状态进行状态流转,从而确保达成正确的恢复流程。目前 Lizard 事务系统支持如下状态表达:

STATUS

含义

ATTACHED

某个 SESSION 正在处理这个事务,也意味着该事务未完结

DETACHED_PREPARE

没有 SESSION 正在处理这个事务,并且这个事务处于 PREPARE 状态

COMMIT

事务处于已提交状态

ROLLBACK

事务处于已回滚状态

NOTSTART_OR_FORGET

事务的事务信息已经被清理掉,或者是这个事务从未存在

通过 XA 事务状态查询,TM 可以决策出其它悬挂的分支事务状态流转方向。

AC 事务定序

分布式数据库 MVCC 理论最核心的问题实际上是一个定序问题。当所有的查询和修改都拥有一个符合操作序列的序号时,则能够实现一致性。

然而,从上述分析可以看到,AC 事务的定序方案与传统的两阶段提交算法里的定序方案有明显的区别。读者可能会奇怪在调整了定序规则后,AC 事务是如何能够保证读一致性。受限于篇幅,此处笔者不打算引入严谨的数学论证,而希望尝试用一个简单的例子来和大家探讨 AC 事务是如何通过定序来解决一致性问题:

  1. AC 事务在 PREPARE 阶段之前从 GMS 获取了预提交号 PRE_GCN,不妨设完成该操作是 T1 时刻

  2. AC 事务在 PREPARE 阶段“锁住”了提交中的记录,不妨设该关键时间点为 T2 时刻

PRE_GCN 不能直接作为该分布式事务的最终提交号 C_GCN,这是因为 T1~T2 期间发生的查询必然取到更大的定序号 S_GCN (snapshot GCN),但却发现该 AC 事务处于 ACTIVE 状态。

更通俗地说,如果使用 PRE_GCN 作为最终的提交号,用户会马上发现这个事务的修改一会看不见(ACTIVE 状态事务不可见),一会又能看见(完成提交后,发现查询事实上发生在修改之后)。

为此,AC 事务会在 T2 时刻后,向 TM 返回 T2 时刻时 RM[i] 上所有已发生的分布式事务里最大的提交号 max_GCN[i]。而最终的提交号:

    C_GCN = max {PRE_GCN, max_GCN[0], max_GCN[1]...}

    可以看到 S_GCN <= C_GCN,反映了其真实的操作时序,因此上述问题将不再存在。

    当然,上述的推论是片面且不严谨的,特别是对于 PolarDB-X 这样的分布式数据库,除了常规的分布式事务,还支持了诸如单分片事务、闪回事务、主从事务等多种事务类型,其分布式 MVCC 机制是复杂且难以短时间阐述清楚的。另一方面,笔者也在尽力避免使用“因果一致性”、“顺序一致性”、“最终一致性”这样的词汇来论述该问题。因此,上述讨论可能只是水面上的冰山,对该方面内容感兴趣的读者可以继续关注我们后续的技术文章解读。

    AC 事务优势

    AC 事务相比传统 TSO 事务具有以下优势:

    1. AC 事务显著降低延迟。传统 TSO 事务需要 3 个 RTT,而 AC 事务仅需要 2 个 RTT,大幅降低了事务提交的响应延时。值得注意的是,对于 PolarDB-X 这样的金融级高可用数据库,所有的状态流转都需要通过分布式共识协议达成多数派。因此,明面上在同步等待上是 3 个 RTT 降为 2 个 RTT,实际上由于减少的 RTT 为 “COMMIT 请求/响应”,在同步路径也无需等待共识协议达成多数派。

    2. AC 事务提高系统吞吐。响应延时与系统吞吐能力是一体两面的关系,降低延时能够有效改善系统吞吐能力。

    3. AC 事务提升资源利用效率。由于最终的 COMMIT 状态流转在异步流程中,并不在同步路径上,因此可以充分地设计调度策略,如按 GROUP 合并请求,在一个 RTT 内完成多个事务的提交。

    Lizard 两阶段提交算法总结

    Lizard 两阶段提交算法是 PolarDB-X 2.0 集分一体、透明分布式理念指导下的分布式事务产品解决方案。实验数据也论证了这一结论,开启 Lizard 两阶段提交策略后能够整体获得 20% 以上的性能提升

    当然,这并非没有代价,新型的事务策略也带来更复杂的恢复流程,以及更复杂的定序设计,来保证事务的 ACID 特性。PolarDB-X 通过各个模块的深度协同,让用户能够透明无感地享受技术红利。关于相关技术点更深入的解读,敬请期待我们后续的技术文章。

    文章转载自PolarDB,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

    评论