TDSQL-C PostgreSQL(CynosDB) 内核实现剖析一

2021-09-17 10:50:19 浏览数 (1)

| 导语 TDSQL-C PostgreSQL(CynosDB)是腾讯云数据库团队自研的新一代云原生数据库,融合了传统数据库、云计算与新硬件技术的优势,采用计算和存储分离的架构,100%兼容 PostgreSQL,高度兼容Oracle语法,提供具备极致弹性、高性能、海量存储、安全可靠的数据库服务。本文旨在从数据库内核的角度揭秘TDSQL-C PostgreSQL计算层的技术内幕。本文适合读者:腾讯云售后服务,TDSQL-C用户,TDSQL-C开发者,需要有基本的数据库与存储知识。

一、概述

      TDSQL-C采用计算和存储分离的架构,所有计算节点共享一份数据,存储容量高达128TB,单库最高可扩展至16节点,提供秒级的配置升降级、秒级的故障恢复和数据备份容灾服务。TDSQL-C既融合了商业数据库稳定可靠、高性能、可扩展的特征,又具有开源云数据库简单开放、自我迭代的优势。《TDSQL-C PostgreSQL(CynosDB) 内核解密》文章已总体介绍了TDSQL-C核心架构与关键技术,本文下面将介绍TDSQL-C 计算层内核相关实现细节。

二、CynosPG 内核实现

      数据库引擎CynosPG基于PostgreSQL而来,PostgreSQL是世界上功能最强大最先进的开源数据库。经过长达30年以上的积极开发和不断演进,PostgreSQL已在可靠性、稳定性、数据一致性等获得了业内极高的声誉。CynosPG主要对PostgreSQL日志系统存储系统进行改造,进行高度Oracle语法兼容,以及深度的内核性能及功能优化,对于PostgreSQL SQL引擎架构改动有限,因此CynosPG可以完全兼容PostgreSQL原生的功能。

      计算引擎CynosPG内核总体模块如上图所示,CynosPG内核涉及修改和优化的模块和机制。灰色部分是PostgreSQL内核原生模块,相对改动较少;有色部分表示在CynosPG内核中新增和修改的模块。CynosPG内核实现无状态,不必要的IO全部卸载计算层本地数据文件将不复存在,仍然包含传统数据库内核的大部分组件:查询处理器、事务管理、锁、缓存实现以及MVCC多版本,移除了PostgreSQL中的FPW特性,脏页面刷盘操作

      SQL引擎:是CynosPG的SQL引擎,包括词法/语法分析,语义分析,查询重写/优化,查询执行。由于不涉及SQL层整体架构的改动,因此CynosPG完全兼容PostgreSQL的SQL语法和语义,同时对Oracle语法进行了高度兼容。

      Access:是CynosPG数据的访问和组织方法。其中包括:

1) heap:表数据访问和元组组织方式,包括:扫描,更新,插入,删除等; 2) btree/gin/gist/spgist/hash/brin:索引访问和实现方式,包括各种索引的实现和扫描; 3) CLOG/MultiXACT:与事务提交和元组级别的并发相关。

      其中Access层是打造的重点模块,将原来访问XLog模块记录日志,修改成Journal Write模块记录日志。PostgreSQL通过原生的XLOG来保证存储的恢复时的一致性,这些XLOG会保存到本地日志文件中。针对CynosPG需要将数据页面的日志改动写入到CynosStore中,例如:

      1) 表、索引以及MultiXACT的修改按照字节修改生成日志;       2) CLOG以及CynosPG的Visibility Map的修改按照bit位的修改形成日志。

      诸如此类。经过这样的改造,CynosPG可以将系统中数据修改形成CynosStore的日志并发送到存储中,但是这些日志与PostgreSQL原生的XLog的格式是不同的。

      Storage/Buffer:buffer pool以及页面的布局、锁等实现。

SMGR:存储管理器。CynosPG可以支持多种类型的存储,MD即是其中一个。为了支持新的分布式块存储,也引入了新的SMGR类型,即 CynosStore存储。在此主要使用CynosRead接口从存储中读取页面。

MD:磁盘存储实现。CynosPG通过MD模块完成从磁盘文件读写数据。

CynosReadJournal Write对应于对CynosStore中的数据读、写。CynosStore也将文件的创建、扩展、删除、以及截断(文件DDL)实现成了日志记录,与数据日志一起写入到CynosStore中。因此CynosStore是一个完整的并且完全的日志系统存储。

读IO进程:用来处理CynosPG对CynosStore发起的读请求;写IO进程:处理CynosPG写入到CynosStore的日志流。

CynosStore Client:封装了对CynosStore的访问接口,例如:读取页面接口,生成CynosStore日志接口等

      CynosPG将原生PostgreSQL的XLOG转化成CynosStore要求的日志格式,同时在原生的Primary和Replica传输的信息由原来PostgreSQL的XLOG修改为CynosStore的日志格式。TDSQL-C 架构最终脱离了PostgreSQL原生的XLOG,同时对XLOG相关的一些特性也做了调整和优化。

日志系统

      可以理解存储层CynosStore是一个支持日志的、提供多版本读的、分布式的块设备,其日志是幂等的,每条日志记录的是页面的物理修改结果,由 <页面号,页面偏移,修改内容,修改长度> 四个部分组成,这样,在同一页面上多次回放同条日志不会造成数据破坏——幂等。以表插入元组为例,原生PostgreSQL的日志格式是(简化方便理解):

      relfilenode pageno来确定一个页面,offsetnum位置插入一条元组,插入的元组是在恢复时由informask2, infomask, hoff, tuple_data等信息进行重构。而CynosPG的日志结构如下,假设在页面号为1的页面上插入元组tuple,CynosPG会生成多条日志格式如下:

这些日志记录了页面在插入元组时的所有修改,相对于原生日志这种格式更像是物理日志。这些日志最终会在CynosFileSystem形成一个MTR加入到日志流中,发送到CynosStore存储。CynosStore合并日志时,需要将此MTR完全应用到数据页面才算成功,因为MTR的部分应用会造成页面结构的不正确,因此我们只要能够保证按照MTR的粒度读取页面,就能够保证读取到的页面的结构是正确的。

      目前看CynosPG的日志量比原生PostgreSQL的XLOG的要大,其实确实是这样。但我们基于最新的日志格式,并且利用其天然的幂等性,对CynosPG的内核中的一些特性进行优化,而优化之后的日志开销与原生PostgreSQL的日志系统的日志开销几乎相等。这些优化和设计包括:

      1) 移除原生PostgreSQL中full page write(FPW)特性。FPW是为了防止系统crash再重启之后,那些半页写(torn page)的页面没有正确恢复,类似MySQL的double write。由于新的日志格式是幂等的,当出现半页写时,系统直接重新在此页面的回放日志,即可将页面修复到一致状态。因此CynosPG无须原生的FPW,从而减少了日志量。

      2) 移除系统中脏页面刷盘操作。CynosPG通过日志保存页面的修改,并且可以通过在基页上合并日志而读取到任何时间点的页面,因此无需原本系统的刷脏操作,仅仅刷日志就足够。

      3) 移除计算并填充页面的CRC操作。在原生PostgreSQL中,页面在刷盘前会计算并填充页面的CRC属性,而在CynosPG中,如果要正确计算页面的CRC属性,应该是在每条页面修改日志之后,都追加一条修改本页面CRC属性的日志。这样势必会造成日志条数增加以及计算CRC而引起的CPU时间增加。为了减少CPU时间和日志条数,我们将CRC的计算下放到CynosStore中,当CynosStore在刷页面前,计算并填充页面的CRC值。但是在CynosPG中,不是所有页面都有CRC值,因此为了支持这个特性,我们将CynosPG的页面布局做了修改,这也是CynosStore对页面布局的要求:保留前10个字节给CynosStore,其中:前8个字节记录LSN,第9和10字节记录CRC。通过这样的设计,我们减少了计算层的CPU使用和日志条数。

      4) 实现文件的异步扩展。在原生PostgreSQL数据库,使用的本地文件系统,其扩展操作是同步并实时的反映到磁盘文件上。但是CynosPG的扩展是通过日志实现,如果每次扩展都对日志做一次flush操作,让扩展实时地在存储上,必然会造成系统的性能下降。因此,我们实现了文件的异步扩展,即:文件扩展的日志可以先保留在系统的日志buffer中,当事务提交的时候再把日志刷到存储上。并且实现了在文件扩展时可以按照策略进行批量扩展。

除了以上在CynosPG内核的优化,CynosPG对日志的记录方式也进行了精简和压缩。CynosPG的日志都有一个大约40字节的日志头(LogHeader),同一个MTR的中的修改相同页面的日志可以合并,减少日志header数,如下图所示:

LH代表LogHeader,Log Element代表对页面的页一次修改。如上图,有两条对Block1的修改日志,并且每个修改都有一个日志头(LH),经过日志头合并优化后,形成新的MTR中,修改Block1的那些日志共享了同一个日志头,减少了40字节大小。同时,如果同一个页面的修改的两条日志是相邻的,那么可以将两条日志合并成一条日志,如果Offset2 <= Offset1 Length1,则日志可以进一步合并如下:

      通过这种方式减少了日志条目,从而可以提高日志合并和页面的生成速度。CynosPG对日志写也进行了并行、流水线及批量优化,通过以上的各种优化,可以很大程度上减少了网络IO和日志量,优化之后的日志开销与原生PostgreSQL的日志系统的日志开销几乎相等。

三、CynosFileSystem 分布式用户态文件系统

      TDSQL-C采用计算和存储分离的架构,所有计算节点共享一份数据(share storage架构),其弹性扩展和高性价比的基石则是分布式用户态文件系统CynosFileSystem,向上为CynosPG提供Pool维度的文件存储服务,提供实例读写访问所需要的分布式的文件管理,负责将文件的读写请求翻译为对应的BLOCK读写,向下访问分布式云存储系统CynosStore(一个支持日志的、提供多版本读的、分布式的块设备存储)。

      CynosFileSystem(CynosFS)与传统的文件系统不同,CynosFS专为TDSQL-C数据库设计的用户态文件系统,它基于CynosStore的共享块存储服务,向上提供非POSIX接口的文件服务,CynosFS对接的CynosStore存储服务接口主要有写日志,读Page构成。CynosFS Pool维度布局如下图所示:

pool: 一个pool与一个数据库实例是一一对应的关系,pool向上提供一个逻辑的存储池并隐藏分布式存储的细节。 segment group (sg): pool逻辑上划分为多个sg,sg是分布式存储层数据复制与迁移的最小单位,同时也是pool扩缩容的最小单位,当前sg的大小为10G。一个sg包括的3个segment实际存储同一份数据,通过一致性协议(Raft)进行同步。 block group: sg逻辑上划分成40个block group,每个block group都bitmap,inode blocks等元数据用于管理本block group的资源。当前一个block group大小为256M,由8k大小的block组成,即一个block group有 256M/8k = 32768个block。block group在逻辑上划分成由如下几个区域:

  • group descriptor block: 占一个block, 存放数据其实就是sg文件系统的控制信息部分,也可以说它是sg资源表。每个sg内仅第一个block group的group descriptor block有效,其它block group的group descriptor block未使用,仅为了保持所有block group结构一致。
  • inode bitmap block: 占一个block, 仅前4k用来标识本block group内inode的空闲情况。
  • block bitmap block: 占一个block, 仅前4k用来标识本block group内blocks的空闲情况,初始时group descriptor block、inode bitmap block、block bitmap block、inode table blocks对应的bit被标识为非空闲状态。
  • inode table blocks: 占多个block, 每个block划分成多个inode, 并且inode不会跨越两个block。
  • extent blocks:占多个blocks, extent tree除根节点以外的节点从这个区域分配。
  • data blocks: 占多个blocks, 用于存放实际的数据, 文件扩展时从这个区域分配blocks。

      inode: 文件名、inode与extent tree三者是一一对应的关系。inode内部存储文件的大小等文件详细属性,同时也存储extree tree的根节点。

      extent tree: 文件和Pool在逻辑上都是由block组成。把文件的block称为logical block, pool上的block称为physics block, 从logic block到physics block的映射就是由extent tree完成的。如下图如示,logical blocks被映射pool上由多个连续的physics blocks组成的区段(extent)。

extent tree类似于B 树,不同之处在于extent tree的插入与删除都是尾部,故extent tree也不存在节点的分裂。extent tree的根节点直接存储在inode中,其它节点从block group中的extent blocks区域分配,即除根节点外其它节点都是一个完整的block。每个节点都由头部和entries两部分组成。

CynosFS 两种队列完成日志写及页面读流程:

1)日志发送流程(写通信队列):日志产生到日志写入Journal Buffer,通知CynosStoreAgent下发日志; 2)页面读取流程(读通信队列):Backend发起读取页面,到返回页面并通知Backend页面可用。

CynosFS是专为云原生数据库而打造的分布式用户态文件系统,将传统分布式文件系统优势与云原生数据库相关特征进行融合,是TDSQL-C弹性扩展、海量存储、高性价比的基石,目前单实例存储容量高达128TB,架构设计理论上是容量大小无限制。CynosFS如何做到在海量存储下(百TB存储,海量小表场景)做到高性能,以及其他模块细节原理,我们后续文章会逐步给大家介绍。

四、CynosStoreAgent 内核实现

      CynosStoreAgent为CynosFS提供SG维度的存储访问,提供SG的读写接口,对于读写请求有不同处理。 写请求:将修改日志通过LOG API发送到CynosStoreNode,读请求:直接通过BLOCK API读取CynosStoreNode数据。CynosStoreAgent 除了承担计算层与存储层的读写交互,也负责主备间日志流同步,对于CynosPG主备实例间只通过CynosStoreAgent进行交互。

      CynosStoreAgent主要系统的模块结构,以及与CynosPG、CynosFS、CynosStoreNode、CynosStoreMeta核心交互关系如下图所示,CynosStoreAgent与TDSQL-C所有的内核组件均有关联。下面主要介绍将其核心部分,后续文章也会给大家分享其他细节实现。

CynosStoreAgent(简称SA)作为一个独立的进程服务本机上的所有实例。(一台机器上可以配置多个CynosStoreAgent)。包含如下几个主要模块:       1) 初始化(Startup Pool Task):完成Pool的初始化工作。当CynosPG实例连接到StoreAgent时,会执行这个过程,初始化会话。包含:Fence功能(防止多主同时对Segment Group进行写的一种机制)、GetWalList/TruncWal返回APPDATA、从Meta获取PoolInfo,以及初始化JournalControlData结构;       2) Read Page Task:接收处理读请求,将数据直接放到CynosPG中的Shared Buffers中,元数据放入CynosFS Cache中;       3) Set MRPL/VDL Task:MRPL下发到CynosStoreNode(简称SN)、VDL随锚点日志下发SN;       4) ReadSlot:记录当前发起的读请求以及他们关联的VDL。ReadSlot用于计算MRPL,发送到SN进行日志回收;       5) Error Handle:处理读流程和日志流程发生的错误,例如:断连接后日志重发,切主等;       6) Journal Flush Task:异步刷新日志到SN;       7) Calculate VDL Task:推进VDL回收日志Buffer;       8) Journal Bitmap:记录哪些日志在SN中已经持久化;       9) 以及上图未标识的主备日志同步逻辑,与StoreMeta(简称SM)存储信息同步逻辑等等;

日志发送

      CynosStoreAgent日志发送流程如上图所示分为两层。Session(pool)层:保存于Pool相关的信息,对CynosPG(CynosFS)实例提供服务。每一个实例对应一个Session数据结构,包含:日志相关的Bitmap、Fence值、PoolInfo。CynosFS启动初始化的时候,需要向CynosStoreAgent进行注册,创建Session数据结构。 Session Task会在UnixSocket上进行wait,当有日志发送请求时,Session Task会将发送的日志通过Segment ID提交到固定的Shard CPU进行处理:调用Fill Journal Task填充日志头部、Term(并:计算CRC)。将填充好的日志根据Segment ID 再dispatch到其他CPU上,通过网络发送到SN(Send Journal Task)。

页面读取

      CynosPG Backend把请求放到读请求队列中,CynosStoreAgent从读队列中获取读请求,并根据映射将请求转发到对应的Read Task,由网络发送出去。读请求的返回值将会先将数据copy到Buffer Pool中,再唤醒Backend。对于错误处理:Net层收到读请求相关的错误,发送到对应的Session Class进行处理,包括:网络错误、TransLeader、NotInService、SN crash、Fence失败等。写日志和读数据的返回都在同一个CPU上的Session进行处理,因此,对于TransLeader需要更新Pool的配置时,可以无锁处理。

  CynosStoreAgent在工程实现上,由于现代多核和多插槽计算机对于跨核间的数据共享(atomic instruction、cache line bouncing、memory fences)代价非常大,CynosStoreAgent(无锁实现)工程实现上使用了创新的share-nothing编程异步框架,一种无需耗时锁定即可在 CPU 内核之间共享信息的设计,面向现代新硬件技术的优势实现了极致性能。由于篇幅限制,StoreAgent其他具体模块细节就不展开了,后续文章会逐步给大家深入每一点进行分享。

五、总结

本文从内核开发的角度介绍云原生数据库TDSQL-C计算层CynosPG、CynosFS、CynosStoreAgent内核实现原理,后续文章内核团队会由点及面的分享计算层的各个技术点细节。

广告时间:TDSQL-C PG数据库团队广招天下英才,这里拥有全球顶尖的数据库专家,汇集了一批极具极客精神的数据库爱好者,团队不断的吸收业界最新的理论和工程成果,最新的硬件和软件技术,将其应用到自研系统中,设计和实现高性能新一代云数据库,TDSQL-C PG还有很长的路要走,如果你也对数据库领域技术感兴趣,欢迎加入我们,让我们一起见证TDSQL-C PG 攀登世界的顶峰。

六、相关概念

Mini-transaction:MTR用来保证硬盘上页数据的ACID特性 Write Ahead Log(WAL LOG):增量日志流水,里面的日志记录保存了页面修改操作,物理日志 Log Sequence Number(LSN):唯一标识一条日志记录,按生成顺序连续单调递增 Consistency Point LSN(CPL):MTR可能对应多个磁盘数据页的修改,相应的会产生多条日志记录,这批日志记录中的最后1条(也就是LSN值最大的1条)标识为CPL,也是MVCC的读一致性点 Segment Group Complete LSN(SGCL):表示SG已经将所有小于等于该值的日志记录持久化到磁盘上了。对于TDSQL-C来说,因为采用了Raft协议,SGCL就是Raft的CommitIndex Volume Complete LSN(VCL):标识已经持久化存储的Pool级别连续日志中的最大LSN值 Volume Durable LSN (VDL):小于等于VCL中的CPL最大值 Read Point LSN (RPL):CynosPG实例的事务读LSN Min Read Point LSN (MRPL):MRPL是read-point的低水位,每个存储节点根据全局的MRPL,不断推进数据页版本,segment的block与对应的block_wal_list进行合并,CynosPG实例的访问的RDL必须大于MRPL,在实例层LSN是连续的,而每个segment内LSN是不连续的,Index是segment内LSN概念,通过lsn_mgr管理lsn与Index映射关系

参考资料: 1. 源代码:《CynosPG源码》、《CynosFS、CynosStoreAgent源码》 2. 《深入了解CynosDB for PostgreSQL实现》 3. 《腾讯云CSIG数据库产品中心PG内核团队WIKI

0 人点赞