开发者
资源
openGauss入门(三):存储引擎三驾马车——Astore、Ustore与CStore
openGauss入门(三):存储引擎三驾马车——Astore、Ustore与CStore
原创
发表于06/17
2190

前两期聊了openGauss的整体差异和事务并发控制,这一期进到存储引擎层。openGauss的磁盘引擎提供了三种存储格式:Astore(追加写行存储)、Ustore(原地更新行存储)和Cstore(列存储)。三者定位不同,实现思路也完全不同。这一期把三套引擎的页面布局、元组结构、多版本管理和空间回收机制逐一拆开来看。

一、Astore:追加写的行存储

Astore是openGauss的默认行存储引擎,全称Append-Store,核心思想是“追加写”——新数据尽量往页面末尾追加,不覆盖已有数据。

3418601-20240429155657026-1793106897.png

astore整体框架

1.1 页面与元组结构

Astore的页面大小为8KB,页面布局与PostgreSQL的heap页类似:页面头部(PageHeaderData)后面跟着一个元组指针数组(ItemId数组),每个指针指向页面内一条元组的偏移和长度。指针数组从页面头部向后生长,元组数据从页面尾部向前生长,中间是空闲空间。

元组结构是理解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字段位图 */
} HeapTupleHeaderData;

几个关键字段的含义:

  • t_xmin:插入这条元组的事务ID。对应系统字段序号-3。
  • t_xmax:删除这条元组的事务ID。如果尚未删除则为0。对应系统字段序号-5。
  • t_cid:插入或删除操作在当前事务内的命令序号。同一事务内执行多条INSERT/UPDATE/DELETE时,靠这个字段区分先后。
  • t_ctid:当前元组的页面号和页内指针下标。如果元组被更新过,则指向更新后新元组的位置。
  • t_infomask:标志位,记录了插入/删除事务是否已提交/回滚、是否有NULL字段、是否有变长字段等信息。

元组内容紧跟在头部之后,偏移由t_hoff指定。

1.2 多版本与可见性

Astore采用Append Update模式:UPDATE操作不会在原地修改元组,而是在页面空闲位置插入一条新元组,旧元组标记为“已删除”。具体来说:

  1. 新版本元组插入到页面中某个空闲位置
  2. 旧版本元组的t_ctid更新为指向新版本的页面号和偏移
  3. 旧版本元组的t_xmax设置为当前事务ID
  4. 新版本元组的t_xmin设置为当前事务ID

版本链通过t_ctid串联:从旧版本顺着t_ctid能找到新版本,但从新版本找不到旧版本(单向链)。查询时用快照中的CSN与元组的t_xmin/t_xmax对应的CSN比较,判断该版本是否可见。不可见则顺着t_ctid找下一个版本。

这种设计的代价很直接:频繁UPDATE会导致同一逻辑行在页面上留下多个死元组版本,页面快速膨胀。需要VACUUM来回收空间。

1.3 空间回收的三级机制

Astore的空间回收分三个级别:

轻量级清理:查询扫描页面时顺带清理。当一个查询访问某个页面时,如果发现页面内有对所有并发事务都不可见的死元组,会顺手将其标记为可复用空间。这种清理不额外消耗资源,但覆盖面有限。

中量级清理:由VACUUM命令触发。扫描表的所有页面,识别死元组并回收其占用的空间。回收后的空间可以被后续INSERT复用。

重量级清理:VACUUM FULL。不仅回收空间,还会重写整个表,消除碎片,将数据紧凑排列。代价是需要获取排他锁,阻塞所有并发读写。

autovacuum后台线程会定期自动执行中量级清理,但遇到长事务时会阻塞回收——因为长事务可能还需要看到旧版本数据。

二、Ustore:原地更新的行存储

Ustore(Undo Store)是openGauss 2.1.0引入的新行存储引擎,核心思路是“原地更新”。

2.1 设计哲学:新旧分离

Astore把新版本和旧版本混在同一个数据页面里,Ustore把最新版本留在数据页面,旧版本全部搬到独立的Undo空间。

Astore数据页: [版本1(死)] [版本2(死)] [版本3(活)] [空闲...]
Ustore数据页: [版本3(活)] [空闲...]  ← 旧版本在Undo区

数据页面不再堆积死元组,表空间增长曲线比Astore平滑得多。Undo空间统一分配、集中回收,复用效率更高。

2.2 核心数据结构

Ustore引入了几套新的数据结构:

TD(Transaction Directory,事务目录) :页面头部记录每个事务的ID及其对应的Undo记录指针。每个TD条目存一个xid + undo_record_ptr。页面能支持的最大并发数由init_td参数控制(默认值,可配置)。

UHeapDiskTupleData:磁盘上的元组结构,包含td_id(指向TD的索引)和xid偏移等信息。

Undo记录:存储在Undo段中,记录了修改前的完整元组数据以及指向更早版本的指针,形成Undo链。

2.3 查询与可见性判断

Ustore的可见性判断与Astore共用同一套CSN事务管理机制。差异在于数据获取路径:

  • Astore:元组头部直接包含t_xmin/t_xmax,可见性判断一步到位
  • Ustore:元组头部只存td_id,需要通过TD找到事务ID,再查CSNLOG判断可见性

如果当前版本不可见,Ustore沿着Undo链向前遍历,从Undo段中读出旧版本数据,重复可见性判断。

Ustore做了几层优化来避免频繁读取Undo:

  1. 有效TD:直接得到元组最新事务信息
  2. 冻结TD(FROZEN) :元组对所有事务可见,无需再查
  3. 无效TD:修改元组的事务已提交且比当前TD中的事务更旧,直接用TD中的事务做可见性判断即可,不需要读Undo

只有当前版本不可见且TD无效时,才真正进入Undo链遍历路径。

2.4 NUMA-Aware的Undo子系统

Ustore的Undo子系统采用NUMA-Aware设计。在多核平台上,每个NUMA节点有独立的Undo空间分配策略,减少跨节点内存访问。Undo launcher和undo worker线程协同工作,负责Undo空间的分配、回收和事务回滚。

Undo空间支持细粒度的资源控制:可按事务运行时长、单事务Undo空间大小、整体Undo空间阈值进行“监管”,超限时事务报错回滚。

2.5 索引的差异

Astore的索引不包含事务信息,索引元组指向堆表元组的物理位置(页面号+偏移)。可见性判断需要回表。

Ustore采用多版本索引技术:索引元组中携带了事务信息,可以在索引层直接判断可见性,减少回表次数。同时解决了Astore的索引膨胀问题——不需要频繁清理索引中的死元组。

三、Cstore:列存储

Cstore是面向AP场景的列存储引擎。

3.1 CU:列存储的基本单元

Cstore的存储单元是CU(Compression Unit,压缩单元)。每个CU存储表中某一列的连续若干行数据(默认MAX_BATCH_ROWS=60000行)。

CU的结构如下:

字段说明
CRC值除CRC字段外所有字节的32位CRC校验
Magic值插入该CU的事务号
属性值16位标志:是否包含NULL、压缩算法类型等
NULL值位图长度如有NULL行,记录位图字节长度
压缩前数据长度用于读取时内存申请和校验
压缩后数据长度用于写入时内存申请和校验
NULL值位图内容每行是否为NULL
压缩后数据内容实际列数据

相邻CU之间按8KB对齐。

3.2 CUDesc表:元数据与可见性

每个CU对应CUDESC表(实际是一个Astore行存储表)中的一行记录。CUDESC行记录了这个CU的元数据信息,包括该列的最小值、最大值——用于查询时跳过不相关CU。

CU的可见性完全由CUDESC表中对应行的可见性决定。CU本身只支持追加写,没有独立的事务信息。DML操作(UPDATE/DELETE)在Cstore上的处理方式是:

  1. 旧CU对应的CUDESC行标记为删除
  2. 新数据写入新的CU,CUDESC表插入新行

CU间和CU内的可见性控制原理与Astore相同。

3.3 Delta表:缓解小批量写入

Cstore是为批量导入设计的,单条INSERT或小批量UPDATE会产生大量小CU,碎片化严重。

解决方案是Delta表——每个Cstore表附带一个Astore行存表作为缓冲。单条插入或小批量更新先写入Delta表,后台线程定期将Delta表中的数据迁移到主CU中。查询时同时扫描主CU和Delta表并合并结果。

3.4 向量化执行

Cstore与openGauss的向量化执行引擎天然配合。向量化引擎一次处理一批元组(而非逐行),底层扫描节点直接装填向量化的列数据。配合SIMD指令,AP查询性能显著优于行存。

四、选型建议

维度AstoreUstoreCstore
适用场景通用OLTP高更新OLTPOLAP/数据分析
更新方式追加写(Append)原地更新(In-place)追加写(通过Delta表缓冲)
旧版本位置数据页内Undo段新CU
表膨胀高(需VACUUM)中(取决于批量粒度)
空间回收VACUUM/autovacuumUndo统一回收CU淘汰+CUDESC清理
索引类型B-tree, Hash, GIN, GiSTUB-treePsort(默认), B-tree, GIN
点查询
范围聚合一般一般

选择不复杂:

  • OLTP业务用行存储,默认Astore。如果UPDATE频繁、对表膨胀敏感,切Ustore。
  • OLAP业务用Cstore,但要求批量导入,避免高频单条DML。
  • 注意:Ustore不支持列存储和段页式存储。

参考文献:cnblogs.com/openGauss-bot/articles/18165927

收藏举报
Level 1
0
帖子
0
粉丝
0
获赞