- 页面元组结构
1) 元组结构
本节介绍行存储引擎ustore表的页面元组结构。
元组结构的定义如下
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成员之外,其他的成员保存了该元组的一些其他系统信息,并构成了该元组剩余的一些系统字段内容,定义如下:
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所示。
2) 页面结构
ustore与astore相同,在openGauss中也使用默认的8kB页面。其结构如图4-12所示。
在一个页面中,页面头部分对应的UHeapPageHeaderData结构体存储了整个页面的重要元信息。UHeapPageHeaderData之后有一个共享的页内事务目录(Transaction Directory,TD),对应元组指针变长数组。元组指针变长数组的每个数组成员存储了页面中从后往前的、每个元组的起始偏移和元组长度。如图4-12所示,真正的元组内容从页面尾部开始插入,向页面头部扩展;相应地,TD插槽目录与记录每条元组的元组指针从页面头定长成员之后插入,往页面尾部扩展。这样整个页面中间就会形成一个空洞,以供后续插入的元组和元组指针使用。每一个ustore表里的一条具体元组都有一个全局唯一的逻辑地址(和astore表里的元组相同),它由元组所在的页面号和页面内元组指针数组下标组成。
页面头具体结构体定义如下:
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所示。
为了节省每个元组存储空间,元组头部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以声明所需的事务目录数量:
(
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所示。
如果当前事务需要申请一个新的事务目录,且系统中不存在空的事务目录时,系统会遍历所有事务目录并寻找可复用的事务目录。
(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) 返回结果。