我跟大家分享一下CynosDBfor PostgreSQL分布式存储的一些关键性技术。这一页是我们数据库历史上主要出现的几个集群模式,目前市面上对这种交易性数据库是现在最主流的两个方向,一个是类似谷歌 Spanner,另一个是类似亚马逊 Aurora。

CynosDB分布式存储的核心原理_java

现在的趋势通常讲NewSQL,我对NewSQL的理解是这样的,既要NoSQL的大规模、高吞吐量、易于扩缩容、自动容错等优势,又要SQL的通用性、支持复杂查询和分布式事务。我们CynosDB就能把这两个对立的矛盾解决好,前面都讲了CynosDB最核心的思想,计算存储分离,我这里再补充一下CynosDB计算存储分离,到底是基于什么样最核心的原理。


CynosDB分布式存储的核心原理_java_02


我们传统的数据库像左边这样,就是要保证数据的一致性,采用这种日志先写的技术,所谓日志先写就是说写数据本身之前要把修改内容作为日志先持久化,主要为了一是保证我们发生故障恢复的时候,能保证数据能恢复出来。而是写日志是顺序IO,而写数据本身是随机IO,在以前机械版为主流的时候,用户请求的关键路径上写日志显然比写数据本身要快。也就是说要写两遍数据,一是日志,二是数据页。


本质上日志就能够描述整个完整的信息,什么意思呢?我一个数据库不写数据页只写日志,只要保证数据库第一条开始的是所有日志都有,这样我理论上是能恢复出任何一个时间点的任何一个数据页的,但我们数据库不会这么做,为什么?一是因为这个恢复的过程太漫长了,因为你要在计算页里面把这些数据进行redo,数据库实现者并不会这么做。如果我们有这么一个存储系统既能存日志又能把日志回放做了,既能存日志又能吐出数据库需要对新的版本的页面,那对我们数据库的计算来说就不需要做数据页的持久化,也就是我只需要把日志写下就好了,存储会自动的把日志重放出新的一页出来,这样计算只需要写日志,而不需要写数据页了,这是我们计算存储分离CynosDB一个最核心的思想。



下面是讲整个存算分离的架构,我们存储是分布式存储,因为你的存储要突破这种单节点限制,第二存储是一个集群。既然是一个集群的话,这个存储集群对DB的实例来说,能抽象虚拟盘一样,要能有这么一个抽象。所以整个架构里面,底下存储是一个一个的存储节点,存储节点上面会有一个驱动式客户端,就是我们存储的客户端,就负责把各个分片抽象成一个逻辑上的盘,或者更进一步成文件系统。这个存储的客户端,就是一个程序包。



现在讲一下我们定义的接口,想强调的是CynosStore是存储计算接口很清晰的一个系统。CynosStore有两层,块设备层、文件系统层,就是我们先对上抽象出一个磁盘出来,就像网盘、云盘一样抽象出一个磁盘出来,这个盘上我们自己又重新实现了一个文件系统,又能给DB提供一个文件系统的抽象。也就是说上面的数据库既可以采用我们这种块设备层的抽象,也可以使用文件系统这一层的抽象。现在我们的PostgreSQL是使用文件系统这层的抽象,文件系统这一层是完全跟底下的分片分布信息解耦的,也就是说我不用感知到这个逻辑的磁盘它的数据是如何在各个存储地方进行分片的。



另外一点是我们除了支持这两个层次的存储之下的,有一个新的能力,数据库里面,不管是MySQL或者是PG,都会有一个写日志,日志逻辑上是一个序列嘛,所以这个地方一定会有锁,我们刚刚同事也讲到一定会有一个锁或者优化成多个锁,各个并行的写操作,一定会在日志这一块有一个锁,会有锁的占有和日志的排序,这个过程我们完全把它从DB给下沉,下沉到存储层,下沉到CynosStore里面。对于DB来讲,实际上我每一个用户的进程不用管日志是怎么样序列化的,怎么样去做保证日志序列的问题,这个过程全在存储中实现了。我们每一个DB的进程,我只管往CynosStore写日志就好了,至于多个后台进程同时去写,怎么去写日志,怎么序列化是完全存储层做的,这样给DB层做了一个非常好的抽象。




这个大家都看过,最主要的一个核心点是MTR,所谓的MTR,我们知道数据库有事务的,这个事务在我们的日志层面上,可能会被打散。但对我们存储来讲,我是不会感知到数据库的事务,但我要给用户,给DB提供一个原子操作,这个原子操作就是我有多个页写,比如对于一个BTree分裂,我可能会写多个页,要么成功要么失败,我的存储也要提供一个事务,这个事务就是一个MTR,一个MTR的日志号一定是连续的,所以我们存储日志就是一个MTR挨着一个MTR,一个一个MTR串起来的,每一个MTR最后就是刚才讲的VDL。


大家知道我们日志一定会有一个拆分,拆分之后下发到SN节点。我们是一个异步的下发,也就是说日志同时下发到多个存储节点,存储节点返回ACK给我们的客户端。这种顺序写下去,但ACK不一定是顺序的,所以我们在客户端一定会有一个排序,就是有这么个逻辑保证这个日志是连续的,并且能支持中间状态时日志的空洞。


讲了这么多,就是想说一点CynosStore是个什么样的东西,一句话就是,基于日志异步写的存储系统。


它的特点,第一是非对称读写,(写的是日志、读的是数据)。第二是异步写,同步读。第三并发写入、日志串行化。第四是支持两层,块设备层、文件系统层。第五能接入任何基于日志先写的存储系统。第六是存储分布式化。


CynosDB还有一个功能就是重做日志的能力,对于一个写来讲,首先第一步就要收日志,日志拿到之后,刚刚MySQL的同事也讲了,这种系统里面,批量、异步、流水线就是性能优化三板斧。拿到日志之后,第一个要把日志持久化,第二个是把日志发送到raft的follower,就是通过Raft把日志同步到其它节点去。这两个动作做完之后,如果用三个副本的话,本地就是写日志,异地副本就是挂日志,挂上去之后就可以返回,就可以给客户端发ACK,表示这一步成功了。其实不挂直接返回也可以,这样客户端去读的时候,如果这个日志没挂列我会判断一下,我去等待你挂链,这两种操作都是可以的,这是用户时延的一个关键路径。


后面还有几个很重要的线程,第一个是对日志做Applier,第二个是对日志做回收,这些日志的修改一定要持久化之后才可以回收。我们读的时候,我们这个页面有一个缓存,读的时候我们读也是一样的,会提供一个多版本的读写能力,你读的时候会带一个日志号,当然这个日志号是有范围的,也就是日志如果回收掉的,就小于这个最小日志号的版本页是读不到的,这个日志还没有在buffer中重构出来,我会在上一个日志页里面把它重构一遍。


CynosDB分布式存储的核心原理_java_03


下面讲一下我们怎么做快照备份的,叫回档。我们会持续将我们的数据备份,让用户构建出过去某个时间点的一个快照出来,其实这个回档思想主要是,这分两个,一个是备份,一个是恢复。备份就比较简单一点,一个是数据的全量快照,一个是日志的增量备份。每一个分片是独立的,各自去备份,备份是感知不到自己数据库的,我只知道这个分片的逻辑。


麻烦一点的是恢复,我们会用这个时间点在我们的日志中找一个对应的LSN,我们会找一个一致性的,每一个分片都回到的同一个点,必须通过这个时间点找同一个恢复的点。然后Repalyer将所有的SG重放到指定LSN,MetaCenter建立新的pool并恢复出整个pool。关键问题是如何根据时间戳找到各个分片可以恢复的统一时间点,第二个问题是如何确定当前时间的分片数目。


CynosDB分布式存储的核心原理_java_04


一写多读,细节不讲了,我补充一点,我们从节点跟主节点的逻辑不太一样的,每个从节点,看到这个时间点或者看到这个快照是不一样的,同时我们日志也不断再往这个从节点发,所以这个重节点逻辑比主逻辑更复杂一些。一个典型的就是重节点提供一个多版本的能力,其实我们的主节点,多版本主要是在客户端以下,DB看到可能都是一个版本,但我们主节点可能DB需要看到多版本。另外是,因为我们存储是提供一个独立的成体系的系统,所以我们这个日志多版本,日志页和Jouranal buffer,这是文件系统的buffer,数据页buffer还是在DB里面,这个地方有点复杂。


CynosDB分布式存储的核心原理_java_05


怎么衡量分布式存储的性能?我们自己设计了一个benchmark,当然这里面场景很多。通过CynosStore Driver异步写入日志,测量指标是每秒持久化的日志数量,看一下这个图是结果。我们存储日志是完全跟数据库解耦的,也就是最原始的几个日志,我们的日志有一个SetByte,写数据页的某一个字节或者延续多个字节,第二个是Setbit,就是我修改某一位。

 

CynosDB分布式存储的核心原理_java_06

  

   

Q:这么大一个数据,应该是提供给我们很多用户用,是每一个人提供租户,还是每个用户只一套数据库?

A:我们底下这个池子,就是一个大池子,一个用户来了之后,我们创建一个实例,这个实例对应到底下的逻辑,也就是我布的实例对应的磁盘,类似一个云盘的东西,它在底下是交错在一起的。


Q:我理解的话,每个租户可能分到某一个节点某一个片上。

A:不是,某一个租户可能有很多个盘。

Q:对,上面层抽象出机制。那如果某一个东西有问题的话,其他的组件可以自动切换过去对不对?

A:对的。


Q:你好,咨询一个小的细节,在raft的主节点挂了,会不会导致上面的DB就会出问题?

A:如果某个节点挂了导致这个节点发生故障,那么会自动切换,因为我们上面的数据库是一直写着字的,一旦分片发生切换客户端就能感知到,因为我们上面的写日志跟上面的刷日志是异步的,也就说我们有独立的线程在客户端往日志里刷,因为你日志一旦切换以后,你的主节点切换之后这部分日志没有等待确认的这部分日志,在新的主节点上有可能要重发一遍,在上面看应该是这个点上时延会变大一些。


Q:我想问一下增加segment和缩减的时候大体是什么样的?

A:我们自己写了一个文件系统,这个跟传统是不一样的,我们这个地方当你扩容的时候,我新增加一个分片,这个分片首先先做一个分片格式化,格式化也是写日志,实际上我们格式化的过程跟普通写日志没什么分别,也就是说看到我们这一个元数据中心,这个元数据中心就负责通过剩余的容量还有多少,是否需要扩容。在存储客户端这一层会感知到扩容,比如你扩了一个分片或者两个分片,我感受之后就把这两个分片进行格式化。之后文件系统和新的页的分配,才会新分配到新的分片上去的。

Q:映射这一层的问题。

A:因为我们是分块的,这个映射不会变的。

Q:比如正常扩容的时候就正常往下走,如果缩容的时候。

A:缩容的问题并不在pool,而是在文件系统缩容,因为有些页是在要缩容块里。我们缩容只在后面,不会是在中间。