TDSQL-C for PostgreSQL 主从架构详解

TDSQL-C PG 版整体架构

在介绍整体架构前,先说一下为什么我们要做 TDSQL-C 这款产品。在传统数据库上,数据库的使用是存在一些问题,主要分为以下四个:

第一是资源利用率低,计算和存储在一台机器上,CPU 和磁盘使用不均衡,例如 CPU 用满,但磁盘很空闲或者 CPU 很空闲但磁盘又满了,这样就会导致资源利用率低。

第二是扩展能力不足,在单排上可能不能满足一些用户要求,无法扩展。

第三是资源规划难,例如用户使用数据库,一开始无法预估这个数据库需要多少次磁盘空间。

第四是备份比较困难,因为每一个实例数据是私有的,所以每个实例都需要单独进行备份。

TDSQL-C 的解决思路:

第一个问题是计算存储分离,计算资源可以弹性调度。例如说可能给用户分配一个 4 核 8G 存储资源,一段时间以后会发现这个无法满足他要求,那可以给它分配一个更高规格实例。比如说 16 核 32G 这样一个实例,这个是做计算资源升配。第二个问题是日志下沉以及异步回放,TDSQL-C 的日志是通过网络,从计算层下放到存储层。第三个问题是共享分布式存储,我们 TDSQL-C 地域的所有实例,在底下是共享一个分布式存储,可以动态向一个实例里添加资源。最后一个是后台持续备份,我们的后台有定期备份任务,将日志和数据备份到地上存储上面。

PG 实例,包括主实例和读实例,主的负责读写,只读是负责数据读取。在 PG 下有一个叫 CynosStore Agent 组件,它主要负责存储层进行通信,包括主的写日志、读页面,从的是读页面。再向下是存储服务或叫 CynosStore,采用的是 RAPS 结构,一主两层。右边是集群管理服务,是对 CynosStore 里存储节点进行管理服务。包括故障迁移是一个节点发生故障然后迁到其它节点功能。

另外一个当需要扩展资源时,也是由这个集群管理服务来实现。下面是对象存储,我们会定期在对象存储备份日志和数据。这里涉及几个核心架构:一个是日志下沉,计算节点产生的日志是通过网络,下沉到存储层。存储层是通过异步方式来回访日志,导出页面上去。另外是我们会提供一个多版本读的能力,PG 上层可能会有多个 Buffer 会话去读这个数据,每个开始时间不一样,可能会读到不同的版本。

介绍一下日志下沉、异步回放这部分。这里边涉及几个概念:

第一个是数据原子修改,我们叫 MTR。当中有很多情况是一个数对应一个数据库要修改多个页面,例如想对数字索引分裂或是一条 UP Date 语句,它可能要给多条元组,分布在不同的页面上,这些都需要保证是原子操作。第二是 CPL 概念,我们 MTR 里修改了多条数据页面修改,最后一个产生日志我们叫它 CPL,另外一个是叫 VDL。这 CPL 是存储层所有连续 CPL 里最大值,我管它叫做 VDL。随着数据库在运行中,这个 VDL 是在不断地向前推进。同时我们的 PG 读也是拿到一个读点,就是一个个的 VDL。

第二个是日志异步写入。日志异步写入是由我们 PG 进程来生成日志,写到我们的日志 Buffer,日志 Buffer 是 PG 进程和我们 CynosStore Agent 进程之间共享的。写到这个 Buffer 以后,由 Agent 进程给它异步发送到存储节点。存储节点是通过挂到日志链上,再异步合并到数据页面上去,整个过程都是异步的。

第三个是日志并行插入。上层 PG 可以有多个 Buffer 同时去写日志,是用一个并行的方式,不是串行的拷贝方式。

接下来看一下,TDSQL-C PG 版的主机优化:

第一点是不必依赖于传统 PG 的 CheckPoint 机制。大家都知道传统 PG 有个 CheckPoint,定期把数据库中日志和对应修改的数据页都刷到本地磁盘上,这是一个比较耗时操作。那在我们的系统里边由于是存在分离,日志是异步通过存储节点来回放,所以就没有了 CheckPoint(07:43 英)的机制。

第二点是不需要在日志中记录全页。PG 上有一个概念,叫 Full Page Write,在 CheckPoint 之后,对每个数据页面的第一次修改,把整个数据页面内容写入到日志当中。这是为了防止断电情况下可能产生数据页面的半页问题,而在我们这种架构下不需要这个,可以减少很多日志。

第三点是快速启动系统。在启动时不需要恢复 XLog、DLog 这些,可以很快将数据库启动起来提供服务。传统 PG 它是先需要恢复大量 XLog 以后,达到一致点才可以对外提供服务。

最后一个是日志合并压缩。这个是在对一个数据页面做修改时,往往需要修改这个页面多个不同偏移,比如说从第 0 个偏移改 8 个字节,然后需要从第 30 个字节开始改 4 个字节,会涉及到多个修改。我们把这同一页面多个修改抽象出来一个共享日志来减少日志大小,进一步减少 IO。

TDSQL-C PG 版主从架构

接下来介绍一下 TDSQL-C PG 版的主从架构。传统数据库 PG 主备模式是先把日志写到本地磁盘,再由主机的 Sender 进程把 XLog 读取出来通过网络发送到备机,备机有个 Receiver 进程接收到这部分日志写到本地磁盘,再由 PG 恢复进程 Starup 读出来,把 XLog 对应修改应用到数据页面上去。但是它有几个缺点,第一个是在创建备机时,需要拷贝主机日志和数据这部分内容,这部分内容拷贝需要一定时间,例如说当实例比较大几个 T 甚至几十个 T 时,需要很多时间,另外一点是需要耗费存储资源,在备机切换成主机和启动过程中,它都是需要去恢复 XLog,达到一致性状态之后才能对外提供服务,导致结果就会启动慢。

我们采用的一主多读模式有以下两种优势:第一个是由于我们搭建从句速度很快,所以横向扩展读能力会比传统 PG 好很多。我们搭建从不需要考虑数据,因为我们的数据是共享的。第二个是由于我们横向扩展能力强,所以从提升主时也不需要来恢复日志,在提升数据库可用性这方面比传统 PG 好很多。

接下来介绍主从架构里边多个节点并恢复日志的实现。这张图里面是一组三层结构,可以看到主从之间发送日志是在我们 CynosStore Agent 这个组件里进行发送。从机是由 CynosStore Agent 组件来接收日志。接收到日志会把它先写到磁盘,再读上来写到共享日志 HAC 表里生成日志链,这个日志链的 Key 就是 Block ID。

这些日志大概分为两类:第一类是运行时一些信息,包括事物列表,还有锁,还有一些运行时快照这些信息。另外一类是对数据页修改产生的日志,包括 Heap 页面、索引页面这些。挂完链以后,这些链上的日志是由 PG 的后台进程读取,然后将日志对应的修改应用到页面上。和 PG 不一样的在于,当我们要应用的日志对应数据页面不在内存当中时,我们跳过这个页面读取,就是日志恢复是不需要从存储上读上来以后再恢复到内存中。这一点是和传统 PG 不一样的地方。

TDSQL PG 版主从机优化

接下来介绍 TDSQL-C PG 版优化,这里涉及到从句优化。从 Starup 进程去读 XLog,可以看到它不管页面是不是在 Buffer 中,它都是需要去存储中把对应数据页面读出来,把 XLog 应用上去在恢复下一条。因为它的数据不是共享的而是私有的,如果不恢复对应日志就会丢失数据。

我们和它区别一是我们有多个应用日志进程来应用日志。二是当这个页面不在 Buffer 的时候,我们可以跳过这部分日志,就不需要去读上来再回复。

接下来介绍一下前面提到的从机并行合并日志。这个图里面我们画的是 ABCD 有这么四个数据块,每个块上面是括号里的值表示的是当前数据块日志应用到的位置,11、17、15、9,它们表示的就是当前数据块应用位置。假设现在又来了 12、18、16、19 对应日志,那就会有多个 Merge 进程来对这些没有相关性的数据块做并行的恢复。例如第一个 Merge 进程会对 A 和 B 这两个数据块做恢复。如果是另外一个 Merge 进程可以对 C 这个数据块做恢复,它们之间是并行的。每恢复完这样一些数据块后,我们的 VDL 就可以向前推进,比如说从 17 到 19 还可以接着向前推。

接下来介绍从机优化是针对 DROP 表和 DROP 数据库优化。在数据库 PG 里,当你主机上 DROP 一个表或 DROP 一个 DB 时,从机需要做这么几件事:

第一是要删除,把这个表或者这个数据库在系统表当中的原数据,像 PG-Class 当中或者是 PG-Attrdef,还有 PG-attribute 的这些系统表当中的原数据,要先给它删掉。

第二是需要遍历一下 Shared Buffer,这些表或数据可能在从机上去读取过,它可能在 Buffer 里留下了一些页面,我们需要把这些页面找到,让它失效掉。

第三是发送失效消息,就是这个表或数据的失效消息。一个时效消息队列,通知其它进程,然后是删除外存文件。

这里第二步是 Shared Buffer,这个操作比较耗时。按照默认 PG 的一个 Buffer 是 8K 来算,那么 1G 的 Shared Buffer 就会有 13 万左右 Buffer,那么有 64G 大概会有 800 多万的 Buffer。被删一张表,在从机这边可能要遍历 800 万次 Buffer,失效一个页面,去淘汰一个页面。当到了这个表比较多的时候,比如说抓回一个 Scheme,下面可能有几十张表时,那么这个遍历次数就更多了。

我们这里做了一个优化,就是将它的第二步也就是遍历 Buffer,失效 Buffer 的操作,把它单独拿出来做为一个进程来做这件事。这个地方我们叫 Log Process。它和 Starup 之间是通过共享内存队列方式来通信,也就是 Starup 进程。当需要失效 Shared Buffer 的时候,我们会把对应的 DBID 或表 ID 放到队列中,Starup 会通知 Log Process,Log Process 会去对接里面取到这个 DBID 或 TableID,再去遍历 Buffer,将对应 Buffer 失效掉。

另外一个优化和 DAG 表相关。PG 在 DAG 表时,会把这个表信息在内存当中保存这个表一些信息,当这个表在主机删除,再从机需要恢复的时候,它会把这个表从它单项列表当中移除掉,也就是这个表第一次创建时会在从机放到一个列表里,当主机删除的时候,备机会把这个表信息从单列表导出来,把它进行删除。

当我们表比较多的时候,比如说有几千、几万表时,单向列表长度可能需要几千几万。当要删除这个表时,要从几千或几万个元素单向列表中去找到要删除的节点,找到它前向节点,最后把它指向要删除这个节点的后向节点。这个单向里查找是一个比较耗时的操作。

这里用法比较简单是把单向链表改为双向链表。让要删除这个节点的前面指向它后面节点,它后面节点指向它前面的节点,就可以达到删除目的。有了这个优化,加上异步淘汰 Buffer 优化,在有一定压力情况下,日志堆积可以从 100GB 降低到几十兆这个级别,几百兆就没有日志堆积了。

接下来是介绍一下传统备机和 TDSQL-C PG 版启动的一个。传统 PG 启动的时候恢复到一致性的点才能对外提供服务。当前画的这个图里比如说最小恢复点是 50,而恢复当中又来了一个 CheckPoint,比如说 CheckPoint 里记录的是 1000,那么它在下一次启动时需要恢复到 1000 点才能对外提供服务。

在 TDSQL-C PG 版里,从机启动时,是需要拿到一个持久化 VDL 就可以获得存储一致性状态,而这个 VDL 是可以从主机传过来的日志当中计算出来。这个速度比 PG 快很多。第二个是快速 Replica 创建备机,就是不需要复制全量数据。

我们的主从优化它解决的问题是避免 PG 在发生主从切换时可能会出现双写,导致日志“分叉”。例如一个一主一丛的 PG 实例,当发生切主时,由于某些原因旧主并没有死掉,可能有些应用还是连在旧主上面,但是另外一些应用连到新主上面,会导致两边数据不一致,需要人工干预才能把数据库恢复到一致状态。

TDSQL-C PG 版采用的是 HA Fencing 机制。当每一个主实例在启动开始写数据时,之前会通过网络去我们的 Meta 服务上面获取一个 Fencing 值,这个 Fencing 值是全局唯一递增的。计算层每次往存储层写日志时都会带着这个值。

当切到一个新主上时,新主又会去 Meta 服务上拿到一个新的值,例如旧主拿的是 100,旧主存储节点通讯时都会用 100,假设这时候新主上来,它会拿到 101,然后它就用 101 和我们的存储节点通信。这时假设旧主还通过网络,以 100 往存储上写数据,存储就会拒绝这个数据的写入,从而达到了避免数据分叉目的。

未来展望

对未来的一些探索,我们可能会采用一些新硬件,包括 RDMA。另外,现在是一种多层的架构,未来会尝试做多主架构、Serverless 无服务化这些来降低计算成本,可能还会做一些兼容性方面的工作,例如 Oracle 兼容性这些。

你可能感兴趣的:(sql)