openGauss源码解析(41)

openGauss源码解析:存储引擎源码解析(9)

2. 页面元组结构

1) 元组结构

本节介绍行存储引擎ustore表的页面元组结构。

元组结构的定义如下:

typedef struct UHeapDiskTupleData {

ShortTransactionId xid;

uint16 td_id : 8, locker_td_id : 8;

uint16 flag;

uint16 flag2;

uint8 t_hoff;

uint8 data[FLEXIBLE_ARRAY_MEMBER];

} UHeapDiskTupleData;

该结构体只是元组头部的定义,真正的元组内容跟在该结构体之后,距离元组头部起始处的偏移由t_hoff成员保存。上面元组头部结构体部分成员信息同时也构成了该元组的系统字段(字段序号小于0的那些字段)。对各个结构体成员的含义说明如下。

(1) flag,元组属性掩码。包含是否有空字段标记、是否有外部TOAST标记、是否有变长字段标记、指定的事务槽位是否已被重复使用标记,以及更新、删除、锁等标记。
(2) flag2,元组另一个属性掩码。包含元组中字段个数。
(3) t_hoff,元组数据距离元组头部结构体起始位置的偏移。
(4) data,字段的NULL值bitmap,每个字段对应一个bit位,因此是变长数组。

ustore元组头部比astore元组头部小一半,因此在相同大小的页面上,ustore可以放置更多的元组。

在内存中,上述元组结构体使用时被嵌入在一个更大的元组数据结构体中,除了保存元组内容的disk_tuple成员之外,其他的成员保存了该元组的一些其他系统信息,并构成了该元组剩余的一些系统字段内容,定义如下:

typedef struct UHeapTupleData {

uint32 disk_tuple_size;

uint1 tupTableType = UHEAP_TUPLE;

uint1 tupInfo;

int2 t_bucketId;

ItemPointerData ctid;

Oid table_oid;

TransactionId t_xid_base;

TransactionId t_multi_base;

UHeapDiskTupleData* disk_tuple;

} UHeapTupleData;

该结构体几个主要成员的含义如下。

(1) disk_tuple_size,元组长度。
(2) ctid,元组所在页面号和页面内元组指针下标。
(3) table_oid,该元组属主表的OID。

常用的元组操作接口和说明如表4-19所示。

表4-19 常用的元组操作接口

函数名

操作含义

UHeapFormTuple

利用传入的、各个元组字段的值数组,生成一条完整的元组,一般用于插入操作

UHeapDeformTuple

利用传入的完整元组以及各个字段的类型定义,解构各个字段的值,生成值数组,一般用于更新前的准备工作

UHeapFreetuple

释放一条元组对应的内存空间

UHeapCopyTuple

复制一条完整的元组,包括元组头和元组内容

UHeapSlotGetAttr

获取一条元组中指定的用户或系统字段值

UHeapGetSysAttr

获取一条元组中指定的系统字段值

UHeapCopyHeapTuple

从ustore槽位构造一条astore元组

UHeapToHeap

将一条ustore元组转换为一条astore元组

HeapToUHeap

将一条astore元组转换为一条ustore元组

2) 页面结构

ustore与astore相同,在openGauss中也使用默认的8kB页面。其结构如图4-12所示。

图4-12 ustore引擎页面结构示意图

在一个页面中,页面头部分对应的UHeapPageHeaderData结构体存储了整个页面的重要元信息。UHeapPageHeaderData之后有一个共享的页内事务目录(Transaction Directory,TD),对应元组指针变长数组。元组指针变长数组的每个数组成员存储了页面中从后往前的、每个元组的起始偏移和元组长度。如图4-12所示,真正的元组内容从页面尾部开始插入,向页面头部扩展;相应地,TD插槽目录与记录每条元组的元组指针从页面头定长成员之后插入,往页面尾部扩展。这样整个页面中间就会形成一个空洞,以供后续插入的元组和元组指针使用。每一个ustore表里的一条具体元组都有一个全局唯一的逻辑地址(和astore表里的元组相同),它由元组所在的页面号和页面内元组指针数组下标组成。

页面头具体结构体定义如下:

typedef struct UHeapPageHeaderData {

PageXLogRecPtr pd_lsn;

uint16 pd_checksum;

uint16 pd_flags;

uint16 pd_lower;

uint16 pd_upper;

uint16 pd_special;

uint16 pd_pagesize_version;

uint16 potential_freespace;

uint16 td_count;

TransactionId pd_prune_xid;

TransactionId pd_xid_base;

TransactionId pd_multi_base;

uint32 reserved;

} UHeapPageHeaderData;

其中各个成员的含义如下。

(1) pd_lsn:该页面最后一次修改操作对应的预写日志位置的下一位,用于检查点推进和保持恢复操作的幂等性。
(2) pd_checksum:页面的CRC校验值。
(3) pd_flags:页面标记位,用于保存各类页面相关的辅助信息,如页面是否有空闲的元组指针、页面是否已满等。
(4) pd_lower:页面中间空洞的起始位置,即当前已使用的元组指针数组的尾部。
(5) pd_upper:页面中间空洞的结束位置,即下一个可以插入元组的起始位置。
(6) pd_special:页面尾部特殊区域的起始位置。该特殊位置位于第一条元组记录和页面结尾之间,用于存储一些变长的页面级元信息,如索引的辅助信息等。
(7) pd_pagesize_version:页面的大小和版本号。
(8) potential_freespace:页面中已被删除和更新的元组的潜在空间。
(9) td_count:共享的页内事务信息描述插槽的数量。
(10) pd_prune_xid:页面清理辅助事务号(64位),通常为该页面内现存最老的删除或更新操作的事务号,用于判断是否要触发页面级空闲空间整理。
(11) pd_xid_base:该页面内所有元组的基准事务号(64位)。该页面所有元组实际生效的64位XID事务号由pd_xid_base(64位)和元组头部的XID成员(32位)相加得到。
(12) pd_multi_base:类似pd_xid_base。当对元组加锁时,会将持锁的事务号写入元组中,该64位事务号由pd_multi_base(64位)和元组头部的XID(32位)相加得到。

页面的主要管理接口如表4-20所示。

表4-20 页面管理接口函数

函数名

操作含义

UPageInit

初始化一个新的ustore页面

UPageAddItem

在页面中插入一条新的元组

UHeapPagePruneOptPage

页面空闲空间整理

为了节省每个元组存储空间,元组头部UHeapDiskTupleData采用32位元组XID的组合设计方式。64位的pd_xid_base和pd_multi_base储存在页面上,元组上储存32位的XID。页面上pd_xid_base和pd_multi_base也需要通过额外的逻辑进行维护:同一个页面中所有元组实际的64位XID,一定要在pd_xid_base和pd_xid_base+232之间,所以如果新写入的事务号和页面上现有任意一个元组的XID事务号差距已经超过232,那么需要尝试对现有元组进行基线移位操作,更新pd_xid_base和pd_multi_base。

3)事务目录

事务目录是一种常用的共享资源。它可以为数据页上的元组(tuple)链接相应的事务表(Transaction Table)及undo子系统中的undo页面。数据库中的每个表可以自定义事务目录的数量,并可以复用那些已完成事务占据的事务目录。

每个数据页默认会有4个事务目录。根据并发需求的不同,事务目录的数量可设置为2到128之间的任意值。在使用CREATE TABLE命令创建表时添加了一个新的选项INIT_TD以声明所需的事务目录数量:

CREATE TABLE t1

(

c1 integer;

c2 boolean;

) WITH (INIT_TD=16);

当需要为新事务目录留位置时,系统会先查找当前页面中是否有空事务目录。若无空事务目录,系统将遍历事务目录列表来寻找可以复用的条目。条目是否可以复用取决于与该条目关联的事务的状态。

通常可以复用那些与已冻结或已中止的事务关联的事务目录。

(1) 对于已经冻结的XID,并复用该事务目录。

对于astore而言,冻结的XID代表着事务在所有的会话中都已经不再活跃。

而在ustore中,仅当一个事务创建的所有的回滚记录都被丢弃后,或者说没有其他的Snapshot需要再观察该事务创建的元组历史版本(tuple version)时,才将该XID视为冻结。ustore中的undo回收进程会维护一个oldestXidInUndo变量,系统将通过比较XID与该变量来确定XID是否含有回滚记录。如果XID < oldestXidInUndo,代表所有该XID产生的回滚记录都已经被丢弃。

(2) 对于已中止的事务,在该事务被回滚后,系统才会复用相应的事务目录条目。
(3) 对于已提交的事务,系统将不会无效化回滚记录地址,这样可以保证undo链的完整性。

当没有事务目录可以复用时,事务目录将会自动扩容以容纳更多的条目。需注意的是,事务目录的后面跟随着元组指针区,在扩展时,首先需要将row pointer array向右挪动来腾出空间。扩展后,新的事务目录条目将会在先前的事务目录条目之后依序添加。设计上,允许事务目录的容量最多扩至页面大小的约25%,即约100个事务目录(在8kB大小的页面中,约20Bytes/事务目录)。目前,系统将以每次增加两个事务目录的方式逐步扩容,最多扩至128个事务目录。ustore暂不支持收缩事务目录空间。

在扩容时,可以增加的总条目数也取决于当前页面中的可用空间。有时,页面中的总剩余空间并不能支持事务目录的扩容。此时若当前操作为INSERT或MULTI-INSERT,事务将会索取一个新的页面来进行操作。若操作为UPDATE或DELETE,事务将等待10毫秒后重试获取事务目录。Lock timeout设置可以控制获取事务目录的最大等待时间。在多由短事务组成的工作负载中,等待是可以接受的。

PG stats会报告事务目录等待等信息,以方便监测系统及描述工作负载。

事务目录申请的过程(UHeapPageReserveTransactionSlot函数)如图4-13所示。

图4-13 事务目录申请处理流程

如果当前事务需要申请一个新的事务目录,且系统中不存在空的事务目录时,系统会遍历所有事务目录并寻找可复用的事务目录。

(1) 首先系统会遍历事务目录,寻找XID < oldestXidInUndo的事务目录。这些条目将被视为已冻结。
(2) 接着系统会遍历目标页面上的元组。

① 系统把已删除的元组标记为死亡,其余的标记为闲置。

② 如果系统发现元组还在活跃状态,且相应的TD条目存在于步骤(1)给出的冻结列表之中,系统会把该事务目录设置为UHEAPTUP_SLOT_FROZEN(冻结)。

③ 设置为冻结之后,事务目录中的XID及Undo指针会被无效化。

(3) 如果上述的冻结操作并未产生可用的槽位,系统会遍历事务目录并寻找与已提交或已中止事务关联的条目。这些条目在满足一定条件的状况下可被复用。
(4) 遍历目标页面上的元组。

① 如果系统发现元组关联的事务目录存在于步骤(3)给出的已提交列表中,系统就把该TD条目的flag设为UHEAP_INVALID_XACT_SLOT(无效)。

② 此外,这些事务目录的XID被重设为无效XID。但为了维护undo链的完整,undo指针将被保留。

(5) 如果并未找到与已提交事务关联的事务目录,最后将寻找与已中止事务关联的事务目录。
(6) 遍历与已中止事务关联的事务目录:对于每个事务目录,沿着undo链执行相关的undo操作。
(7) 如果并未找到事务目录,扩展事务目录。
(8) 返回结果。
posted @ 2024-04-29 16:18  openGauss-bot  阅读(26)  评论(0编辑  收藏  举报