简单分析下OPENGAUSS的USTORE
OPENGAUSS 3.0已经开始支持USTORE了,虽然USTORE还不是默认的存储引擎,不过我们已经可以创建USTORE的表了。实际上在一个数据库产品中支持多个存储引擎,并使用相同的WAL和事务管理,MVCC等机制,对数据库的代码要求还是挺高的,因为这些方面的实现方式都会有较大的不同。
说起使用UNDO的USTORE数据库,ORACLE是十分典型的,其数据块的结构也可以作为USTORE存储的其他数据库的参考对象。
上面是Oracle block结构的示意图,实际上这张图直接可以拿来给openGauss的USTORE使用。openGauss USTORE的PAGE结构是完全相同的。
这是从openGauss源码knl_upage.h的注释中直接截的图,这个注释写的很不错,十分清晰的介绍了openGauss USTORE的PAGE布局。UheapPageHeaderData是页头,随后是事务槽,openGauss的数据结构叫TD,定义在knl_utype.h中。随后是RowPtr数组,指向每个具体的行。紧跟着的是空闲空间,然后就是最后一行数据。这个结构和Oracle的数据块结构相似度已经相当高了。我们先来看看UHeapPageHeaderData的数据结构。
typedef struct UHeapPageHeaderData { PageXLogRecPtr pd_lsn; /* PAGE最后一次修改对应的WAL位置指针*/ uint16 pd_checksum; /* 页面的CRC */ uint16 pd_flags; /* PAGE的状态信息,位图信息参考下面 */ uint16 pd_lower; /* RowPtr和最后一行行头之间的空间的起始位置 */ uint16 pd_upper; /* RowPtr和最后一行行头之间的空间的终止位置 */ uint16 pd_special; /* 尾部特殊区域的起始地址, 说明openGauss使用的是可变长尾部, Oracle这样的定长尾部不需要这个指针 */ uint16 pd_pagesize_version; /* 页面版本和大小,说明 USTORE支持多个版本和不同页大小 */ uint16 potential_freespace; /* PAGE中估算的可用空间 */ uint16 td_count; /* PAGE中事务槽TD的数量,USTORE初始化是4 ,最大128 */ TransactionId pd_prune_xid; /* PAGE中最老的删除或更新的XID, 用于PAGE清理操作 */ TransactionId pd_xid_base; /* PAGE中XID的BASE,openGauss 使用了XID 64,采用了类似巴图诺夫的方案, XID BASE 中的32位存储在页头, OFFSET存储在TUPLE里 */ TransactionId pd_multi_base; /* PAGE中MULTI事务的XID的BASE, openGauss使用了XID 64,采用了类似 巴图诺夫的方案,XID的BASE 32位存储 在页头,OFFSET存储在TUPLE里 */ uint32 reserved; } UHeapPageHeaderData;
|
openGauss的事务槽十分简单,只有两个字段,一个是事务ID,一个是指向UNDO记录的指针。在创建openGauss的USTORE表的时候,可以通过WITH (INIT_TD=8)来指定每个页面的初始化事务槽的数量,默认是4个。当事务槽不够用的时候,openGauss也会像Oracle一样自动扩展事务槽。对于Oracle来说,ITL槽无法获得时会等待TX锁,而openGauss采用了失败重试的方式,当占用事务槽失败后,等待超时后重试。
从代码上看,如果申请事务槽失败,会调用UHeapSleepOrWaitForTDSlot后再进行重试。
从这里看,最坏的情况会休眠10毫秒,然后才会重试。对于一个需要低延时插入数据的高并发的应用来说,10毫秒休眠并不是一个好办法。
openGauss的TD重用方面的算法可能会制约PAGE中高并发修改的性能,这一点从我们在USTORE的测试上已经可以感受到了。在这地方,openGauss抄作业抄的还不够完整,USTORE的TD重用机制过于僵化,导致了在某些极端负载下TD可能成为性能瓶颈。不过这些年来,Oracle也是在这地方做了大量的优化,直到Oracle 9.2以后才逐渐成熟。首先UNDO数据链的保全问题,当一个事务提交后,这个ITL(openGauss中对应的就是TD,我看代码的时候发现,很多注释上把TD也说成是ITL,足可见openGauss的开发人员是在照着ITL写TD的代码)就立即可以被重用的,哪怕这个UNDO链暂时还没过期,还需要被其他的会话使用。这部分ITL数据被覆盖后,Oracle是把它放在UNDO RECORD里的,这样哪怕这个事务槽在undo retention期间被使用多少次,也是没有关系的了。而在openGauss中为了保全UNDO链,对已经提交事务的TD还是进行不必要的保护,这导致了TD申请时没有采用类似Oracle ENQUEUE的锁机制,而是采用WAIT & SLEEP的方式,其实从根子上就引入了一个潜在的堵点。
Oracle在这方面的优化上还包含延时块清除等机制,实际上也是openGauss可以借鉴的。这里因为篇幅问题,我就不多做介绍了。
读openGauss这部分代码的时候我感觉还是挺顺的,主要是其主体思路是完全模仿Oracle的,下面我们来看看openGauss UNDO的实现。因为openGauss没有类似Oracle的UNDO表空间,因此在UNDO实现上是和Oracle有着一定的区别的。openGauss的undo数据存储在$PGDATA/undo目录下。
可以看到undo目录下有三个目录和一个undometa文件,undometa是undo的元数据文件。permanent是存储UNDO的持久化数据的。
这部分数据丢失,会导致USTORE表数据的不一致。因此$PGDATA/undo目录绝对不能随便删除。
openGauss的UNDO是通过使用多个UNDO ZONE来确保其并发性能的,openGauss通过undo_zone_count来控制undo zone的数量,这个参数默认为0,是通过系统自动管理的方式来控制ZONE的数量。通过undo_space_limit_size来限制UNDO空间的总大小,并且通过undo_limit_size_per_transaction来限制某个事务的最大UNDO SIZE。这个Size的默认值是32GB,这个值够大,不过还是有限的,在使用USTORE的时候还是要注意。如果要对一个较大的USTORE表做UPDATE操作,很可能会失败。
UNDO ZONE的设计有点类似于Oracle的ROLLBACK SEGMENTS,多个UNDO ZONE有助于提高大并发下的性能。在设计上,openGauss根据现代的NUMA架构做了一定的优化,比如UndoZone可以根据NUMA进行分组,当某个backend分配UndoZone的时候,可以根据Numa进行分配,从而把NUMA的负面影响降低到最小。openGauss的UNDO记录寻址采用64位,分为ZONEID(20)+BLOCKID(31)+OFFSET(13),从这里看,每个UNDO ZONE的大小也是受限的,最大不能大于BLOCKID的最大值*UNDO BLOCKSIZE,而一个事务是不能跨UNDO ZONE的,因此从理论shan上讲,USTORE下的一个事务的最大大小的物理极限在我们当前的数据库应用环境中还是有可能达到的。
UNDO ZONE的数据结构如上图,还是比较简单的,大家有兴趣可以自己看,我这里就不多做解释了。
UNDO RECORD的结构分为头部和修改数据部分两部分,头部主要是事务号,提交号,相关的rel信息等。头部信息的数据结构如上图。
在昨天阅读USTORE的代码的时候,正好有个朋友给我发来了一些关于EDB zheap项目的资料。也有些朋友说华为的USTORE是抄了zheap项目的作业。到底是不是这样呢?这算是给我留的作业,我们明天来讨论这个问题吧。
《简单分析下OPENGAUSS的USTORE》来自互联网,仅为收藏学习,如侵权请联系删除。本文URL:https://www.bookhoes.com/4435.html