TDSQL-C for PostgreSQL 主从架构详解

278 阅读14分钟

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 兼容性这些。