cynosdb是腾讯云自主研发的新一代关系型云原生数据库,既拥有分布式设计的低成本优势,又具有集中式的易用性。
cynosdb采用存储计算分离设计,满足各种规模应用场景需求。
前言
cynosdb是新一代分布式数据库,100%兼容mysql和postgresql,支持存储弹性扩展,一主多从共享数据,性能更是超越社区原生mysql和postgresql。cynosdb采用share storage架构,其弹性扩展和高性价比的基石则是cynosdb file system(简称cynosfs):一款腾讯云自研的用户态分布式文件系统。本文旨在从整体上讲述cynosdb和cynosfs的核心架构设计。
挑战与应对
cynosdb是公有云原生架构的,其核心思想是在资源池化的基础上实现公有云高性价比、高可用性以及弹性扩展等诸多优势。实现资源池化的最大技术挑战是高效、稳定的弹性调度能力,该能力也是公有云产品高可用性的基石。计算、网络和存储是公有云三大类iaas产品,前面两类都有比较成熟的弹性调度架构,唯独存储产品例外。究其根本还是数据调度的代价太高。数据库是典型的计算 存储的产品,要做到资源池化需要:
存储计算分离:这样计算资源(主要包含cpu和内存)就可以使用现有成熟的容器、虚拟机等技术实现资源池化
分布式存储:将数据分割成规格化的块,引入分布式调度系统,实现存储容量和io的弹性调度,从而实现存储资源的池化
那么对于数据库产品来说,是否有现成架构可以很好的满足以上两个需求呢。我们发现虚拟机 分布式块存储(云盘)的方案非常合适,目前主流公有云平台也都有非常成熟的产品。腾讯云今年早些时候推出的mysql基础版以及aws上的rds都是基于这种架构。图1简单描述了这种架构:
img
图 1
但该架构有如下不足:
网络io重:可以看到就1个数据库实例,就有大量数据需要写到云盘,这些数据主要包括:wal log、页数据、防止页部分写的double write或者full page write。除此之外,云盘还要将这些数据做多个备份
主从实例不共享数据:一方面浪费了大量存储,另一方面进一步加重了网络io
这些不足导致基于该架构的数据库产品在系统吞吐能力上无法与基于物理机部署的主从架构竞争,而且延迟也受到很大的挑战,这对oltp类业务的影响非常明显。同时每个数据库实例都单独拥有一份(实际可能是2-3份)存储,成本也很高。
cynosdb针对这两个不足,采用了如下设计:
日志下沉: wal log从逻辑上已经包含了数据的所有变动,cynosdb将wal log下沉到存储层,数据库实例就只需要将wal log写到存储层,不再需要写页数据(页数据以及double write或full page write),同时这个wal log也作为raft协议的日志来完成多个数据备份的同步,进一步减少了网络io。
主从实例共享数据:cynosdb的主从实例共享共一份存储数据,进一步减少了网络io,同时极大的减少了存储容量
循着这两个解决思路,我们对传统基于云盘的架构进行优化,就有了cynosdb如下架构:
图 2
图中组件包括:
db engine:数据库引擎,支持一主多从。
distributed file system:用户态分布式文件系统,主要提供分布式的文件管理,负责将文件的读写请求翻译为对应的block读写
log/block api:storage service提供的读写接口,对于读写请求有不同处理:
写请求:将修改日志(等价于wal log)通过log api发送到storage service
读请求:直接通过block api读取数据
ldb cluster manager:负责一主多从db集群的ha管理。
storage service:负责日志的处理、block数据的异步回放、读请求的多版本支持等。同时还负责将wal log备份到cold backup storage
segment(seg):storage service管理数据block和日志的最小单元(10gb),也是数据复制的实体。图中同样颜色的3个seg实际存储同一份数据,通过一致性协议(raft)进行同步,我们叫做segment group(sg)。
pool:多个sg从逻辑上构成一个连续的存储数据block的块设备,供上层的distributed file system分配使用。pool和sg是一对多的关系。
storage cluster manager:负责storage service以及segment group 的ha调度,以及维护pool和sg的对应关系。
cold backup service:接收到wal log后,进行增量备份,根据增量备份,可以灵活的生成全量备份和差异备份
可以看到,上面中除了db engine和db cluster manager外的所有模块构成了一个与数据库引擎无关的用户态分布式文件系统,我们命名为:cynosfs。
日志下沉和异步回放
这里cynosdb借鉴了aws aurora论文中日志即数据库的思想,将wal log下沉到storage serivce,计算层只写wal log,而由storage serivce异步地将wal log应用到对应的block上,在减少写io的同时,也是storage serivce提供mvcc读能力的基础。而mvcc是主从节点共享存储的基础。storage serivce中的数据和日志都是以segment为单元进行管理的,图3描述了写的基本流程:
图 3
1.接收到更新日志,写到磁盘,并行发起第2步
2.发起raft日志复制
3.raft多数派提交成功,返回写日志成功
4.异步的将日志记录挂载到对应数据block的更新链上
5.合并更新链中的日志记录到数据block
6.将日志备份到冷备系统
7.回收无用日志
这里第4步会对更新的数据block形成1个更新链,从而支持数据block多个版本的读,进而提供mvcc能力。通过控制第5步的合并进度,就可以控制mvcc的起始窗口。
同时cynosdb还将页面crc等操作放到了存储服务中,充分利用存储服务器的cpu资源
mvcc实现
cynosfs的mvcc能力是cynosdb一主多从架构的基础,本小节会详细描述其运行机制。
首先我们引入一系列概念:
mini-transaction(mtr):就像数据库事务用来保证事务的acid特性,mtr用来保证磁盘上页数据的acid特性。举个例子,1个b 树的插入操作,在造成节点分裂的情况下,最多会修改3个数据页,这3个数据页的修改需要当做1个事务来处理,这就是mtr。数据库实例在磁盘上的数据是按照mtr为最小单元进行更新的
write ahead log(wal log):关系数据库常用的保证数据一致性的技术。wal log是一个增量日志流水,里面的日志记录(log records)保存了页面修改操作。由于cynosfs是与具体数据库引擎无关的分布式文件系统,其wal log中记录的是对数据block的二进制修改,是物理日志
log sequence number(lsn):wal log中每条log record都会有1个唯一标识的序列号,从1开始,按log record产生的连续单调递增。
consistency point lsn(cpl):一个mtr可能对应多个磁盘数据页的修改,相应的会产生多条日志记录,这批日志记录中的最后1条(也就是lsn值最大的1条)标识为cpl,这条日志记录对应了一个磁盘上的数据状态,该状态表示1个mtr的完整应用,也是mvcc的读一致性状态
segment group complete lsn(sgcl):1个数据库实例的数据页会分布到多个sg上,因此每个sg只会持有部分日志记录,scl表示该sg已经将所有小于等于scl的日志记录持久化到磁盘上了。对于cynosfs来说,因为sg采用了raft协议,scl就是raft的commitindex。
pool complete lsn(pcl):表示构成该pool的所有sg已经将小于等于pcl的日志持久化到磁盘上。因为lsn是连续单调递增的,这也要等价于中间没有空洞
pool consistency point lsn(pcpl):不大于pcl的最大的cpl。逻辑上表示:已经持久化到磁盘的可提供mvcc读一致性的点。
写
首先主实例对所有数据页的修改都经过distributed file system的转换,形成pool级别的日志,并附带连续单调递增的lsn,分发到不同的sg。sg会在完成raft commmit后推进自己的sgcl并返回,主实例根据所有sg的完成情况,推进pcl和pcpl。下面我们通过2幅图描述pcl和pcpl的推进过程:
图 4
首先该pool由2个sg构成,每个sg中有3个segment,存储同一份数据。当前有lsn为1~8的日志已经持久化到2个sg中,其中1、3、4、7在sg a上,2、5、6、8在sg b上。这8条日志属于3个mtr,分别是:mtr-x(1、2、3),mtr-y(4、5),mtr-z(6、7、8),其中3和5都是cpl,表示mtr-x和mtr-y对应的日志已经全部持久化了,8不是cpl,说明mtr-z后续还有日志记录。按照上面的定义,此时pcl为8,而pcpl为5
图 5
图5中,mtr-z的1条日志cpl9被持久化到sg a,按照定义,此时pcl和pcpl都更新到了9。
读
这里主要指从实例的读流程。从实例具有自己的内存数据,在主实例更新了数据后,需要同步更新从实例的内存数据,否则会出现不一致的情况。主实例会将日志记录以及最新的pcpl值推送给从实例。因此从实例的pcpl值会较主实例有所延迟。这个pcpl值决定了从实例上所有事务能读到的最后1个mtr。同时为了减少从实例的延迟,主实例可能不等pcpl值推进先把日志推送到从实例,因此从实例需要保证大于本地pcpl值的那些日志不被应用。从实例对这些更新日志的处理方式有两类:如果对应修改的数据页在buffer pool中,则应用此日志更新到对应数据页,否则直接丢弃。最后从实例上的读事务在每次访问数据页时(不管直接从buffer中获取到,还是从storage service获取),因为可能一次读入多个页,所以需要取当前的pcpl值为read point lsn(简称:rpl),用rpl达成到一致性读(因为prl是一个pcpl值,其一定能保证mtr的完整性)。cynosdb支持一主多从,每个从实例上都会有很多读事务,这些读事务都具有自己的rpl,所有这些rpl中最小的我们称之为min-rpl(简称:mrpl),那么在mrpl和pcpl之间可能会有多个cpl点,不同读事务的rpl可能在这个区间的任何1个cpl点上。因此storage service要能支持读取在这个区间内任意cpl点的数据,也就是mvcc。cynosdb使用数据页 更新日志链的方式来实现。
最后让我们回到图3,sg需要定期回收无用的日志记录,我们可以看到,所谓无用的日志记录就是其lsn值小于mrpl的那些。而大于等于mrpl的那些日志因为要用来支持mvcc,所以必须保留。
事务提交
一个数据库事务由多个mtr组成,最后1个mtr的最后1条日志的lsn(必然也是个cpl)我们称之为commit lsn(简称:clsn)。为了提升效率,日志到storage service的推送采用异步流水线的方式,同时按照sg进行分组并行推送。全局来看,因为是并行的,所以可能会有乱序,但推送到某个sg上的日志一定是按照lsn单调递增的(不连续)。随着pcpl的推进,一旦pcpl值大于某个事物的clsn,该事物就可以完成commit操作了。
崩溃恢复
首先,cynosdb会持续的保存主实例的pcpl,这个pcpl的备份值我们称之为:last pcpl(简称:l-pcpl)。cynosdb需要保证l-pcpl值的更新足够快,以确保那些大于l-pcpl的日志记录没有被回收。在此前提下,我们通过如下步骤实现崩溃恢复:
图 6
首先,新的主实例获取到l-pcpl值为3。该实例由2个sg组成,其中sg a中已经持久化了日志(1、4、5),而sg b中持久化了日志(2、3、6、8)。
图 7
主实例向2个sg读取大于3的所有日志lsn,获取到sg a的4、5以及sg b的6、8。这样形成了3、4、5、6、8的日志流。按照定义,因为没有lsn为7的日志,所以8是无效的,其次6不是cpl,所以6也是无效的,这样就计算出了最新的pcpl值为5。然后将新的pcpl值推送给所有的从实例。
图 8
最后,主实例将新的pcpl=5推送给所有sg,以便其将大于5的所有日志都清理掉。
经过上面三步,整个数据就回到了pcpl=5这个一致性的点。主实例和从实例就可以提供服务了。
持续备份
实际上,在cynosdb中,几乎不需要全量的备份操作,只要保证sg的wal log在回收前,被增量保存到了冷备系统中就行了。因为逻辑上wal log是包含了所有数据的修改操作,我们可以使用增量的wal log,按照客户需求,生成全量备份和差异备份。而这些操作都可以是由另一套独立的系统基于增量备份数据离线处理的,对cynosdb的线上系统不会造成任何的影响。另外我们还需要对实例的状态配置信息进行备份,以便恢复时能获取到相关信息。