内附原文|详解SIGMOD’24最佳论文:PolarDB破解多主架构经典难题

简介: 在今年的SIGMOD会议上,阿里云瑶池数据库团队的论文《PolarDB-MP: A Multi-Primary Cloud-Native Database via Disaggregated Shared Memory》获得了Industry Track Best Paper Award,这是中国企业独立完成的成果首次摘得SIGMOD最高奖。PolarDB-MP是基于分布式共享内存的多主云原生数据库,本文将介绍这篇论文的具体细节。


1. 背景

现如今,对于支持写扩展的多主云原生数据库的需求日益增长。多主架构的数据库不仅可以支持写扩展,同时还可以提供更高的高可用性,并且能够更好地在serverless数据库中提供计算节点的弹性。目前,主流的多主数据库大多是基于share-nothing和share-storage架构。在share-nothing架构中,当一个事务需要访问多个分区时,必须使用分布式事务机制,如两阶段提交策略。这通常会引入显著的额外开销。此外,当系统需要扩容或缩容时,数据可能需要重新分区,这个过程常常伴随着繁重且耗时的数据迁移。而share-storage架构则完全相反,所有数据都可以从所有节点访问。这种架构下不需要使用分布式事务,每个节点都可以独立处理事务。DB2 pureScale和Oracle RAC是基于共享存储的两个传统数据库。它们通常部署在专用机器上,并且成本通常比当下云原生数据库更高。Aurora-MM和Taurus-MM是最近提出的云原生多主数据库。Aurora-MM采用乐观并发控制来处理写冲突,因此在发生冲突时会引发较多的事务中止。相反,Taurus-MM采用悲观并发控制,但是它在不同节点同步数据的效率较低,在高冲突场景下,扩展性不高。


为了应对当前多主数据库面临的这些问题,我们设计并实现了PolarDB-MP,利用分布式共享内存池来解决现有架构的挑战。

2. PolarDB-MP的整体架构

下图展示了PolarDB-MP的架构,不同于其他基于共享存储的多主数据库。PolarDB-MP引入了基于分布式共享内存的Polar Multi-Primary Fusion Server(PMFS)来实现对多主架构的支持。PMFS包括三个核心组件:事务融合(Transaction Fusion)、缓存融合(Buffer Fusion)和锁融合(Lock Fusion)。Transaction Fusion旨在管理全局事务处理,保证事务的ACID属性。Buffer Fusion在维护所有节点间的缓存一致性中扮演着至关重要的角色。Lock Fusion负责两种锁协议:页面锁(PLock)和行锁(RLock)。PLock协议确保在不同节点上访问页面时的物理一致性。RLock协议保证用户数据的事务一致性。此外,PolarDB-MP提出了逻辑日志序列号(LLSN)来对不同节点的redo log(write-ahead log)进行排序。LLSN为不同节点生成的所有redo log提供了一个偏序关系。

0625-01.png

长图首页-不带button.png

3. 设计实现

3.1 事务融合(Transaction Fusion)

事务融合解决的第一个问题是时间戳的问题,PolarDB-MP采用了TSO(timestamp oracle)的方式。事务提交时,会从TSO请求一个提交时间戳(CTS)。这个CTS是一个逻辑上递增分配的时间戳,确保事务提交的有序性。CTS通常通过RDMA操作获取,通常在几微秒内完成,并且在我们的测试中发现它不会成为瓶颈。为了有效管理集群中的所有事务信息,PolarDB-MP采用了一种分散的方法将这些信息分布在所有节点中。PolarDB-MP中的每个节点都保留了一小部分内存来存储其本地事务信息。节点可以通过RDMA远程访问其他节点的事务信息。下图展示了这种设计。每个节点都在事务信息表(TIT)中维护其本地事务的信息。TIT在管理事务方面发挥着关键作用,它为每个事务维护四个关键字段:pointer、CTS、version和ref。pointer是内存中事务对象的指针,CTS标记事务的提交时间戳,version用于识别同一槽中的不同事务,ref用来表示是否有其他事务在等待此事务释放其行锁。

0625-02.png

当一个事务在某个节点上开始时,会为该事务分配一个本地递增的唯一ID。然后,为此事务分配一个空闲的TIT slot。由于TIT slot可以被重用,version字段用于区分不同时期占用同一slot的事务,每个新事务都会使版本递增。为了在集群维度识别一个事务,PolarDB-MP将node_id、trx_id、slot_id和version组合成一个全局事务ID(g_trx_id)。有了这个g_trx_id,任何节点都可以通过RDMA从目标节点远程访问其他节点的事务的CTS。在PolarDB-MP中,在更新某个记录时,会将全局事务ID(g_trx_id)存储在该记录的元数据中。当事务提交时,如果当前事务所修改的记录仍然在缓存中,会将事务的CTS写入到这些记录的元数据,否则它们的CTS仍然是默认值(CSN_INIT)。在判断记录的可见性时,如果该记录的CTS字段是一个有效值,而不是初始值(CSN_INIT)时,可以直接从该记录的元数据中获取CTS。如果该记录的CTS没有填充,则需要使用TIT来获取该记录的CTS。首先需要从该记录的元数据字段中获取修改这条记录的事务ID(g_trx_id)。然后可以使用这个g_trx_id获取相应的TIT slot。如果TIT中的version与正在检查的g_trx_id的version不匹配,表明TIT slot已被新的事务重用,意味着原始的事务已经提交。在这种情况下,我们可以返回一个最小的CTS值以表明该记录对所有事务都是可见的。这是因为只有当一个TIT slot的CTS小于所有活跃事务的read view时,该slot才会被释放并重用。如果slot的version和当前g_trx_id的version匹配时,说明这个slot还没有被其他事务重用,我们可以直接从slot中直接获取CTS。

3.2 缓存融合(Buffer Fusion)

在PolarDB-MP中,每个节点都可以更新任何数据页(page),这导致不同节点之间频繁地传输数据页。为了实现高效的跨节点数据移动,PolarDB-MP提出了Buffer Fusion。每个节点可以将其数据页写入到Buffer Fusion的分布式缓存池(distributed buffer pool, DBP),随后其他节点可以从这个DBP访问其他节点修改过的页面。在这种情况下,页面可以在不同节点之间快速移动。下图展示了Buffer Fusion的设计。当DBP中存储了页面的新版本时,Buffer Fusion会将其他节点上的副本失效。这样,当这些节点需要访问该页面时,它们会从DBP中重新加载新的页面版本,确保所有操作都基于最新的数据状态进行,从而维持数据库的整体一致性。DBP的实现高度融合了RDMA,使得LBP和GBP之间的延迟非常低。

0625-03.png

3.3 锁融合(Lock Fusion)

Lock Fusion实现了页面锁(PLock)和行锁(RLock)两种协议。PLock类似于单节点数据库中的页面锁,确保对页面的原子访问以及内部结构的一致性。另一方面,RLock用于维护跨节点的事务一致性,遵循两阶段锁协议。PLock确保在并发访问时,只有持有锁的节点才能对数据库页面进行读写操作,这样可以避免数据冲突和不一致的问题。RLock则更加关注事务的一致性,允许更细粒度的数据访问控制,并使用两阶段锁协议。


PLock(页面锁):PLock主要用于维护物理数据的一致性,解决跨节点并发访问数据页的问题。PLock在节点级别管理,如下图所示。每个节点跟踪它持有或正在等待的PLock,并用一个引用计数来指示使用特定PLock的线程数。Lock Fusion维护所有PLock的信息,跟踪每个锁的状态。节点在对页面执行任何更新或读取之前,必须持有相应的排他锁(X)或共享锁(S)PLock。当节点需要一个PLock时,它首先检查其本地PLock管理器,查看是否已经持有所需的锁或更高级别的锁。如果没有,它会通过基于RDMA的RPC从Lock Fusion请求PLock。Lock Fusion会处理这个请求。如果存在冲突,请求节点将被挂起,待其 PLock 请求可以被满足时再唤醒。此外,当节点释放PLock时,会通知Lock Fusion,随后更新锁的状态,并通知其他等待该PLock的节点。

0625-04.png

RLock(行锁):PolarDB-MP在每一行数据中直接嵌入行锁信息,并且只在Lock Fusion上维护等待关系。对于每一行,会增加一个字段来表示持锁事务的ID。当事务尝试锁定一行时,它只需将其全局事务ID写入此字段。如果行的事务ID字段已经被一个活跃事务占用,则检测到冲突,当前事务必须等待。在PolarDB-MP中,当尝试更新一行时,必须已经持有包含该行的页面上的排他锁(X PLock)。因此,只有一个事务可以访问写入行的锁信息,只有一个事务可以成功锁定这一行。类似于单节点数据库,RLock协议也遵循两阶段锁协议,即事务持有锁直到事务提交。这种设计减少了锁信息的管理复杂性和存储开销,因为不需要在一个中心节点位置维护所有的锁状态信息。通过直接在行中嵌入锁信息,可以快速检测和解决锁冲突,从而提高了事务处理的效率和数据库的整体性能。


下面的例子展示了行锁的设计,节点2上的事务T30试图对一行数据施加排他锁(X-lock)。通过检查该记录的元数据发现,该行已被另一个事务(T10)施加了X-lock。随后,T30首先远程设置T10的元数据中的引用(‘ref’)字段。这个操作表明有一个事务(T30)正在等待T10释放锁。然后,T30与Lock Fusion通信,发送有关其等待状态的信息。这些信息被添加到Lock Fusion中的等待信息表中。一旦T10完成其事务并提交,它会检查其‘ref’字段。发现有其他事务正在等待时,T10通知Lock Fusion它已经提交。Lock Fusion在收到T10的通知后,检查等待信息表,然后通知T30,此时T30可以被唤醒并继续其处理。

0625-05.png

3.4 LLSN:保证redo log的顺序关系

在PolarDB-MP中,每个节点可以生成各自的redo log,导致多个节点对同一页面有不同的redo log。由于每个redo log记录了对特定页面所做的更改,崩溃恢复的关键在于按照它们生成的顺序回放同一页面的redo log,而来自不同页面的log可以按任意顺序应用。所以,我们不必维护所有redo log的全序关系,相反,只需要确保对应同一页面的redo log有序。


因此,PolarDB-MP引入了逻辑日志序列号(LLSN),为不同节点的日志建立了偏序关系,确保与同一页面相关的redo log按生成顺序维护。然而,LLSN不对来自不同页面的redo log保证先后顺序,这也是不必要的。为了实现这个设计,每个节点维护一个节点本地的LLSN,该LLSN在每次日志生成时自动递增。当节点更新页面并生成日志时,新的LLSN记录在页面元数据中,也分配给相应的日志。如果节点从存储或DBP读取页面,它会更新其本地LLSN以匹配访问页面的LLSN,前提是页面的LLSN超过了节点当前的LLSN。这确保了节点的LLSN与其访问的页面保持同步。随后,当节点更新页面并生成日志时,其LLSN递增,保证新的LLSN大于之前更新该页面的任何节点的LLSN。由于PLock设计,一次只有一个事务可以更新页面。因此,当一个页面在不同节点上依次更新时,LLSN有效地维持了日志的生成顺序。

4. 实验分析

我们用多种不同的工作负载和不同的配置测试了PolarDB-MP的性能。由于篇幅限制,这里只列出了两组实验结果,更多完整实验分析可以参见论文原文。我们首先对比了PolarDB和华为Taurus多主的性能。在下图中,y轴代表吞吐量,而每个条形图上的数字表示可扩展性(相对于单节点性能的归一化吞吐量)。PolarDB-MP在单节点时与Taurus-MM的性能相当。然而,PolarDB-MP在多节点中的优势变得明显。例如,在read-write和read-only工作负载中,PolarDB-MP在八节点集群中的吞吐量分别是Taurus-MM的3.17倍和4.02倍

0625-06.png

我们进一步在只有10%的共享数据时对比了PolarDB-MP和Aurora-MM的性能。由于Aurora-MM仅支持最多4个节点,因此其8节点的结果被省略。即使在共享数据比例较低的情况下,Aurora-MM在read-write工作负载中从2节点增至4节点也没有显示出任何的性能提升;在write-only工作负载中,2和4节点的表现甚至不如单节点,这归因于Aurora-MM使用乐观并发控制。在这些冲突较轻的场景中,尽管Taurus-MM表现出比Aurora-MM更高的可扩展性,但它仍然落后于PolarDB-MP,尤其是在八节点集群中。在write-only负载下,4节点时,PolarDB-MP的扩展性是Aurora-MM的5.4倍。8节点时,PolarDB-MP的扩展性是Taurus-MM的2.2倍。

0625-07.png


更多细节可参阅论文原文,点击下载

PolarDB多写架构已经上线阿里云,欢迎使用和测试。点击前往


作者介绍
目录

相关产品

  • 云原生数据库 PolarDB