【云原生进阶之数据库技术】第四章-GaussDB-关键技术-2.4.2.2-Astore存储引擎剖析

1 Astore存储引擎剖析

Astore(追加存储)是一种主要用于日志型数据库的存储方法,数据在这种存储模型下是以追加的方式进行存储的。这意味着每当有新的数据需要写入时,它们会被追加到现有数据的末尾,而不是覆盖旧的数据。Astore 的特点包括:

  • 高效写入:因为数据只是简单地追加到末尾,所以写入操作非常高效。
  • 写入顺序:数据按时间顺序写入,便于恢复和重建数据。
  • 历史数据保存:旧数据不会被覆盖,适合需要保留所有历史记录的场景。

适用场景:

  • 日志记录系统(如审计日志、交易日志)
  • 数据仓库中的数据加载过程

1.1 Astore整体框架

        astore整体框架如图1-1所示。如上所述,作为行存储子格式之一,astore需要实现自己的堆表存取(访存)管理接口、堆表页面结构、堆表元组结构、元组多版本机制,以及空闲空间管理和回收机制。

图1-1 astore整体框架示意图

1.2 Astore堆表页面元组结构

        本节介绍astore堆表的页面和元组结构。

        所谓堆表,是指元组无序存储,数据按照“先来后到”的方式存储在页面中的空闲位置。作为对比,在索引表中,元组根据索引键键值的排序,在页面内部有序存储,且各个页面之间在逻辑上也是有序存储的。堆表存储数据主体,索引表仅存储索引键键值以及对应的、完整元组的物理位置(即完整元组在堆表中的页面号和页内偏移)。

1) astore堆表元组结构

        astore堆表元组结构的定义部分代码如下:

typedef struct HeapTupleFields {
    ShortTransactionId t_xmin; /* 插入元组事务的事务号 */
    ShortTransactionId t_xmax; /* 删除元组事务的事务号 */
    union {
        CommandId t_cid;           /* 插入或删除命令在事务中的命令号 */
        ShortTransactionId t_xvac;
    } t_field3;
} HeapTupleFields;

typedef struct HeapTupleHeaderData {
    union {
        HeapTupleFields t_heap;
        DatumTupleFields t_datum;
    } t_choice;
    ItemPointerData t_ctid; /* 当前元组或更新后元组的行号 */
    uint16 t_infomask2; /* 字段个数和标记位 */
    uint16 t_infomask; /* 标记位 */
    uint8 t_hoff; /* 包括NULL字段位图、对齐填充在内的元组头部大小 */
    bits8 t_bits[FLEXIBLE_ARRAY_MEMBER]; /* NULL字段位图 */
    /* 实际元组数据再该元组头部结构体之后,距离元组头部处偏移t_hoff字节 */
} HeapTupleHeaderData;

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

(1) t_xmin,插入元组的事务号(32位)。对应系统字段序号是MinTransactionIdAttributeNumber(-3)。

(2) t_xmax,删除元组的事务号(32位)。如果元组还没有被删除,那么为零。对应系统字段序号MaxTransactionIdAttributeNumber(-5)。

(3) t_cid,插入或删除元组的命令号。对应系统字段序号MinCommandIdAttributeNumber(-4)和MaxCommandIdAttributeNumber(-6)。

(4) t_ctid,当前元组的页面和页面内元组指针下标。如果该元组被更新,为更新后元组的页面号和页面内元组指针下标。

(5) t_infomask2,元组属性掩码,包含元组中字段个数、HOT(heap only tuple,堆内元组)更新标记、HOT元组标记等。

(6) t_infomask,元组另一个属性掩码,包含是否有空字段标记、是否有变长字段标记、是否有外部TOAST(the oversized-attribute storage technique,过长字段存储技术)标记、是否有OID字段标记、是否有压缩标记、插入事务是否提交/回滚标记、删除事务是否提交/回滚标记、是否被更新标记等。如果OID标记存在,那么元组OID从“t_hoff”偏移位置之前4个字节获得,对应系统字段序号ObjectIdAttributeNumber(-2)。

(7) t_hoff,元组数据距离元组头部结构体起始位置的偏移。

(8) t_bits,所有字段的NULL值bitmap。每个字段对应t_bits中的一个bit位,因此是变长数组。

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

typedef struct HeapTupleData {
    uint32 t_len;           /* 包括元组头部和数据在内的元组总大小 */
    ItemPointerData t_self; /* 元组行号 */
    Oid t_tableOid;         /* 元组所属表的OID */
    TransactionId t_xid_base;
    TransactionId t_multi_base;
    HeapTupleHeader t_data; /* 指向元组头部 */
} HeapTupleData;

        该结构体主要成员的含义如下:

(1) t_len,元组长度。

(2) t_self,元组所在页面号和页面内元组指针下标,对应系统字段序号SelfItemPointerAttributeNumber(-1)。

(3) t_tableOid,该元组所属表的OID,对应系统字段序号TableOidAttributeNumber(-7)。

介绍了astore堆表元组结构,下面介绍常用的astore堆表元组操作接口。如表4-11所示。

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

        在上述操作接口中,heap_getattr操作接口是最常用的操作接口之一,执行流程如图4-3所示。

图4-3 heap_getattr操作接口从元组中获取单个字段值的流程图

        heap_getattr操作接口在代码上做了多处优化:

(1) 判断待访问的字段序号是否大于元组头部保存的元组实际字段个数;如果大于,则通过访问pg_attribute系统表得到。该优化来自快速追加表字段特性。该特性允许用户在不需要重写一张表所有行的情况下,在一张表的最后增加一个或多个带默认值约束的字段。

(2) 如果该元组的字段全部非空并且待查询字段之前所有的字段都是定长的,那么在上一个heap_getattr查询该字段的操作过程中,会缓存该字段在元组中的字节偏移;之后再次查询时,当满足元组字段全部非空的情况下会使用上述缓存的偏移位置直接读取字段内容。

(3) 读取元组头部的NULL值bitmap,如果该字段对应的bitmap中的比特位非0,则直接返回NULL值。

1.3 Astore堆表页面结构

        由于整体行存储格式默认的介质管理器是磁盘文件系统,因此采用了和文件系统类似的段页式设计,最小I/O单元为一个页面,这样可以在大多数场景下获得比较好的I/O性能和较低的I/O开销。一个astore堆表页面默认大小为8kB,其结构如图4-4所示。

图4-4 astore堆表页面结构示意图

        在一个astore堆表页面中,页面头部分对应HeapPageHeaderData结构体。其中,pd_multi_base以及之前的部分对应定长成员,存储了整个页面的重要元信息;pd_multi_base之后的部分对应元组指针变长数组,其每个数组成员存储了页面中从后往前的、每个元组的起始偏移和元组长度。如图4-4所示,真正的元组内容从页面尾部开始插入,向页面头部扩展;相应的,记录每条元组的元组指针从页面头定长成员之后插入,往页面尾部扩展;整个页面中间形成一个空洞,供后续插入的元组和元组指针使用。

        对于一个astore堆表的一条具体元组,有一个全局唯一的逻辑地址,即元组头部的t_ctid,其由元组所在的页面号和页面内元组指针数组下标组成;该逻辑地址对应的物理地址,则由ctid和对应的元组指针成员共同给出。通过页面、对应元组指针数组成员、页面内偏移和元组长度的访问顺序,就可以完整获取到一条元组的完整内容。t_ctid结构体和元组指针结构体的定义代码如下。

/* t_ctid结构体*/
typedef struct ItemPointerData {
    BlockIdData ip_blkid;  /* 页号 */
    OffsetNumber ip_posid; /* 页面偏移,即对应的页内元组指针下标 */
} ItemPointerData;

/* 页面内元组指针结构体 */
typedef struct ItemIdData 
{
    unsigned lp_off : 15, /* 元组起始位置(距离页头) */
    lp_flags : 2,     /* 元组指针状态 */
    lp_len : 15;      /* 元组长度 */
} ItemIdData;

        如上两级的元组访问设计,主要有两个优点。

(1) 在索引结构中(参见“4.2.5 行存储索引机制”小节),只需要保存元组的t_ctid值即可,无须精确到具体字节偏移,从而降低了索引元组的大小(节约两个字节),提升索引查找效率;

(2) 将页面内元组的地址查找关系自封闭在页面内部的元组指针数组中,和外部索引解耦,从而在某些场景下可以让页面级空闲空间整理对外部索引数据没有影响,降低空闲空间回收的开销和设计复杂度。具体实现机制在“5. astore空间管理和回收”小节中介绍。

        astore堆表页面头具体结构体定义代码如下:

typedef struct {
    PageXLogRecPtr pd_lsn;    /* 页面最新一次修改的日志lsn */
    uint16 pd_checksum;       /* 页面CRC */
    uint16 pd_flags;           /* 标志位 */
    LocationIndex pd_lower;   /* 空闲位置开始出(距离页头) */
    LocationIndex pd_upper;   /* 空闲位置结尾处(距离页头)*/
    LocationIndex pd_special; /* 特殊位置起始处(距离页头) */
    uint16 pd_pagesize_version;
    ShortTransactionId pd_prune_xid;
    TransactionId pd_xid_base;
    TransactionId pd_multi_base;
    ItemIdData pd_linp[FLEXIBLE_ARRAY_MEMBER];
} HeapPageHeaderData;

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

(1) pd_lsn:该页面最后一次修改操作的预写日志结束位置的下一个字节,用于检查点推进和保持恢复操作的幂等性(幂等指对接口的多次调用所产生的结果和调用一次是一致的)。

(2) pd_checksum:页面的CRC校验值。

(3) pd_flags:页面标记位,用于保存各类页面相关的辅助信息,如页面是否有空闲的元组指针、页面是否已满、页面元组是否都可见、页面是否被压缩、页面是否是批量导入的、页面是否加密、页面采用的CRC校验算法等。

(4) pd_lower:页面中间空洞的起始位置,即当前已使用的元组指针数组的尾部。

(5) pd_upper:页面中间空洞的结束位置,即下一个可以插入元组的起始位置。

(6) pd_special:页面尾部特殊区域的起始位置。该特殊位置位于第一条元组记录和页面结尾之间,用于存储一些变长的页面级元信息,如采用的压缩算法信息、索引的辅助信息等。

(7) pd_pagesize_version:页面的大小和版本号。

(8) pd_prune_xid:页面清理辅助事务号(32位),通常为该页面内现存最老的删除或更新操作的事务号,用于判断是否要触发页面级空闲空间整理。实际使用的64位prune事务号由“pd_prune_xid”字段和“pd_xid_base”字段相加得到。

(9) pd_xid_base:该页面内所有元组的基准事务号(64位)。该页面所有元组实际生效的64位xmin/xmax事务号由“pd_xid_base”(64位)和元组头部的“t_xmin/t_xmax”字段(32位)相加得到。

(10) pd_multi_base:类似“pd_xid_base”字段,当对元组加锁时,会将持锁的事务号写入元组中,该64位事务号由“pd_multi_base”字段(64位)和元组头部的“t_xmax”字段(32位)相加得到。

(11) pd_linp:元组指针变长数组。

        对于astore堆表页面的主要管理接口如表4-12所示。鉴于astore采用的元组多版本设计实现方式(参见“3. astore元组多版本机制”小节),删除操作并不会直接从页面中删除指定的元组,页面管理也没有提供这样的接口。对于被删除的、过于陈旧的元组,通过页面空闲空间整理流程(参见“5. astore空间管理和回收”小节)完成。

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

        在astore堆表页面中,采用64位页面“pd_xid_base”字段和32位元组“t_xmin/t_xmax”字段组合设计方式的原因如下。

   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江中散人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值