存储引擎原理简述

1 MARS2 概述

MARS2 存储引擎主要是面向数据高速加载和查询而设计的,其内部采用有序存储省却搜索。

1.1 概念说明

1.1.1 排序键

MARS2 中,数据是有序存储的,在创建表时,需要通过创建索引的方式制定排序的顺序。 这个排序顺序涉及的字段,称为排序键。 排序键只能指定一次,不能修改,不能删除。 为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键。 比如设备监控表,可以采用事件时间戳和设备 ID 作为排序键。 如果排序键是文本类型,且能接受按照字节顺序排序,那么在这个列采用 COLLATE C 能够加速排序。

1.1.2 Run 和元信息

根据排序键,MARS2 中存储的数据是有序的,一段连续有序的数据称为 Run。 为了能够找到 Run 存储在哪里,会记录 Run 的存储位置,称为 Run 的元信息。 同时,元信息还记录 Run 的最小最大值,这样可以在查询时进行过滤。 插入数据时,会先在内存中对数据进行排序,因此 Run 的大小受制于排序内存大小。

1.1.3 合并

重叠的 Run 会造成读放大,降低查询效率。 因此,当磁盘上的 Run 数量超过一定值时,MARS2 会将磁盘上的多个 Run 加载到内存,进行排序,最后输出为一个 Run。 这个过程称为合并。合并过程中,数据依然可读可写:
(1)读数据时,只会读合并的输入文件
(2)写数据时,合并过程不会读新写入的数据
(3)读,写,合并三者之间不会互相阻塞

1.1.4 Level

为了让合并的输入文件尺寸相近(避免超大文件与小文件合并),Run 被组织到 Level 中,有 3 层:L0,L1,L2。 当数据合并完超过一定大小就会升级到更高的 Level 去,过程如下:

  1. 新插入的数据在 L0,当 Run 数量达到一定数量时(可配置,见配置参数 sortheap_automerge_threshold),触发 L0 合并,将 L0 所有的 Runs 合并为一个 Run。
  2. 如果结果 Run 大小超过 25MB(可配置,见配置参数 level0_upgrade_size),则将其升级到 L1。
  3. 如果升级到 L1 后,L1 的 Run 大小之和超过 1000MB(可配置,见 level1_upgrade_size),则触发合并,L1 的所有 Runs 合并为一个 Run。
  4. 如果结果 Run 大小超过 1000MB(可配置,见 level1_upgrade_size),则将其升级到 L2。 到达 L2 之后的 Run 不再合并。

1.1.5 列存

MARS2 采用列存方式存储数据,访问磁盘时只需要访问用到的列,这样减少了 I/O。 同一列数据类型相同,更容易进行压缩,节省磁盘空间。 另外,列式存储适合于向量化执行,可以大幅加速查询执行速度,具体见向量化执行引擎

1.1.6 MINMAX 过滤

前面提到了 Run 的元信息存储了最小最大值,用于在查询时进行过滤。 因为是通过元信息进行过滤,不需要加载数据本身,相比于顺序扫描(先访问数据本身,再进行过滤)其 I/O 开销更低,能更快的进行过滤。 使用 MARS2 建表时不会默认记录最小最大值,需要显式声明,例如:

CREATE TABLE metrics (
    ts              timestamp   ENCODING(minmax),
    dev_id          bigint      ENCODING(minmax),
    power           float,
    speed           float,
    message         text
) USING mars2;
CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);

排序键和非排序键都可以定义 MINMAX,定义在排序键上时效果更为显著,表达式中 MINMAX 全部大写或全部小写均可。 另外,最多只能给 32 个列定义 MINMAX。

1.1.7 MARS2 Btree 索引

除了 MINMAX,MARS2 还内置了一个索引(即建表时创建的索引)。 目前只能定义一个索引,即只能有一个全局排序。 与普通 Btree 索引不同,MARS2 Btree 索引:

  • 是稀疏的,因此尺寸更小
  • 由于数据本身是有序的,因此不存在索引扫描产生的随机 I/O

    1.1.8 压缩

    默认情况下,所有的数据列,采用 lz4 进行压缩。 用户也可以手动指定压缩算法,可以整个表指定,也可以单个列指定:

    CREATE TABLE metrics (
      ts              timestamp,
      dev_id          bigint,
      power           float,
      speed           float,
      message         text        ENCODING(compresstype=lz4, compresslevel=3)
    ) USING mars2 WITH (compresstype=zstd, compresslevel=3);
    CREATE INDEX ON metrics
    USING mars2_btree (ts, dev_id);

    上面例子中, message 字段是 lz4 压缩的,而其他字段是 zstd 压缩的。 当前支持的压缩算法有:(括号内是级别)

  • lz4(1-20)
  • zstd(1-19)
  • zlib(1-9)

1.2 MARS2 使用

1.2.1 创建 MARS2 表

在已创建 matrixts 扩展的前提下,最简洁的建表方式,只需要在 CREATE TABLE 语句加上 USING 子句,并创建索引即可。延伸示例请见表设计最佳实践

CREATE TABLE metrics (
    ts              timestamp,
    dev_id          bigint,
    power           float,
    speed           float,
    message         text
) USING mars2;

CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);

1.2.2 数据写入

数据通过 INSERT 写入到内存中,之后经历这些过程,生成一个 Run。 如果插入数据量较大,超过了排序内存,则会生成多个 Run。 排序内存的大小是可以配置的,详见上文配置项部分。

1.2.3 更新和删除

MARS2 暂时不支持更新和删除。
更新操作可以用 Unique Mode 代替更新操作。
删除操作只能通过删除或者 Truncate 分区表完成。

1.2.4 配置项

以下几个参数用于控制合并(Merge),其影响合并的方式见上文 Level 部分。

合并控制参数 单位 默认值 取值范围 描述
sortheap_automerge_threshold run 32 10 - 2048 用于控制 所有 的 MARS2 表,L0 达到多少个 Run 触发合并。如果要为表单独指定可以使用表选项 level0_merge_threshold
level0_merge_threshold run 32 1 - 2048 用于控制单表 L0 达到多少个Run 触发合并
level0_upgrade_size MB 25 1 - 10000 控制单表 L0 -> L1 升级的大小,当 L0 发生合并后结果 Run 超过这个大小将升级到 L1
level1_upgrade_size MB 1000 1 - 10000 控制单表 L1 -> L2 升级的大小,当 L1 发生合并后结果 Run 超过这个大小将升级到 L2
  • 如果每次插入较少的数据(每次插入对应一个 Run),很快就会触发 Merge,这种 Merge 效率不高。此时可以调高 sortheap_automerge_threshold / level0_merge_threshold 提高 Merge 输入的 Run 数量,降低 Merge 的频率。
  • 当 L0/L1 的 Merge 输出不到升级到 L1 的大小,下一轮的 Merge 会将其与新的 Run 进行合并,导致写放大。为了避免这种情况,可以降低 level0_upgrade_size / level1_upgrade_size 使得其升级到下一层。

以下参数用于压缩控制,调节压缩效果,如果配置过低压缩效果不明显,配置过高消耗内存较多。

压缩控制参数 单位 默认值 取值范围 描述
compress_threshold 元组 1200 1 - 100000 压缩阈值。用于控制单表多少元组(Tuple)进行一次压缩,是同一个单元中压缩的 Tuple 数上限

以下参数用于内存控制。这部分参数控制插入数据时,使用的排序内存的大小,当有多张分区表被插入时,每个分区表会分配 sortheap_sort_mem_core 配置的内存;如果不够用则扩大,但总量不会超过 sortheap_sort_mem

排序内存参数 单位 默认值 取值范围 描述
sortheap_sort_mem KB 2097152KB(2GB) 128KB - 2147483647KB (~ 2048GB) 控制单个插入的排序内存大小,如果插入目标表是分区表,它们将共享这个大小
sortheap_sort_mem_core KB 16384KB(16MB) 128KB - 2147483647KB (~ 2048GB) 控制单个分区表至少分配多少排序内存

2 HEAP 概述

HEAP 是 YMatrix 的默认存储引擎,又称作堆存储,从 PostgreSQL 继承而来,只支持行存储,不支持列存储及压缩,支持分区表。它基于 MVCC 机制实现,适用于有大量更新、删除需求的场景。

2.1 使用 MVCC 机制

MVCC(Multiversion Concurrency Control)机制通常被称为多版本管理。它的核心是对数据的更新、修改和删除处理。
多版本管理中,数据的更新和删除并不一定会在原数据上进行修改,而是需要创立一个新的版本,把原数据标记为失效的数据,再在新版本上增加新数据,数据具有多个版本。每个数据带有一个版本信息,且历史版本均会被保存。
在 MVCC 机制影响下,HEAP 表在处理更新和删除操作的时候,并没有真正删除数据,而只是依靠数据版本信息屏蔽了老的数据(控制了数据的可见性)。因此,HEAP 表大量进行更新或删除操作,占用的物理空间会不断增大,需要你有计划地定期清理老数据。

2.2 HEAP 使用

你可以运用以下 SQL 语句在 YMatrix 中创建一个 HEAP 表。

CREATE TABLE disk_heap(
    time timestamp with time zone,
    tag_id int,
    read float,
    write float
)
DISTRIBUTED BY (tag_id);

3 AO 概述

以 AOCO、AORO 为存储引擎的表统称为 AO(Append-Optimized)表,又称作追加优化表,可以支持插入、更新与删除操作,支持压缩。
AORO 支持行存储,AOCO 支持列存储。
AO 表无论是在表的逻辑结构还是物理结构上,都与 HEAP 表存在很大的不同。如上节所述 HEAP 表使用 MVCC 机制控制更新及删除操作后数据的可见性,而AO表则使用一个附加的 bitmap 表来实现,这个表的的内容就是表示 AO 表中哪些数据是可见的。 对于有大量更新及删除操作的 AO 表,同样需要计划地定期清理老数据,不过在AO表中,清理数据工具 vaccum 需要对 bitmap 进行重置并压缩物理文件,因此通常比 HEAP 进度慢。

注意!
MARS2、HEAP、AO 存储引擎详细信息、使用及最佳实践请见表设计最佳实践