GaussDB(DWS)基本IO框架

举报
Naibaoofficial 发表于 2022/03/02 10:37:47 2022/03/02
【摘要】 数仓的IO结构一直是影响性能的关键部分,数据库的操作最终还是要体现在最后的物理文件上,那么了解整个IO模型对于数仓的调优,索引的使用等都具有很好的指导作用。本篇博文主要讲解GaussDB(DWS)的基本IO框架,分别讲解行存,列存的读取和写入两个部分讲解GuassDB(DWS)的IO框架。

1. 前言

适用版本:【8.1.3及以上】

数仓的IO结构一直是影响性能的关键部分,数据库的操作最终还是要体现在最后的物理文件上,那么了解整个IO模型对于数仓的调优,索引的使用等都具有很好的指导作用。本篇博文主要讲解GaussDB(DWS)的基本IO框架,分别讲解行存,列存的读取和写入两个部分讲解GuassDB(DWS)的IO框架。

2. 存储结构

  • OID(Object identifiers):对象的唯一标识。
  • 每个表存在对应数据库的文件夹中,用relfilenode标识。

行存文件结构

例如表row1,可以直接查询对应的文件

test=# select pg_relation_filepath('row1');
 pg_relation_filepath
----------------------
 base/16385/55984
(1 row)
  • 每个表的读取写入以页(文件块)为基本单位,页的大小是一个BLCKSZ,默认8KB,其结构如下:
    Page/Block的基本结构

  • Tuple保存了当前一行的数据,分为Header和Data两块,头部保存元组的相关信息(列数,事务信息,是否有Toast表等)。

  • 每个Tuple最大为2kb,若Data过大无法压缩至2KB,则采用额外的Toast表存储,此时Tuple内的Data保存Toast表的相关信息。

3. 行存框架:

行存框架

这里面涉及到几个比较大的内核机制:

  • 本地缓存:

    这里面的本地缓存介绍了三个比较常用的缓存结构,这里直接引用了官方的英文解释。

    temp_buffers: Sets the maximum number of temporary buffers used by each database session. These are session-local buffers used only for access to temporary tables.

    work_mem: Specifies the amount of memory to be used by internal sort operations and hash tables before writing to temporary disk files.

    maintenance_work_mem: Specifies the maximum amount of memory to be used by maintenance operations, such as VACUUM, CREATE INDEX, and ALTER TABLE ADD FOREIGN KEY.

  • 共享内存:可由整个GaussDB(DWS)共享

    包括shared_bufferwal_buffer, 分别用来存放Page和Clog,Wal Segment。

  • WalWriter,BgWriter:

    主要是将共享内存的内容落盘,WalWriter一般是在事务提交时就需要落盘,但是有时候可以放弃一定的事务一致性原则,从而让WalWriter异步落盘加快速度。BgWriter负责将shared_buffer中的内容落盘。

  • 外存管理:

    负责上层与外存之间的文件交互。

4. IO管理框架:读取

读取的过程相对简单,就是从物理文件先装到shared_buffer中,然后从shared_buffers返回相关的结果。

行存读取

shared_buffers中就是以Page为单位进行存储的,因为每个Page的大小是固定的,所以shared_buffers能存放的page个数也就是确定的。这里面就需要考虑一个问题,因为这个资源是共享的,如果一个线程读取了大量的文件,这样势必会使得其他线程的缓存命中率下降。

GaussDB(DWS)在这里引入了Ringbuffer的机制,可以限制一个线程所使用的shared_buffers的大小,从而解决掉这个问题。

5. IO管理框架:写入

  • 写入操作是增加的新的元组,Update操作相当于先Delete,再Insert。

  • INSERT
    行存Insert

  • UPDATE
    行存更新

将旧元组标记为Dead,然后插入新的元组,由Vacuum负责清理。当然,这里面Data变为DELETE只是用来描述删除的是此Tuple,实际上Data当中的值是不变的。

5.1. 行存插入操作到提交的流程:

行存写入框架

  • 共享缓冲池(Shared Buffer Pool): 插入操作开始时,元组首先被写入内存中的共享缓冲池。这是所有后台进程共享的内存区域。
  • WAL(Write-Ahead Logging)缓冲区:元组写入共享缓冲池的同时,相关更改的WAL记录被写入WAL缓冲区。
  • LSN(Logic Sequence Number): WAL记录包含一个逻辑序列号(LSN),确保每条记录的位置唯一。
  • 物理文件: 虽然元组已经在共享缓冲池中,但尚未写入磁盘上的物理文件。这个写入动作依赖于后续的刷新或检查点操作。需要说明的是,GuassDB(DWS)依赖linux系统的写入(例如fwrite),所以仍然会有一定的延迟。
  • WAL写入者(WAL Writer):负责定期将WAL缓冲区中的记录写入磁盘上的WAL文件。
  • 后端(Backend):后端线程处理客户端请求,在这里是执行插入操作的进程。
  • Bgwriter 和 Checkpointer: 这些后台线程负责将共享缓冲池中的数据定期写入磁盘。Bgwriter 可以异步写入,而 Checkpointer 则在检查点发生时写入。
  • CLOG(Commit Log)缓冲区:事务提交时,其状态被写入 CLOG 缓冲区。CLOG 记录了事务的提交或回滚信息。
  • 检查点(Checkpoint):数据库执行的定期操作,确保所有之前的事务修改都已经写入磁盘。

5.2. WAL 和 CLOG 写入策略

5.2.1. WAL(Write-Ahead Logging)

  • 非即时写入

    • WAL 记录首先写入 WAL 缓冲区,并不是立即写入硬盘。这是为了减少磁盘 I/O,优化性能。
  • 定期写入

    • 由后台线程 WAL Writer 负责定期将 WAL 缓冲区中的记录刷新到磁盘。
  • 事务提交时确保写入

    • 事务在 COMMIT 时,必须确保该事务的所有 WAL 记录已经写入硬盘,从而保证事务的持久性。
    • GuassDB(DWS) 通过 synchronous_commit 参数来保证事务提交的持久性。
    • synchronous_commiton,提交事务会等待所有 WAL 记录写入后才返回成功。
    • 用户在收到 COMMIT 命令的成功响应后,可以确信事务已经安全提交。
    • 在系统故障的情况下,GaussDB(DWS) 的恢复机制会使用 WAL 来保证事务的完整性。
    • 这个机制确保了用户在事务级别上的数据持久性,即使用户无法直观感知 WAL 的具体写入细节。

5.2.2. CLOG(Commit Log)

CLOG(Commit Log)记录事务是否提交或回滚,而 WAL(Write-Ahead Logging)记录了事务的具体修改操作。两者的作用虽然相似,都是为了保证事务的持久性和数据库的恢复能力,但它们的设计和写入时机有所不同。

  • 可能延迟写入

    • CLOG 记录事务的提交状态,可能不会在每次事务提交时立即写入硬盘。
  • 由 Checkpointer 处理

    • CLOG 的写入通常在检查点时由 Checkpointer 或其他后台进程执行,确保所有已提交事务的 CLOG 记录被写入硬盘。

6. 列存的IO管理框架

6.1. 列存的存储单元

  • 列存的存储单元为CU(CStore Unit)
  • CU的大小为8k对齐
  • 适合大批量导入的场景
  • 同一列的CU存在一个新文件中,大于1GB时,切换到新文件中。
  • 列存用一个CUDesc的行存表描述CU的相关信息,可以理解成为一个Toast表。
  • CUDesc:行存表,记录CU的相关信息, 主要属性如下:
    1. col_id,cu_id: 第col_id列,第cu_id个CU
    2. min, max, row_count, size
    3. cu_mode: information mask(RLE,LZ4,Delta表等)
    4. cu_pointer:指向每一个CU,记录delete bitmap
    5. magic:和CU头部的magic相同,校验使用

6.2. CU结构

CU的结构

6.3. 列存索引

这里介绍两个索引,C-Btree和Psort,这里不做过多介绍。
主要涉及的是IO相关的内容。

6.3.1. C-Btree

  • 索引结构和行存无差别,同样以行存形式存储
  • C-Btree可以提升点查效率
  • 存储key->ctid(cu_id, offset)
  • 过程:
    1. 根据B-tree索引找到ctid集合
    2. 对集合进行批量排序(减少IO开销)
    3. 在CUDesc找到对应的cu_id,根据offset找到数据
  • 举例,等值查询 n=49, 范围查询 23<n<64。
    Btree查找举例

6.3.2. PSort

PSort是一个聚簇索引,对索引进行排序,然后将排序后的索引和行号存入一个新的表,用单独的列存表存储。
简单示意如下,图片来源:https://www.modb.pro/db/108155
Psort说明

6.3.3. IO管理框架:读取

  • 读取过程:

    1. 根据where条件,做MIN/MAX过滤的谓词条件
    2. 加载CUDesc
    3. MIN/MAX过滤
    4. 读取CU到CU Cache中
    5. 解析并填充
  • CacheMgr: 用来缓存CU到内存中,可以提高重复查询的性能。

  • CU的物理文件:
    1. CStore_1.0: 当前基本不怎么实用
    2. CStore_2.0: 重整了CU的文件结构,避免列数过多导致文件结构复杂。

列存读取简化

6.3.4. IO管理框架:写入

列存的插入要分两种情况,少量的插入和大量的插入,列存主要是对大批量数据设计的,因此为了弥补小量插入的打包CU性能开销,设计了一个delta行存表,用来记录插入结果,可以减少膨胀和提升性能,最后定期的整理。

  • 写入框架如下
    列存插入

列存的删除比较简单,如果是delta表,先从delta表中删除满足谓词条件的记录,然后在CUDesc表中更新待删除CU的delete_bitmap。

7. 总结

文章主要讲解了行存列存的设计结构,以行存和列存为例对GaussDB(DWS)的IO方面的主要机制做了梳理。分别对行存和列存和IO的读写做了解释。行存和列存由于组织方式不同,适合不同的场景,GaussDB(DWS)现在也有HStore。解决了CU上并发更新的锁冲突问题,delta表也解决了小CU的问题,无论是批量的读写还是单点的读写,性能都无限接近与上面介绍的行存和列存,欢迎大家使用!

8. 参考文档

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。