目录
- openGauss数据库SQL引擎
- openGauss数据库执行器技术
- openGauss存储技术
一、openGauss存储概览
二、openGauss行存储引擎
Ⅰ、行存储引擎总体架构
Ⅱ、行存储的基本模型与页面组织结构
Ⅲ、行存储的多版本管理以及DML操作
Ⅳ、基于CSN的MVCC机制
Ⅴ、行存储的空间回收
Ⅵ、行存储的共享缓存管理
Ⅶ、并行日志系统设计
Ⅷ、持久化及故障恢复系统设计
三、openGauss列存储引擎
四、openGauss内存引擎
- openGauss事务机制
- openGauss数据库安全
openGauss存储技术
二.openGauss行存储引擎
行存储的多版本管理以及DML操作
03
openGauss行存储的多版本机制与业界比较常见的关系型数据库有较大的不同,核心区别为行存储的多版本在更新的时候并不是就地更新,而是在原有页面中保留上一个版本,转而在这个页面(如果空间不够会在新页面中)创建一个新的版本,来进行历史版本的累积更新。
相应的页面中会同时存有不同版本的同一行数据,拿到不同快照的事务,在读写这些不同版本时互不冲突,有着很好的并发性能。对历史版本的检索可以在页面本身或邻近页面进行,也不需要额外的CPU开销以及IO开销,有着非常高的效率。同时,事务管理以及持久化角度也变得非常的清晰简洁,省去了类似于就地更新所需要记录、执行以及持久化的Undo等相关操作。
以下就以一个DML的例子简单展开行存储结构以及MVCC的实现:
假设我们在一个Xid为10的事务中,在一个只有一列varchar(变长字符串(类型))数据的表中插入一条数据’A’,该行数据落入编号为0的数据页面上,则该行结构如图5所示。
图5 行存储结构示意图1
可以看到xmax为0,此时此记录为有效记录。
假设我们在此基础上在事务xid=20做了delete此行的操作,则此记录变为如图6所示。
图6 行存储结构示意图2
此时xmax被标记为20,如果此事务提交,那么此行最终会被回收。
如果我们在之前insert的基础上,在事务xid=30中连续对该行做两次更新,则改行记录则会如下所示:
第一次更新如图7所示。
图7 行存储结构示意图3
原有行失效,通过ctid记录新版本的ctid,进而指向下一行
第二次更新如图8所示。
图8 行存储结构示意图4
第二个版本也变为历史版本,通过ctid指向最新版本,不过值得注意的是,第二个版本的xmin、xmax都为30,即此版本在同一事务中被删除,而最新版本xmin也仍为30,只是cid从0增加为1(假设此事务连续执行了这两次Update操作)。
更新后的页面如图9所示。
图9 行存储结构示意图5
以上几个简单的例子比较直白的展示了行存储的基本存储结构、行存储的DML以及行存储的MVCC是如何结合在一起共同作用的。
存储引擎内部,索引也是重要的组成部分,索引本身指向存储的是key到ctid的映射。上面我们也提到过了,ctid实际上指向的是line_pointer的检索信息,因此索引的页面上存储的信息以及其与数据页面的关系如图10所示。
图10 索引的页面上存储的信息以及其与数据页面的关系
当然,可能会出现更新操作的新版本无法放入旧版本所在页面的情况,这种情况下页面和索引情况的对比如图11所示。
图11 新版本无法放入旧版本所在页面时的页面和索引情况
此种情况下,Index会有两条entry(记录),两条entry(记录)代表了key(键)对应新旧版本的ctid,这样方便从索引直接跨页面进行搜索。
基于CSN的MVCC机制
04
openGauss采用行级MVCC机制,历史版本集中存储,垃圾清理代价低。每个事务有一个单独的事务状态存储区域,记录了该事务的状态信息和CSN(Commit Sequence Number,提交顺序号)。CSN在openGauss内部使用一个全局自增的长整数来作为逻辑的时间戳,模拟数据库内部的时序。
举例来说:如图12所示,图中每个非只读事务在运行过程中会取得一个XID(事务号),在事务提交时会推进CSN,同时会将当前CSN与事务的XID映射关系保存起来。
图12 CSN-XID映射
因此当一个事务拿到的快照为CSN=3时,事务TX2、TX4、TX6、TX7、TX8的CSN分别为4、6、5、7、8,对于该事务的快照而言,这几个事务的修改都不可见。
MVCC解决的是读写并发冲突问题。更新数据的时候,原地更新,把老版本放到历史版本区页面里,同时维护新版本TUPLE(元组)到老TUPLE(元组)的指针。读TUPLE的时候, 根据快照SNPAHOST.CSN来判断应该读到哪个版本。
数据库在执行SQL的时候,首先会获取一个快照时间戳SNAPSHOT,当扫描数据页面的时候,根据SNAPSHOT.CSN和事务状态来判断哪个TUPLE版本可见或者都不可见。主要分以下3种场景:
(1) TUPLE(元组)的事务状态区里是回滚状态或者运行中,不可见。
(2) TUPLE(元组)的事务状态区里是提交状态,如果SNAPSHOT.CSN比事务区里的CSN小,当前TUPLE不可见,读取前一个版本继续比较CSN。反之可见。
(3) TUPLE(元组)的事务状态区里是待提交状态,需要等待提交。
CSN(Commit Sequence Number,提交顺序号)本身与XID(事务号)也会留存一个映射关系,以便将事务本身以及其对应的可见性进行关联,这个映射关系会留存在CSNLog中,如图13所示。
图13 CSNLog中映射关系
此映射机制类似于Clog本身,只不过不同的是,Clog记录的是事务ID的相关运行状态(运行中/提交/回滚),如图14所示。
图14 Clog记录的事务ID的相关运行状态
进一步结合前面讲过的行头的结构(其中的xmin、xmax)以及Clog、上述CSNLOG的映射机制,MVCC的大致判断流程如图15所示。
图15 MVCC判断流程
简单的总结来说:
§ 如果当前事务ID小于一行的xmin,那么就需要检索xmin对应的clog,读取此事务状态,以此来判断此行数据是否对当前事务可见。
§ 反之,如果当前事务ID大于一行中的xmax,那么说明此行数据的更新/删除发生于本事务开始之前,此行数据对本事务一定不可见(但不排除此行数据的新版本对本事务可见,因为新旧版本是单独进行判断的)
§ 如果XID落在了Xmin、Xmax中间,就需要依据CSN来判断本事务的快照下,对应数据是否应该被看到,需要检索CSNLog来进行对比判断。
行存储的空间回收
05
通过上述章节所介绍的行存储的多版本管理机制,可以发现由于更新和删除并不实际在页面中删除页面本身,数据库长时间运行后,会有大量的历史版本残存在存储空间中,造成了空间的膨胀。为了解决这一问题,存储引擎内部需要定期的对历史数据进行清理,以保证数据库的健康运行。
行存储对于存储空间的清理存在于多个层面、有多种方式。其中在页面一级的机制,成为heap_page_prune。顾名思义,就是在页面内部进行空间的清理。这种清理模式能够比较好的解决更新多版本带来的同一个数据记录关联的长长的历史版本堆叠、标记删除的记录以及无效的记录。这种pruning的手段在对页面进行读取的过程中由页面的空闲空间阈值触发,仅改动heap页面本身,不对索引页面进行改动。因此heap_page_prune是一种较为轻量化的清理方式。举例如下:
如有一个记录a,被前后更新导致同时有6个历史版本,保存于两个不同的页面中,如图16所示。
图16 记录a的6个历史版本
page页面级别的自我清理效果为图17所示。
图17 page页面级别的自我清理
可以看到,清理过程中分别对page1和page2中的内容进行了回收,但是由于之前的跨页面导致的两个索引entry指向不同页面,却被保留了下来。
在页面级别的清理之外,还有表级别、数据库级别的整体清理,这个机制称之为Vacuum操作。Vacuum操作在整个数据库级别进行废旧元组的清理,同时也会清理索引。Vacuum操作可以由数据库用户对数据库或数据库内对象主动调起,同时数据库后台也会有工作线程在满足阈值时或者定期进行数据库自动的Vacuum。如图18所示。
图18 Vacuum操作
Vacuum自身除了清理空间外,也顺带圣诞了更新统计信息的功能,以便优化器能更准确的进行代价估算。
Vacuum在过程中,还会对整个数据库级别都可见的元组进行freeze操作。举例来说,当一个元组被插入并提交,而后续没有更新操作,数据库系统上也不再有早于这个提交的事务时间点、需要对这条元组做可见性判断的事务,此时认为此元组就可以被任何人看见了,那么其相关的事务ID就可以被转化为一个特殊的事务ID——Freeze Xid,以表示这种状态。当Vacuum清理整个系统时,系统中最小活跃事务之前的提交日志(Clog),也同上面说到的,不在被需要,因此Vacuum操作也会对这部分Clog日志进行清理和回收。
当然,Vacuum本身是一个相对高成本的操作,因此,每个表文件会有一个对应的可见性映射(visibility map),来记录这个表数据文件中对应的页面是否已经处于全部可见状态,这种情况下Vacuum在执行过程中就可以跳过这部分页面,节省开销。由于一般系统中存储的绝大部分数据都不与当前活跃事务相关,因此此优化可以大大提升Vacuum的效率。