openGauss存储技术(三)——列存储引擎

上一篇内容我们介绍了openGauss存储技术——行存储引擎,本文重点介绍openGauss列存储引擎。

openGauss列存储引擎

传统行存储数据压缩率低,必须按行读取,即使读取一列也必须读取整行。在分析性的作业以及业务负载的情况下,数据库往往会遇到针对大量表的复杂查询,而这种复杂查询中往往仅涉及一个较宽(表列数较多)的表中个别列。此类场景下,行存储以行作为操作单位,会引入与业务目标数据无关的数据列的读取与缓存,造成了大量IO 的浪费,性能较差。因此openGauss提供了列存储引擎的相关功能。创建表的时候,可以指定行存储还是列存储。

总体来说,列存储有以下优势:

(1)列的数据特征比较相似,适合压缩,压缩比很高,在数据量较大(如数据仓库) 场景下会节省大量磁盘空间,同时也会提高单位作业下的IO 效率。(2)当表中列数比较多,但是访问的列数比较少时,列存储可以按需读取列数据,大大减少不必要的读IO,提高查询性能。(3)基于列批量数据向量运算,结合向量化执行引擎,CPU 的缓存命中率比较高,性能比较好,更适合 OLAP大数据统计分析的场景。

(4)列存储表同样支持 DML操作和 MVCC,功能完备,且在使用角度上做了良好的兼容,基本是对用户透明的,方便使用。

(一)列存储引擎的总体架构

列存储引擎的存储基本单位是 CU(Compression Unit,压缩单元),即表中一列的一部分数据组成的压缩数据块。行存储引擎中是以行作为单位来管理,而当使用列存储时,整个表整体按照不同列划分为若干个 CU,划分方式如图1所示。

933d1580bd013c795802ad6d3f87079a.jpeg

图1 CU 划分方式

如图1所示,假设以6万行作为一个单位,则一个12万行、4列宽的表被划分为8个 CU,每个 CU 对应一个列上的6万个列数据。图中有列0、列1、列2、列3四列,数据按照行切分了两个行组(Row Group),每个行组有固定的行数。针对每个行组按照列做数据压缩,形成 CU。每个行组内部各个列的 CU 的行边界是完全对齐的。当然,大部分时候,CU 在经过压缩后,因为数据特征与压缩率的不同,文件大小会完全不同,如图2所示。

3e6d4f8aeae6beb49b84fe9daa6bd358.jpeg

图2 示意图

为了管理表对应的CU,与执行器层进行对接来提供各种功能,列存储引擎使用了CUDesc(压缩单元描述符)表来记录一个列存储表中CU 对应的元信息,如图3所示。

434ca13e53589c60efe56fb716ef98b4.jpeg

图3 列存储引擎整体架构图

注:Cmn表示第 m 列的、CUid是n(第n个)的压缩单元。每个 CU 对应一个 CUDesc的记录,在 CUDesc里记录了整个 CU 的事务时间戳信息、CU 的大小、存储位置、magic校验码、min/max等信息。

与此同时,每张列存储表还配有一张 Delta表,Delta表自身为行存储表。当有少量的数据插入到一张列存储表时,数据会被暂时放入 Delta表,等到到达阈值或满足一定条件或操作时再行整合为 CU 文件。Delta表可以帮助避免单点数据操作带来的加重的 CU 操作与开销。

设计采用级别的多版本并发控制,删除通过引入虚拟列映射 (Virtual Column Bitmap)来标记删除。映射(Bitmap)是多版本的。

(二)列存储的页面组织结构

上文讲到了CUDesc表及其用来记录元信息的目的。CUDesc的典型结构如图4所示。

97ea979565b25c11b8e59f088cb120ec.jpeg

图4  CUDesc的典型结构

其中:

(1)_rowTupleHeader为传统行存储记录的行头,其中包含了前面提到过的事务及位置信息等,用来进行可见性判断等。(2)cu_mode实际为此 CUDesc对应 CU 的infomask,记录了一些 CU 的特征信息(比如是否为 Full,是否有 NULL等)。(3)magic是 CUDesc与 CU 文件之间校验的关键信息。(4)min/max(最小值/最大值)为稀疏索引,后续会进一步展开介绍。

而 CU 文件结构如图5所示。

2ac09cf79751cfdd81b42c12a24444d0.jpeg

图5 文件结构

列存储在 CUDesc表的存储信息基础上设计了一套与上层交互的操作 API。除了上面列存储的页面组织结构以及文件管理中天然可以展示出的结构机制之外,列存储还有如下一些关键的技术特征:

(1)列存储的 CU 中数据的删除,实际上是标记的删除。删除操作,相当于更新了CUDesc表中CU 对应CUDesc记录的删除位图(delete bitmap)结构,标记列中某行对应数据已被删除,而CU 文件数据不会被更改。这样可以避免删除操作带来大量的IO开销及压缩、解压的高额 CPU 开销。这样的设计,也可以使得对于同一个 CU 的查询(select)和删除(delete)互不阻塞,提升并发能力。(2)列存储CU 中数据更新,则是遵循仅允许追加(append-only)原则的,即CU 文件仅会向后进行延展扩充,抑或是启用新的 CU 文件,而不是就对应行在 CU 中的位置就地更新。(3)由于 CU 以及 CUDesc的元数据管理模式,原有系统中的 Vacuum 机制实际上并不会非常有效地清除 CU 中已经失效的存储空间,因为 LazyVacuum(清理数据时,只是标识无用行的状态,使得空间可以复用,不会影响对表数据的操作)仅能在CUDesc级别进行操作,在多数场景下无法对 CU文件本身进行清理。列存储内部如果要对列存储数据表进行清理,需要执行 VacuumFull(除了清理无用行,还会合并数据块,整个过程会锁定表)操作。

(三)列存储的 MVCC设计

理解了 CU、CUDesc的基本结构,以及 CUDesc的管理,或者说是其“代理”角色,列存储的 MVCC设计以及管理,实际上就非常好理解了。

由于列存储的操作基本单位 CU 是由 CUDesc表中的行进行管理的,因此列存储表的CU 可见性判断也是由CUDesc的行头信息,按照传统的行存储可见性进行判断的。

同样的,列存储可见性的单位也是CU 级别(CUDesc),不同于行存储的 Tuple级别。

列存储表的并发控制是 CU 文件级别的,实际上也等同于其 CUDesc代理表的CUDesc行之间的并发控制。多个事务之间在一个 CU 上的并发管控,实际上取决于其在对应的 CUDesc记录上是否冲突。例如:

(1)两个事务并发去读一个CU 是可行的,两个事务都可以拿到此CU 对应 CUDesc 行级别的共享锁(sharelock)。(2)两个事务并发去更新一个 CU,会因为在 CUDesc上的锁冲突而触发一个事务回滚[当然,如果是读已提交(read committed)隔离级别并打开允许并发更新的开关,这里会做的事情是拿到此 CUDesc最新版 本 的 ctid,然后重运行一部分查询树 (queryTree)来进行更新操作。此部分内容,后面文章将会介绍]。(3)两个事务并行执行,一个事务对一个 CU 执行了删除操作并先行提交,则另一个事务在可重读(repeatableread)的隔离级别下,其获取的快照只能看到这个CUDesc在操作发生前的版本,这个版本的 CUDesc中的删除位图(delete_bitmap)对应数据没有被标记删除,也由于 CU 的行删除是标记删除的机制,因此数据在原有 CU 的数据文件中依旧可用,此事务依旧可以在其对应的快照下读到对应行。

删除 CU 中部分数据所进行的实际操作如图6所示。

038fe5dfcb6b1f511e529f58440df9ad.jpeg

图6 删除 CU 中部分数据所进行的实际操作

从上面的几个例子可以看出,列存储对于更新的仅允许追加策略以及对于删除操作的标记删除方式,对于列存储事务 ACID的支持,是至关重要的。

(四)列存储的索引设计 

列存储支持的索引设计有:

■ B树索引;

■ 稀疏索引;

■ 聚簇索引。

1.列存储的B树索引 

列存储引擎在 B树索引的支持角度,与传统的行存储引擎无本质差别。对于一般用于应对大数据批量分析性负载的列存储引擎来说,B树索引有助于帮助列存储大大提升自身的点查效率,更好地适应混合负载。

行存储相关 B树索引的索引页面上,存储的是key→ctid(键→行号)的映射,在列存储的场景下,这个映射依旧为key→ctid,但列存储的结构并不能像行存储一样,通过ctid中的块号(block number)和偏移量(offset)直接找到此行数据在数据文件页面中的位置。列存储ctid中记录的是(cu_id,offset),要通过 CUDesc结构来进行查找。

在基于 B树索引的扫描中,从索引中拿到ctid后,需要在对应的 CUDesc表中,根据 CUDesc在cu_id列的索引找到对应的 CUDesc记录,并由此打开对应的 CU 文件,根据偏移量找到数据。

如果此操作设计大量的存储层性能开销,因此列存储的 B树索引,与列存储的其他操作一样,统一都为批量操作,会根据 B树索引找到ctid的集合,然后对此集合进行排序,再批量地对排序后的ctid进行 CU 文件级别的查找与操作。这样可以做到顺序单调地进行索引遍历,大大减少了反复操作文件带来的 CPU 以及IO 开销。

2.列存储的稀疏索引

列存储引擎每个列自带 min/max稀疏索引,每个CUDesc存储该CU 的最小值和最大值。

那么在查询的时候,可以根据查询条件做简单的 min/max判断,如果查询条件不在(min,max)范围内,肯定不需要读取这个 CU,可以大大地减少IO 读取的开销,稀疏索引如图7所示。

8a529c4a3c199e07790bba64764a01e4.jpeg

图7稀疏索引

注:txn_info表示事务信息;CUPtr表示压缩单元的指针;CU-None表示肯定不命中;CU-Some表示可能有数据匹配;CU_Full表示压缩单元数据全命中。

3.列存储的聚簇索引

列存储表在建立时可以选择在列上建立聚簇索引(partial sort index)。

如果业务的初始数据模型较为离散,那么稀疏索引在不同 CU 之间的 min、max会有大量交集,这种情况下在给定谓词对列存储表进行检索的过程中,会出现大量的CU 误读取,甚至可能导致其查询效率与全表扫描近似。如图8所示,查询2基本命中了所有 CU,min/max索引没有能够有效筛选。

e7b3655c86bdc08b1fc30ce9e0980a21.jpeg

图8数据模型较为离散时的查询效果图

聚簇索引可以对部分区间内的数据做相应的排序(一般区间会包含多个CU所覆盖的行数),可以保证 CU 之前交集尽量少,可以极大地提升在数据离散场景下稀疏索引的效率。

其示意图如图9和图10所示。

3c75c5ba182a1e64eff4a9ecc2796dd3.jpeg

图9 聚簇索引生效前

a664c198f2a25552f8cfa726522189c8.jpeg

图10 聚簇索引生效后

同时,聚簇索引会使得 CU 内部的数据临近有序,提升 CU 文件本身的压缩比以及压缩效率。

(五)列存储自适应压缩

每个列自适应选择压缩,支持差分编码(delta value encoding)、游 程 编 码 (Run length encoding)、字典编码(dictionary encoding)、LZ4、zlib等混合压缩。根据数据特性的不同,压缩比一般可以有3X~20X。

列存储引擎支持低、中、高三种压缩级别,用户在创建表的时候可以指定压缩级别。

导入1TB原始数据量,分别测试低、中、高三种压缩级别,入库后数据大小分别是100GB、73GB、61GB,如图11所示。

cf0f5cab6d1022cf3f716ee14af50cae.jpeg

图11压缩比示意图

每次数据导入,首先对每列的数据按照向量组装,对前几批数据做采样压缩,根据数值类型和字符串类型,选择尝试不同的压缩算法。一旦采样压缩完成后,接下来的数据就选择优选的压缩算法了。如图12所示,面向列的自适应压缩主要分为数值压缩和字符压缩。其中对 Numeric小数类型,会转换为整数后,再进行数值压缩。对数值型字符串,也会尝试转换为整数再进行数值压缩。

1272b49796e652f7c0a11df776702f88.jpeg

图12 面向列的自适应压缩

(六)列存储的持久化设计

在列存储的组织结构与 MVCC机制的介绍中提到,列存储的存储单位由 CUDesc和CU文件共同组成,其中 CUDesc记录了CU相关的元信息,控制其可见性,实际上充当了一个 “代 理”的角色。但是CUDesc和CU,实质上还是分离的文件状态。CUDesc表本质上还是行存储表,其持久化流程遵从行存储的共享缓冲区脏页与 Redo日志的持久化流程,在事务提交前,CUDesc的改动会被记录在 Redo日志中进行持久化。单个 CU 文件本身,由于含有大量的数据,使用正常的事务日志进行持久化需要消耗大量的事务日志,引入非常大的性能开销,并且恢复也十分缓慢。因此根据其应用场景,仅允许追加(append-only)的属性及与 CUDesc的对应关系,列存储的 CU 文件,为了确保 CUDesc和 CU 持久化状态的一致,在事务提交、CUDesc对应事务日志持久化前,会先行强制刷盘(Fsync),来确保事务改动的持久化。

由于数据库主备实例的同步也依赖事务日志,而 CU 文件并不包含在事务日志内,因此在与列存储同步时,主备实例之间除去正常的日志通道外,还有连接的数据通道,用于传输列存储文件。CUDesc的改动会通过日志进行同步,而 CU 文件则会被直接通过数据通道传输到备机实例,并通过 BCM(bitchangemap)文件来记录主备实例之间文件的同步状态。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值