暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

openGauss 磁盘引擎之 cstore

原创 小小亮 2022-02-23
3249

列存储格式是OLAP类数据库系统最常用的数据格式,适合复杂查询、范围统计类查询的在线分析型处理系统。本节主要介绍openGauss数据库内核中cstore列存储格式的实现方式

一、cstore整体框架

cstore列存储格式整体框架如下图所示与行存储格式不同cstore列存储的主体数据文件以CUI/O单元只支持追加写操作因此cstore只有读共享缓冲区CU间和CU内的可见性由对应的CUDESEastore)决定,因此其可见性和并发控制原理与行存储astore基本相同

cstore列存储格式整体框架示意


二、cstore存储单元结构

CU结构示意图

上图所述cstore的存储单元是CU分别包括以下内容

1CUCRC,为CU结构中除CRC成员之外,其他所有字节计算出的32CRC
2CUmagic为插入CU的事务号
3CU的属性值16位标志值包括CU是否包含NULLCU使用的压缩算法等CU粒度属性信息
4压缩后NULL值位图长度,如果属性值中标识该CU包含NULL则本CU在实际数据内容开始处包含NULL值位图此处储存该位图的字节长度如果该CU不包含NULL行,则无该成员
5) 压缩前数据长度,即CU数据内容在压缩前的字节长度用于读取CU时进行内存申请和校验
6压缩后数据长度CU数据内容在压缩后的字节长度,用于插入CU时进行内存申请和校验
7) 压缩后NULL值位图内容,如果属性值中标识该CU包含NULL则该成员即为每行的NULL值位图,否则无该成员。
8) 压缩后数据内容,即实际写入磁盘的CU主体数据内容

每个CU最多保存对应字段的MAX_BATCH_ROWS行(默认60000)数据。相邻CU之间按8kB对齐

CU模块提供的主要CU操作接口如所示

CU操作接口

函数名称

接口含义

AppendCuData

向组装的CU中增加一行(仅对应字段)

Compress

压缩(若需)和组装CU

FillCompressBufHeader

填充CU头部

CompressNullBitmapIfNeed

压缩NULL值位图

CompressData

压缩CU数据

CUDataEncrypt

加密CU数据

ToVector

CU数据解构为向量数组结构

UnCompress

解压(若需)和解析CU

UnCompressHeader

解析CU头部内容

UnCompressNullBitmapIfNeed

解压NULL值位图

UnCompressData

解压CU数据

CUDataDecrypt

解密CU数据


三、cstore多版本机制

cstore支持完整事务语义的DML查询原理如下

1CU间的可见性:每个CU对应CUDESCastore行存储表)中的一行记录(一对一),CU的可见性完全取决于该行记录的可见性
2) 同一个CU内不同行的可见性:每个CU的内部可见性对应CUDESC表中的一行(多对一),该行的bitmap字段为最长MAX_BATCH_ROWSbit的删除位图bit 1表示删除bit 0表示未删除),通过该位图记录的可见性和多版本,来支持CU内不同行的可见性同时由于DML操作都是行粒度操作的因此对于行号范围相同的不同字段的多个CU均对应同一行位图记录
3CU文件读写并发控制CU文件自身为APPEND-ONLY只在追加时对文件大小扩展进行加锁互斥,无须其他并发控制机制
4同一个字段的不同CU,对应严格单调递增的cu_id编号,存储在对应的CUDESC表记录中cu_id的获取通过4-24中的文件扩展锁来进行并发控制
5对于cstore的单条插入以及更新操作,提供与每个cstore表对应的delta表(astore行存储表),来接收单条插入或单条更新的元组,以降低CU文件的碎片化。

可见cstore表的可见性依赖于对应CUDESC表中记录的可见性。一个CUDESC表的结构如下表所示其与CU的对应关系如下图所示

  CUDESC表的结构

字段名

类型

含义

col_id

integer

字段序号,即该cstore列存储表的第几个字段;特殊的,对于CU位图记录,该字段恒为-10

cu_id

oid

CU序号,即该列的第几个CU

min

text

CU中该字段的最小值

max

text

CU中该字段的最大值

row_count

integer

CU中的行数

cu_mode

integer

CU模式

size

bigint

CU大小

cu_pointer

text

CU偏移8k对齐);特殊的,对于CU位图记录该字段为删除位图的二进制内容

magic

integer

CU magicCU头部的magic相同校验用

extra

text

预留字段

 


CUDESC表和CU对应关系示意图

所示,下面结合并发插入和并发插入查询2种具体场景,介绍openGausscstore多版本的具体实现方法

  cstore表并发插入示意图

  cstore表并发插入和查询示意图

1 并发插入操作

对于并发的插入操作,会话1会话2首先分别在各自的局部内存中完成待插入CU的拼接然后假设会话1先获取到cstore表的扩展锁那么会话2会阻塞在该锁上。在持锁阶段,会话1申请到该字段下一个cuid 1001预占了该cu文件0 - 6 K的内容(即cuid 1001的内容大小),将cuid的大小偏移以及cuid 1001头部部分信息填充到CUDESC记录中,并完成CUDESC记录的插入接着会话1放锁并将cuid 1001的内容写入到CU对应偏移处记录日志再将删除位图记录插入CUDESC表中会话1释放cstore表的扩展锁之后会话2就可以获取到该锁然后类似会话1的后续操作完成cuid 1002的插入操作

2 并发插入和查询操作

假设在上述会话2的插入事务(事务号101执行过程中有并发的查询操作执行。对于查询操作,首先基于col_idcuid这两个索引键对CUDESC表做索引扫描。由于事务号101在查询的快照中因此cuid 1002的所有记录对于查询事务不可见查询事务只能看到cuid 1001(事务号100)的那些记录。然后,查询事务根据CUDESC记录中对应的CU文件偏移和CU大小cuid 1001的数据从磁盘文件或缓存中加载到局部内存中,并拼接成向量数组的形式返回。


四、cstore访存接口和索引机制

cstore访存接口如下表所示主要包括扫描插入删除和查询操作

  cstore访存接口

接口名称

接口含义

CStoreBeginScan

开启cstore扫描

CStore::RunScan

执行cstore扫描,根据执行计划,内层执行cstore顺序扫描或者cstore min-max过滤扫描

CStoreGetNextBatch

继续扫描,返回下一批向量数组

CStoreEndScan

结束cstore扫描

CStore::CStoreScan

cstore顺序扫描

CStore::CStoreMinMaxScan

cstore min-max过滤扫描

CStoreInsert::BatchInsert(VectorBatch)

将输入的向量数组批量插入cstore表中

CStoreInsert::BatchInsert(bulkload_rows)

将输入的多行数组插入cstore表中

CStoreInsert::BatchInsertCommon

将一批多行数组(最多MAX_BATCH_ROWS)插入cstore表各个列的CU文件中、插入对应CUDESC表记录插入索引

CStoreInsert::InsertDeltaTable

将一批多行数组插入cstore表对应的delta表中

InsertIdxTableIfNeed

将一批多行数组插入cstore表的索引表中

CStoreDelete::PutDeleteBatch

将一批待删除的向量数组暂存到局部数据结构中,如果达到局部内存上限,则触发一下删除操作

CStoreDelete::PutDeleteBatchForTable

CStoreDelete::PutDeleteBatch对于普通cstore表的内层实现

CStoreDelete::PutDeleteBatchForPartition

CStoreDelete::PutDeleteBatch对于分区cstore表的内层实现

CStoreDelete::PutDeleteBatchForUpdate

CStoreDelete::PutDeleteBatch对于更新cstore表操作的内层实现(更新操作由删除操作和插入操作组合而成)

CStoreDelete::ExecDelete

执行cstore表删除,内层调用普通cstore表删除或分区cstore表删除

CStoreDelete::ExecDeleteForTable

执行普通cstore表删除

CStoreDelete::ExecDeleteForPartition

执行分区cstore表删除

CStoreDelete::ExecDelete(rowid)

删除cstore表中特定一行的接口

CStoreUpdate::ExecUpdate

执行cstore表更新

 

cstore表查询执行流程可以参考下图中所示。其中,灰色部分实际上是在初始化cstore扫描阶段执行的,根据每个字段的具体类型,绑定不同的CU扫描和解析函数,主要有FillVectorFillVectorByTidsFillVectorLateRead3CU扫描解析接口

  cstore表查询流程示意图

cstore表插入执行流程可以参考下图所示。其中灰色部分内的具体流程可以参考图中所示。当满足以下3个条件时,可以支持delta表插入

1打开enable_delta_store GUC参数
2) 该批向量数组为本次导入的最后一批向量数组。
3) 该批向量数组的行数小于delta表插入的阈值。

 

cstore表插入流程示意图

cstore表的删除流程主要分为两步

1如果存在delta那么先从delta表中删除满足谓词条件的记录
2) 在CUDESC表中更新待删除行所在CU的删除位图记录

cstore表的更新操作由删除操作和插入操作组合而成流程不再赘述

openGausscstore表支持psortcbtree两种索引

psort索引是一种局部排序聚簇索引psort索引表的组织形式也是cstorecstore表的字段包括索引键中的各个字段,再加上对应的行号(TID)字段。如图所示,将一定数量的记录按索引键经过排序聚簇之后,与TID字段共同拼装成向量数组之后插入psort索引cstore表中,插入流程和上面cstore表插入流程相同。

  psort索引插入原理图

查询时如果使用psort索引扫描,会首先扫描psort索引cstore(扫描方式和上面cstore表扫描流程相同)。在一个psort索引CU的内部由于做了局部聚簇索引因此可以使用基于索引键的二分查找方式快速找到符合索引条件的记录在psort索引中的行号该行的TID字段值即为该条记录在cstore主表中的行号。上述流程如下图所示。值得一提的是由于做了局部聚簇索引,因此在索引cstore表扫描过程中,在真正加载索引表CU文件之前可以通过CUDESC中的min max做到非常高效的初筛过滤

  psort索引查询原理图

cstore表的cbtree索引和行存储表的B-Tree索引在结构和使用方式上几乎完全一致,相关原理可以参考行存储索引章节( 行存储索引机制),此处不再赘述。

openGauss cstore表索引对外提供的主要接口所示

  cstore表索引对外接口

接口名称

接口含义

psortgettuple

通过psort索引返回下一条满足索引条件的元组伪接口实际psort索引扫描通过CStore::RunScan实现

psortgetbitmap

通过psort索引返回满足索引条件的元组的tid bitmap伪接口实际psort索引扫描通过CStore::RunScan实现

psortbuild

构建psort索引表数据主要流程包括cstore主表中扫描数据局部聚簇排序插入到psort索引cstore

cbtreegettuple

通过cbtree索引返回下一条满足索引条件的元组。内部和btgettuple都是通过调用_bt_gettuple_internal函数实现

cbtreegetbitmap

通过cbtree索引返回满足索引条件的元组的tid bitmap。内部和btgetbitmap都是通过调用_bt_next函数实现

cbtreebuild

构建cbtree索引表数据。内部实现与btbuild类似,先后调用_bt_spoolinitCStoreGetNextBatch_bt_spool_bt_leafbuild_bt_spooldestroy等几个主要函数实现btbuild区别在于B-Tree的构建过程中扫描堆表是通过heapam接口实现的,而cbtree扫描的是cstore因此使用的是CStoreGetNextBatch


五、cstore缓存机制

考虑到cstore列存储格式主要面向只读查询居多的OLAP类业务因此openGauss提供只读的共享CU缓冲区机制

openGaussCU只读共享缓冲区的结构如下图所示。和行存储页面粒度的共享缓冲区类似,最上层为共享哈希表,哈希表键值为CUslot类型relfilenodecolidcuidcupointer构成的五元组,哈希表的记录值为该CU对应的缓冲区槽位slot id(对应行存储共享缓区的buffer id)。在全局CacheDesc数组中,用CacheDesc结构体记录slot id对应的缓存槽位的状态信息(对应行存储缓冲区的BufferDesc结构体)。在共享CU数组中CU结构体记录与slot id对应的缓存CU的结构体信息

与行存储固定的页面大小不同不同CU的大小可能是不同的(行存储页面大小都是8 K),因此上述CU槽位只记录指向实际内存中CU数据的指针另一方面为了保证共享内存大小可控通过另外的全局变量来记录已经申请的有效槽位中所有CU的大小总和

  CU只读共享存结构示意图

CU只读共享缓冲区的工作机制如下图所示。

1) 当从磁盘读取一个CU放如Cache Mgr时,需要从FreeSlotList里拿到一个free slot(空闲槽位)存放CU,然后插入到哈希表中。
2) 当FreeSlotListNULL的时,需要根据LRU算法淘汰掉一个slot(槽位),释放CU data占的内存,减小CU总大小计数,并从哈希表中删除,然后存放新的CU,再插入哈希表中。
3) 缓存大小可以配置。如果内存超过设置的大小,需要淘汰掉适量的slot,并释放CU data占用的内存。
4) 支持缓存压缩态的CU或解压态的CU两种模式,可以通过配置文件修改,同时只能存在一种模式。

  CU只读共享缓存读取示意图

CU只读共享缓冲区相关的关键数据结构代码如下

typedef struct CUSlotTag {
    RelFileNodeOld m_rnode;
    int m_colId;
    int32 m_CUId;
    uint32 m_padding;
    CUPointer m_cuPtr;
} CUSlotTag;
/* slot id哈希表键值主要部分,各个成员的含义从命名中可以清晰看出 */

typedef struct DataSlotTag {
    DataSlotTagKey slotTag;
    CacheType slotType;
} DataSlotTag;
/* slot id哈希表键值结构体,成员包括CUSlotTag与slot类型(CU、OBS外表等) */

typedef struct CacheLookupEnt {
    CacheTag cache_tag;
    CacheSlotId_t slot_id;
} CacheLookupEnt;
/* slot id哈希表记录结构体,成员包括哈希表键值和对应的slot id  */

typedef struct CacheDesc {
    uint16 m_usage_count;
    uint16 m_ring_count;
    uint32 m_refcount;
    CacheTag m_cache_tag;
    CacheSlotId_t m_slot_id;
    CacheSlotId_t m_freeNext;
    LWLock *m_iobusy_lock;
    LWLock *m_compress_lock;
    /*The data size in the one slot.*/
    int m_datablock_size;
    bool m_refreshing;
    slock_t m_slot_hdr_lock;
    CacheFlags m_flag;
} CacheDesc;
/* CU共享缓冲区槽位状态结构体,其中m_usage_count、m_ring_count为LRU淘汰算法需要的使用计数,m_refcount为判断能否淘汰的被引用计数,m_freeNext指向下一次空闲的slot槽位(如果本槽位在free list中的话,否则m_freeNext恒等于-2),m_iobusy_lock为I/O并发控制锁,m_compress_lock为压缩并发控制锁,m_datablock_size为CU实际数据的大小,m_slot_hdr_lock保护整个CacheDesc的并发读写操作,m_flag表示槽位状态(包括全新、有效、freelist中、空闲、I/O中、错误等状态)*/ 


最后修改时间:2022-02-23 13:35:59
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论