openGauss源码解析(38)
openGauss源码解析:存储引擎源码解析(6)
4. astore访存管理
openGauss中的astore堆表访存接口如表4-13所示。
表4-13 astore堆表访存接口
接口名称 | 接口含义 | 对应的行存储统一访存接口 |
---|---|---|
heap_open | 打开一个表,得到表的相关元信息 | 无 |
heap_close | 关闭一个表,释放该表的加锁或引用 | 无 |
heap_beginscan | 初始化堆表(顺序)扫描操作 | tableam_scan_begin |
heap_endscan | 结束并释放堆表(顺序)扫描操作 | tableam_scan_end |
heap_rescan | 重新开始堆表(顺序)扫描操作 | tableam_scan_rescan |
heap_getnext | (顺序)获取下一条元组 | tableam_scan_getnexttuple |
heap_markpos | 记录当前扫描位置 | tableam_scan_markpos |
heap_restrpos | 重置扫描位置 | tableam_scan_restrpos |
heapgettup_pagemode | heap_getnext内部实现,单页校验模式 | 无 |
heapgettup | heap_getnext内部实现,单条校验模式 | 无 |
heapgetpage | (顺序)获取并扫描下一个堆表页面 | tableam_scan_getpage |
heap_init_parallel_seqscan | 初始化并行堆表(顺序)扫描操作 | tableam_scan_init_parallel_seqscan |
heap_insert | 在堆表中插入一条元组 | tableam_tuple_insert |
heap_multi_insert | 在堆表中批量插入多条元组 | tableam_tuple_multi_insert |
heap_delete | 在堆表中删除一条元组 | tableam_tuple_delete |
heap_update | 在堆表中更新一条元组 | tableam_tuple_update |
heap_lock_tuple | 在堆表中对一条元组加锁 | tableam_tuple_lock |
heap_inplace_update | 在堆表中(就地)更新一条元组 | 无 |
以astore堆表顺序扫描为例,执行流程如下。
(1) 调用heap_open接口打开待扫描的堆表,获取表的相关元信息,如表的行存储子格式为astore格式等。该步通常要获取AccessShare一级表锁,防止并发的DDL操作。
(2) 调用tableam_scan_begin接口,从g_tableam_routines数组中找到astore的初始化扫描接口,即heap_beginscan接口,完成初始化顺序扫描操作相关的结构体。
(3) 循环调用tableam_scan_getnexttuple接口,从g_tableam_routines数组中找到astore的扫描元组接口,即heap_getnext接口,顺序获取一条astore元组,直到完成全部扫描。顺序扫描时,每次先获取下一个页面,然后依次返回该页面上的每一条元组。这里提供了两种元组可见性的判断时机:
➀ heapgettup_pagemode。在第一次加载下一个页面时,加上页面共享锁,完成对页面上所有元组的可见性判断,然后将可见的元组位置保存起来,释放页面共享锁。后面每次直接从保存的可见性元组列表中返回下一条可见的元组,无须再对页面加共享,使用快照的查询,默认都使用该批量模式,因为元组的可见性在同一个快照中不会再发生变化。
➁ heapgetpage。除了第一次加载下一个页面时需要批量校验元组可见性之外,在后面每一次返回该页面下一条元组时,都要重新对页面加共享锁,判断下一条元组的可见性。该模式的查询性能较批量模式要稍低,适用于对系统表的顺序扫描(系统表的可见性不参照查询快照,而是以实时的事务提交状态为准)。
(4) 调用tableam_scan_end接口,从g_tableam_routines数组中找到astore的扫描结束接口,即heap_endscan接口,结束顺序扫描操作,释放对应的扫描结构体。
(5) 调用heap_close接口,释放对表加的锁或引用计数。
5. astore空间管理和回收
openGauss中采用最大堆二叉树结构来记录和管理astore堆表页面的空闲空间,该最大堆二叉树结构按照页面粒度进行与存储介质的读写操作,并单独储存于专门的空闲空间位图文件中(free space map,简称FSM)。该FSM文件的结构如图4-8所示。
图4-8 astore FSM文件结构示意图
所有页面分为叶子节点页面和内部节点页面两种。两种页面的页面内部结构完全相同,区别在于:对于叶子节点页面,其页面中记录的二叉树的叶子节点对应堆/索引表页面的空闲空间程度;对于内部节点页面,其页面中记录的二叉树的叶子节点对应下层FSM页面的最大空闲空间程度。
使用FSM页面中的1个字节(即256档)来记录一个堆/索引页面的空闲空间程度。在FSM页面中不会记录任何堆/索引页面的页号信息,也不会记录任何根、子FSM节点页面的页号信息,这些信息主要通过以下的规则来计算得到:
(1) 在一个FSM页面内部,二叉树节点按照从上到下、从左到右逐层排布,即:第一个字节为根节点的空闲程度,第二个字节为第一层内部节点最左边节点的空闲程度,依次类推。
(2) 所有FSM页面在物理存储上采用深度优先顺序,即某个FSM页面之前所有的物理页面包括:该FSM页面所在子树的所有上层节点,加上该FSM页面所有左侧子树。
(3)所有FSM叶子节点页面中的二叉树的叶子节点,对应堆/索引表页面的空闲空间程度,且根据从左到右的顺序,分别对应第1个、第2个、….、第n个堆/索引表物理页面。
(4)除了(3)中这些FSM节点之外,其他FSM父节点保存子节点(子树)中空闲空间的最大值。
根据上述算法,可以高效地查询出具有足够空闲空间的堆/索引页面的页面号,并将待插入的数据插入其中。
FSM模块主要的对外接口和含义如表4-14所示。
表4-14 FSM模块主要的对外接口
接口名称 | 接口含义 |
---|---|
GetPageWithFreeSpace | 获取空闲程度大于入参的堆/索引页面号 |
RecordAndGetPageWithFreeSpace | 更新当前(不满足条件的)堆/索引页面的空闲空间程度,寻找新的空闲程度大于入参的堆/索引页面号 |
RecordPageWithFreeSpace | 更新单个堆/索引页面的空闲空间程度 |
UpdateFreeSpaceMap | 更新多个(批量插入的)堆/索引页面的空闲空间程度 |
FreeSpaceMapTruncateRel | 删除所有储存大于某个堆/索引页面号空闲信息的FSM页面 |
FreeSpaceMapVacuum | 修正所有FSM内部节点的空闲空间信息 |
此外,为了保证FSM信息的维护操作不会带来明显的开销,因此FSM的所有修改都是不记录日志的。同时,对于某个堆/索引页面对应的FSM信息,只在页面初始化和页面空闲空间整理(见本节后面介绍)两种场景下才会主动更新,除此之外,只有当新插入的数据发现该页面实际空间不足时才会被动更新该页面对应的FSM信息(也包括由于宕机导致的FSM页面损坏)。
空闲空间的管理难点在于空闲空间的回收。在openGauss中,对于astore存储格式,有3种回收空闲空间的方式,如图4-9所示。
图4-9 astore空闲空间回收机制示意图
1. 轻量级堆页面清理
当查询扫描到某个astore堆表页面时,会顺带尝试清理该页面上已经被删除的、足够老的元组(足够老是指在元组对于所有并发查询均为已经删除状态,具体参见事务处理章节)。由于只是顺带清理该页面内容,因此只能删除元组内容本身,元组指针还需要保留,以免在索引侧造成空引用或空指针(可参见4.2.5 行存储索引机制)。一个比较特殊的情况是HOT场景。HOT场景是指对于该表上所有的索引更新前后的索引键值均没有发生变化,因此对于更新后的元组只需要插入堆表元组而不需要新插入索引元组。对于同一个页面内一条HOT链上的多个元组,如果它们都足够老了,那么在清理时可以额外删除所有中间的元组指针,只保留第一个版本的元组指针,并将其重定向到第一个不用被清理的元组版本的元组指针。
轻量级堆页面清理的接口是heap_page_prune_opt函数,关键的数据结构是PruneState结构体,定义代码如下:
typedef struct {
TransactionId new_prune_xid;
TransactionId latestRemovedXid;
int nredirected; /* 待重定向的元组个数 */
int ndead; /* 待标记死亡的元组个数 */
int nunused; /* 待回收的元组个数 */
OffsetNumber redirected[MaxHeapTuplesPerPage * 2];
OffsetNumber nowdead[MaxHeapTuplesPerPage];
OffsetNumber nowunused[MaxHeapTuplesPerPage];
bool marked[MaxHeapTuplesPerPage + 1];
} PruneState;
其中,“new_prune_xid”字段用于记录页面上此次没有被清理的、但是已经被删除的元组的xmax,用于决定下次何时再次清理该页面;“latestRemovedXid”字段用于记录该页面上被清理的元组的最大的xmax,用于判断热备上回放页面整理时是否需要等待只读查询;nredirected、ndead、nunused、redirected、nowdead和nowunused分别记录该页面上待重定向的、待标记死亡的、待回收的元组。