eygle.com   eygle.com
eygle.com eygle
eygle.com  
 

« 墨天轮国产数据库沙龙 | 胡彦军:华为GaussDB迁移工具解密 | Blog首页 | openGauss 数据库内存引擎 »

openGauss 数据库列存储引擎
modb.pro

本文来源于墨天轮:https://www.modb.pro/db/168711

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

* 总体来说,列存储有以下优势:* (1) 列的数据特征比较相似,适合压缩,压缩比很高,在数据量较大(如数仓)场景下会节省大量磁盘空间;压缩比高同时也会提高单位作业下的IO效率。

(2) 当表中列数比较多,但是访问的列数比较少时,列存储可以按需读取列数据,大大减少不必要的读IO,提高查询性能。

(3) 基于列批量数据向量运算,结合向量化执行引擎,CPU的缓存命中率比较高,性能比较好,更适合OLAP大数据统计分析的场景。

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

1. 列存储引擎的总体架构

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

CU划分方式.png

CU划分方式

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

压缩单元示意图.png

压缩单元示意图

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

列存引擎整体架构图.png

列存引擎整体架构图
注: Cmn 表示第m列的cuid是n的压缩单元。

每个CU对应一个CU Desc的记录,在CU desc里记录了整个CU的事务时间戳信息、CU的大小、存储位置、magic校验码、min/max等信息。

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

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

2. 列存的页面组织结构

上面9.3.1讲到了CUDesc表以及其用来记录元信息的目的。CUDesc的典型结构如图。

CUDesc的典型结构.png

CUDesc的典型结构

其中:

  • rowTupleHeader为传统行存记录的行头,其中包含了前面提到过的事务以及位置信息等,用来进行可见性判断等。

  • cu_mode实际为此CUDesc对应CU的infomask,记录了一些CU的特征信息(比如是否Full,是否有NULL等等)

  • magic是CUDesc与CU文件之间校验的关键信息。

  • min/max(最小值/最大值)为稀疏索引,后续会进一步展开。 而CU文件本身结构,则如图所示。

CU文件本身结构.png

CU文件本身结构

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

  • 列存储的CU中数据的删除,实际上是标记删除。删除操作,相当于是更新了CUDesc表中CU对应CUDesc记录的delete bitmap(删除位图)结构,标记列中某行对应数据已被删除,而CU文件数据不会被更改。这样可以避免删除操作带来的IO放大以及解压、压缩的高额CPU开销。这样的设计,也可以使得对于同一个CU的select(查询)和delete(删除)互不阻塞,提升并发能力。

  • 列存储CU中数据更新,则是遵循append-only(仅允许追加)原则的,即CU文件仅会向后进行延展扩充,亦或是启用新的CU文件,而不是在对应行在CU中的位置就地更新。

由于CU以及CUDesc的元数据管理模式,原有系统中的Vacuum机制实际上并不会非常有效的清除CU中已经失效的存储空间,因为Lazy Vacuum(清理数据时,只是标识无用行的状态可以录入新数据,不会影响对表数据的操作)仅能在CUDesc级别进行操作,在多数场景下无法对CU文件本身进行清理。列存储内部如果要对列存数据表进行清理,需要执行Vacuum Full(除了清理无用行,还会合并数据块,整个过程会锁定表)操作。

3. 列存的MVCC设计

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

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

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

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

  • 两个事务并发去读一个CU是可进行的,两个事务都可以拿到此CU对应CUDesc行级别的share lock(共享锁)。

  • 两个事务并发去更新一个CU,会因为在CUDesc上的锁冲突而触发一个事务回滚(当然,如果是read commited(读已提交)隔离级别并打开允许并发更新的开关,这里会做的事情是拿到此CUDesc最新版本的ctid,然后重跑一部分queryTree(查询树),来进行更新操作)。

  • 两个事务并行执行,一个事务对一个CU执行了delete操作并先行提交,另一个事务在repeatable read(可重读)的隔离级别下,其获取的快照,只能看到这个CUDesc在delete发生前的版本,这个版本中的CUDesc中的delete_bitmap(删除位图),对应数据没有被标记删除。也由于CU的行删除是标记删除的机制,因此数据在原有CU的数据文件中依旧可用,此事务依旧可以在其对应的快照下读到对应行。

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

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

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

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

4. 列存的索引设计 列存支持的索引设计有:

  • Btree索引

  • 稀疏索引

  • 聚簇索引

(1) 列存的Btree索引

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

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

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

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

(2) 列存的稀疏索引

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

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

稀疏索引图.png

稀疏索引图

注:txninfo 表示事务信息;CUPtr 压缩单元的指针;CUNone 表示肯定不命中;CUSome 表示可能有数据匹配; CUFull 表示压缩单元数据全命中。

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

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

数据模型较为离散时查询效果图.png

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

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

其示意图如下图所示。

聚簇索引生效前.png

聚簇索引生效前

聚簇索引生效后.png

聚簇索引生效后

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

5. 列存储自适应压缩

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

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

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

压缩比示意图.png

压缩比示意图

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

面向列的自适应压缩.png

面向列的自适应压缩

6. 列存储的持久化设计

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

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


历史上的今天...
    >> 2011-11-18文章:
    >> 2010-11-18文章:
    >> 2009-11-18文章:
    >> 2008-11-18文章:
    >> 2007-11-18文章:
           我的装修以及装修的生意
    >> 2006-11-18文章:
    >> 2005-11-18文章:
    >> 2004-11-18文章:
           使用USE_CONCAT提示

By enmotech on 2021-11-18 10:16 | Comments (0) | | 3437 |


CopyRight © 2004~2020 云和恩墨,成就未来!, All rights reserved.
数据恢复·紧急救援·性能优化 云和恩墨 24x7 热线电话:400-600-8755 业务咨询:010-59007017-7040 or 7037 业务合作: marketing@enmotech.com