VLDB论文解读|PolarDB MySQL高性能强一致集群核心技术详解
以下文章来源于阿里云开发者 ,作者陈浩、章颖强
引言日前,在加拿大温哥华召开的数据库领域顶会VLDB 2023上,来自阿里云瑶池数据库团队的论文《PolarDB-SCC:A Cloud-Native Database Ensuring Low Latency for Strongly Consistent Reads》,成功入选VLDB Industrial Track(工业赛道)。
论文中,PolarDB-SCC提出了一个全局强一致的主从架构的云原生数据库。目前该架构已在PolarDB架构中上线一年有余,是业内首个在业务无感知情况下实现全局一致性读的主从架构云原生数据库,解决了一直以来海量客户的一致性痛点。
论文背景经典的关系型数据库架构包括云原生关系型数据库通常采用主从(“一写多读”)架构,即由一个读/写(RW)节点和一个或多个只读(RO)节点组成。在这种设计中,RW的数据更新通常是通过日志的方式传输到RO。RO通过回放日志更新其内存数据。但是,RW到RO的日志传输以及RO的日志回放通常是异步的。因此,用户通常要么接受这种弱一致性(最终一致性或会话一致性),即从RO节点读取的数据可能是过期数据,要么必须容忍读取延迟方面的性能下降,因为强一致性需要等待日志的传输和回放。市场上大多数商业数据库和云数据库,通常会优先选择力保性能而非强一致性。然而,很多业务场景都存在强一致性的需求,同时我们也收到了许多用户的反馈,希望能够提供高性能的RO强一致读。比如,在电商场景中,如果用户下完单后,查询订单的读请求路由到了RO上,如果没有强一致,用户可能查询到订单没有提交或者订单没支付这种不一致状态。在某些微服务场景中,用户有多个微服务,每个服务都连接到同一个数据库集群,多个微服务之间有相互依赖关系,如果数据库集群不能提供强一致性,上层的多个微服务就无法有效的工作。如果不能保证RO的强一致性,这些应用就无法在RO节点处理任何请求,其资源无法得到充分利用。针对此问题,阿里云数据库团队设计了新的基于主从架构的云原生数据库,PolarDB-SCC,在几乎没有性能损失的情况下实现了全局强一致。
现状分析
现有的基于共享存储的云原生一写多读数据库通常都是采用异步的日志传输和日志回放,导致RO上会返回过期的数据,只能保证最终一致性。我们测试了几款数据库,发现其RO节点都会返回过期的数据。在我们的测试中,首先在RW节点写入数据,然后过一小段时间后在RO节点读取该数据,统计在RO节点上读到过期数据的比例。结果如下所示,其中DB-A和DB-B为两个商业的云原生数据库,这里做了匿名处理。PolarDB指的是没有SCC功能的形态。QueryFresh是来自学术界的一个解决方案。我们可以发现只有PolarDB-SCC可以完全避免读到过期数据。

为了实现RO节点的强一致性,现有的常用算法主要是commit-wait和read-wait。在commit-wait中,RW在事务提交之前需要等待该事务的日志已经传输到所有RO节点,并且已经回放完。这种做法严重影响了RW节点的性能。Read-wait算法中,在RO节点处理请求时,首先需要获取RW节点当前的最大提交时间戳,然后等RO节点的日志回放到该时间戳后,再实际处理请求。这种做法会使RO的处理延迟变大。
为了实现低延迟的全局一致性,我们重新设计并实现了PolarDB-SCC,其基于与RDMA的深度融合,采用了交互式多维度主从信息同步机制取代了传统的主从日志复制架构,并通过巧妙的设计,减少RO节点获取时间戳的次数,同时避免了不必要的日志回放等待,最终在几乎没有性能损失的情况下实现了RO的全局强一致读。总体架构

▶︎ 基于RDMA的日志传输。PolarDB-SCC采用了单边RDMA的接口来实现RW到RO的数据传输,极大地提高了日志传输速度,同时减少了日志传输时带来的CPU开销。
方案实现1. 线性Lamport时间戳RO在处理读请求时,必须获取RW节点的最新时间戳。如果RO节点的负载很重,一方面会占用更多的网络带宽,另一方面会给RO的请求带来更大的延迟。而通过设计线性Larmport时间戳,可以在高并发时让一个时间戳服务多个请求。其核心思想是,如果一个请求发现在其到达时间之后已经有其他请求从RW获取了一个时间戳,那么它可以直接重用该时间戳,而不需要再向RW获取一个新的时间戳,这样仍然可以保证强一致性。可以用下面的例子来证明。

上图中一个RO上有两个并发的读请求r1和r2。r2在t2时向rw发送读取时间戳的请求,在t3时刻拿到了RW的时间戳TS3rw。我们可以得到这几个事件的关系:e2TS3rw
e3。r1在t1时刻到达。通过在RO给每个事件分配一个时间戳,可以确定同一个RO上不同事件的先后关系。如果t1在t2之前,我们可以得到,e1
e2
TS3rw
e3。也就是说r2拿到的时间戳,其实已经反应了r1到达之前的所有更新,所以r1就可以直接使用r2的时间戳,而不必去拿新的时间戳。基于这个原则,RO每次拿到RW的时间戳时,都会把这个时间戳保存在本地,并且会记录获取到该时间戳的时间,如果某个请求的到达时间早于本地缓存时间戳的获取时间,则该请求可以直接使用该时间戳。
为了在RO上实现强一致读,RO需要首先获取RW当前的最大时间戳。然后等待RO上的日志回放到该时间戳,最后才可以实际处理读请求。然而实际上,RO在等待日志回放时,可能当前请求的数据已经是最新的了,并不需要再等待日志回放。为了避免这些不必要的等待时间。PolarDB-SCC使用了更加细粒度的修改追踪。在RW上维护三层修改信息:全局的最新修改的时间戳,表级的时间戳和页面(page)级别的时间戳。
RO在处理读请求时,首先获取RW上全局时间戳,如果全局时间戳不满足条件,可以进一步获取当前所访问的表的时间戳,如果仍不满足,进一步检查需要访问的对应的page的时间戳是否满足条件。只有当page的时间戳仍然比RO日志回放时间戳还大时,RO才需要等待日志回放。
RW上三个层次的时间戳都是放在内存哈希表中的,为了减少这部分的内存使用量,会存在多个表或page的时间戳放到同一个位置,但是只允许大的时间戳替换小的时间戳,这样,即使RO拿到一个大的时间戳也不影响一致性。具体设计如下图所示,TID和PID分别表示表和page的ID。RO拿到对应的时间戳都会按照前面提到的线性Lamport时间戳的设计将其缓存到本地,可以给其他符合条件的请求使用。
3. 基于RDMA的日志传输
实验分析PolarDB-SCC已经正式在PolarDB MySQL上商业化了,并且已经在线上运行了一年多。所以我们的实验都是在阿里云的公有云环境上进行的。大部分实验都是使用8c32g的实例。因为PolarDB-SCC是在PolarDB-MySQL上实现的,所以我们的主要比较对象也是PolarDB。我们主要和PolarDB的几个不同配置进行比较:
● PolarDB-default:这种配置所有请求都是在RW上处理,整个系统是强一致的。
● PolarDB-read-write:使用基本的read-write方案在RO上实现强一致读。
1. 整体性能
总结实现从(只读)节点的强一致性一直以来都是数据库业内难以突破的技术难题。PolarDB-SCC颠覆了传统的主从复制架构,提出了一种全新的数据库架构。新架构利用RDMA的多种算子全面重构了主从节间的数据通信模式,通过追踪细粒度的数据修改以及设计新的时间戳方案,并融合基于时间序的新一代事务系统实现了高性能全局一致性读。目前该架构已在PolarDB上线,详见:https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/scc