以下文章来源于阿里云开发者 ,作者陈浩、章颖强
引言日前,在加拿大温哥华召开的数据库领域顶会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的全局强一致读。
总体架构PolarDB-SCC采用了基于共享存储的一写多读架构,包含一个读写(RW)节点和多个只读(RO)节点。RW和RO节点可以进一步连接到代理节点上。代理节点可通过将读请求分发到RO节点并将写请求转发到RW节点来实现读写分离,从而提供透明的负载平衡。RW节点和RO节点通过基于RDMA的网络连接,可快速传输日志和获取时间戳。PolarDB的事务系统是一套重新设计的基于时间戳的事务系统,其完全重构了MySQL基于活跃事务数组的传统事务系统,以支持分布式的事务扩展和提升单机性能,这是PolarDB-SCC的基线。PolarDB-SCC的核心设计主要是以下三个:▶︎ 线性Lamport时间戳。由于最新修改的时间戳保存在RW节点上,因此RO节点必须在每次处理请求时从RW节点获取该时间戳。虽然RDMA网络速度很快,但如果RO节点的负载很重,开销仍然很大。为了减少时间戳获取的开销,我们提出了线性Lamport时间戳。在线性Lamport时间戳的基础上,RO节点从RW节点获取时间戳后,可将其存储在本地。任何早于该时间戳到达RO节点的请求都可以直接使用本地存储的时间戳,而不用从RW节点获取新的时间戳。当RO节点负载较重时,这可以节省许多获取时间戳造成的开销。▶︎ 分层的细粒度的修改跟踪。在RW维护全局、表级、页面(page)级的更新时间戳。当RO处理请求时,首先获取全局的时间戳,如果全局时间戳比RO回放日志的时间戳小,RO不会立即进入等待,而是继续比较请求需要访问的表、page的时间戳。只有当需要访问的page的时间戳仍然不满足时,才会等待日志回放。这样可以避免一些不必要的日志回放等待。
▶︎ 基于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。我们可以得到这几个事件的关系:e2TS3rwe3。r1在t1时刻到达。通过在RO给每个事件分配一个时间戳,可以确定同一个RO上不同事件的先后关系。如果t1在t2之前,我们可以得到,e1e2TS3rwe3。也就是说r2拿到的时间戳,其实已经反应了r1到达之前的所有更新,所以r1就可以直接使用r2的时间戳,而不必去拿新的时间戳。基于这个原则,RO每次拿到RW的时间戳时,都会把这个时间戳保存在本地,并且会记录获取到该时间戳的时间,如果某个请求的到达时间早于本地缓存时间戳的获取时间,则该请求可以直接使用该时间戳。
2. 分层的细粒度的修改跟踪
为了在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的日志传输
如果RO总是通过共享存储获取日志,会导致日志的传输延迟变慢,如果RW用TCP/IP的方式给RO发送日志,不仅会浪费RW/RO上的CPU资源,同时延迟也比较高。因此,在PolarDB-SCC中。RW通过单边RDMA的方式远程将日志写入到RO缓存,该过程不需要RO的CPU参与,同时延迟也很低。具体实现如下图所示,RO和RW都维护了相同大小的log buffer。RW的后台线程会将RW的log buffer通过RDMA写入到RO的log buffer。由于RO的CPU并不参与日志的传输,所以RO是无法感知其缓存内哪些日志是有有效的,而RW也无法感知RO上哪些日志已经回放完了。所以日志传输协议需要一些额外的控制信息来保证不会出现覆盖的情况。由于篇幅限制,具体细节这里就不具体展开了,可以参考论文。
实验分析PolarDB-SCC已经正式在PolarDB MySQL上商业化了,并且已经在线上运行了一年多。所以我们的实验都是在阿里云的公有云环境上进行的。大部分实验都是使用8c32g的实例。因为PolarDB-SCC是在PolarDB-MySQL上实现的,所以我们的主要比较对象也是PolarDB。我们主要和PolarDB的几个不同配置进行比较:
● PolarDB-default:这种配置所有请求都是在RW上处理,整个系统是强一致的。
● PolarDB-read-write:使用基本的read-write方案在RO上实现强一致读。
● PolarDB-stale-read:RO直接处理请求,可能返回过期的数据。
1. 整体性能
上图展示了在SysBench read-write负载下,不同压力时各个系统的性能。在低负载时(比如16,32线程),由于系统没有达到瓶颈,不同系统性能差不多,即使所有请求都在RW上处理,也是可以的。随着压力增加。PolarDB-default和PolarDB-read-write的性能有小幅增长,但很快就饱和了。然而,PolarDB-SCC始终可以保持和PolarDB-stale-read相似的性能。所以,PolarDB-SCC不仅可以实现强一致,而且性能几乎没有损失。
2. 性能breakdown分析
我们进一步测试PolarDB-SCC不同设计带来的性能提升。LLT表示线性Lamport时间戳,HMT表示多层细粒度的修改追踪。从中可以发现,PolarDB-SCC的各个设计对总体的性能提升都是有帮助的。并且RO数量越多,性能提升越明显。
3. PolarDB-SCC和Serverless的结合PolarDB-SCC可以通过代理提供一个统一的强一致的endpoint。用户可以将应用连接到该endpoint,并在后端动态增加或减少RO节点的数量,而无需对应用进行任何更改。PolarDB的Serverless功能能够针对动态的工作负载动态调整RO节点的数量。与PolarDB-SCC集成后,应用程序看起来像是只连接到一个拥有动态资源的RW节点,同时保证了强一致性。下图中测试了这种场景。在该测试中,工作负载在300秒、600秒和900秒时变得更重,数据库集群会在后台动态添加更多RO节点,当集群中添加更多RO节点时,性能会以几乎线性的方式迅速提高。
总结实现从(只读)节点的强一致性一直以来都是数据库业内难以突破的技术难题。PolarDB-SCC颠覆了传统的主从复制架构,提出了一种全新的数据库架构。新架构利用RDMA的多种算子全面重构了主从节间的数据通信模式,通过追踪细粒度的数据修改以及设计新的时间戳方案,并融合基于时间序的新一代事务系统实现了高性能全局一致性读。目前该架构已在PolarDB上线,详见:https://help.aliyun.com/zh/polardb/polardb-for-mysql/user-guide/scc