存储引擎原理简述
1 MARS3 概述
MARS3 存储引擎在 MARS2 基础上进行多项优化,采用行列结合的存储方式,同时实现数据的高速写入与极限压缩。
MARS3 支持通过 UPDATE
与 DELETE
子句实现数据更新与删除(Unique Mode 模式除外)。
MARS3 支持增删列,支持 COPY
、pg_dump
操作。
1.1 内部原理
对于每个 MARS3 单表而言,其内部均采用 LSM Tree 结构存储。LSM Tree(Log Structured Merge Tree)是一种分层的,有序的,面向磁盘的数据结构。其核心思想是充分利用磁盘性能进行批量的顺序写操作,性能远高于随机写。
MARS3 内部原理图如下:
我们将以概念逐层剖析的形式来解读上图。
1.1.1 排序键
- MARS3 中,数据是有序存储的。在创建表时,需要通过指定排序列(可以多列)的方式指定排序的顺序。这个排序顺序涉及的字段,称为排序键。
- 排序键只能指定一次,不能修改,不能新增,不能删除。
- 为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键。比如设备监控表,可以采用事件时间戳和设备 ID 作为排序键。
- 如果排序键是文本类型,且能接受按照字节顺序排序,那么在这个列采用 COLLATE C 能够加速排序。
- 指定排序键的 SQL 关键词:ORDER BY。
1.1.2 行存和列存
- MARS3 支持采用先行后列的存储方式存储数据,也支持直接写为列存,可通过
prefer_load_mode
参数设置加载模式,详见下文配置项。 - 如果采用行列混存的模式,则写入的数据会先以行存的形式储存下来,等数据量积累一定程度在进行列存转换。
- 和直接把数据变成列存相比,有以下几种好处:
- 对于高频、小批量数据来说写入速度更快
- 不需要大量的内存进行数据缓存
- 保证每个数据块元组数量的均匀
1.1.3 Run、增量文件和元信息
- 根据排序键,MARS3 中存储的数据是有序的,一段连续有序的数据称为 Run。
- Run 分成两种,为了能够高速写入,插入的数据会以行存 Run 的形式储存下来,之后为了方便读取和压缩,我们会把行存 Run 转换成列存 Run。
- 每个 Run 都有自己的增量文件,除了记录主要数据的 Data 文件之外,还有储存大数据块的 Toast 文件,储存删除信息的 Delta 文件,储存索引信息的 Index 文件,储存合并(详见下节"合并与回收")信息的 link 文件等(行存 Run 和列存 Run 的增量文件稍有不同)。
- 同时,为了知道这些文件的所在位置,我们额外记录一些信息,称为元信息。元信息会记录文件的存储位置,文件大小,数据条数,压缩状况等信息。
- Run(使用
rowstore_size
参数配置)的大小可以灵活的进行调节,以适应不同场景达到最优性能。
1.1.4 合并和回收
- Run 中如果数据范围有重叠,则会造成读放大,降低查询效率。因此,当磁盘上的 Run 数量超过一定值时,MARS3 会将磁盘上的多个 Run 进行归并排序,最后输出为一个 Run。这个过程称为合并。
- 合并过程中,数据依然可读可写:
- 读数据时,只会读合并的输入文件
- 写数据时,合并过程不会读新写入的数据
- 读,写,合并三者之间不会互相阻塞
- 合并完成后,参加合并的 Run 会根据事务 ID 自动决定何时不再被需要,并且标记为可回收状态。
1.1.5 Level
- 为了让合并的输入文件尺寸相近(避免超大文件与小文件合并),Run 被组织到 Level 中,最大可有 10 层:L0,L1,L2......L9。
- 每一层的 Run 个数达到一定数目,或者同一层多个 Run 的大小总和达到阈值都会触发合并,合成一个 Run 后升级到更高层去;并且为了加快 Run 的升级,允许同一层中同时进行多个合并任务。
1.1.6 MARS3 Brin 索引
- MARS3 支持创建 Brin 索引,支持 Brin 索引的删除和新增。
- 每个 Run 在生成的时候都会创建自己独立的 Brin 索引文件。
- 示例:
CREATE INDEX brin_idx ON t1 USING mars3_brin(time,tag_id);
1.1.7 压缩
- 默认情况下,MARS3 所有的数据列,采用 lz4 进行压缩。
- 支持手动指定编码链压缩算法,可以整个表指定,也可以单个列指定。
1.1.8 支持 MVCC 机制
- MVCC(Multiversion Concurrency Control)机制通常被称为多版本管理。它的核心是对数据的更新、修改和删除处理。
- 多版本管理中,数据的更新和删除并不一定会在原数据上进行修改,而是需要创立一个新的版本,把原数据标记为失效的数据,再在新版本上增加新数据,数据具有多个版本。每个数据带有一个版本信息,且历史版本均会被保存。
- MARS3 的更新和删除操作都不是采用原地修改数据的方式,而是依靠 Delta 文件和版本信息屏蔽掉了老数据,从而控制数据的可见性。
- 注意:持续进行更新或删除同一个 Run 的数据会让此 Run 的 Delta 文件占用的物理空间持续增加,但当当前 Run 的所有数据都被删除之后就不会再增加了。而且 MARS3 的合并操作可以自动清除已
Dead
的数据,当然你也可以有计划地定期使用VACUUM
清理已Dead
的数据。
1.1.9 数据写入
- 数据通过
INSERT
写入到内存中,再写入 L0 的 Run 中。 - L0 中的 Run 大小是可以配置的,详见下文配置项部分。
1.1.10 更新和删除
- MARS3 通过
DELETE
进行删除,删除会在对应 Run 的 Delta 文件中进行记录,在进行 Run 合并的时候真正把数据删除。 - MARS3 通过
UPDATE
进行更新,更新会先删除原本数据,再重新插入一条新数据。 - MARS3 的 Unique Mode 模式暂时不支持删除,更新则无需显式使用
UPDATE
子句,直接执行INSERT
子句即可自动完成操作。如果想要更新某个 Unique Key(即建表时指定的排序键所对应的具体键值)对应的某条数据,直接插入一条相同 Unique Key 的新数据即可。例如CREATE TABLE mars3_t(c1 int NOT NULL, c2 int) USING MARS3 WITH (uniquemode=true) ORDER BY (c1, c2);
,其中 Unique Key 即为(c1, c2)
。
注意!
如开启 Unique Mode,则ORDER BY
子句的第一个字段在定义时需要添加NOT NULL
约束。
1.2 MARS3 使用
1.2.1 创建 MARS3 表
在已创建 matrixts
扩展的前提下,最简洁的建表方式,只需要在 CREATE TABLE
语句加上 USING
子句,并附加 ORDER BY
子句。延伸示例请见表设计最佳实践。
=# CREATE TABLE metrics (
ts timestamp,
dev_id bigint,
power float,
speed float,
message text
) USING MARS3
ORDER BY (dev_id,ts);
注意!
MARS3 表支持创建 Brin 索引,但非必须创建;MARS3 表建表时必须使用ORDER BY
子句制定排序键。
1.2.2 配置项
注意!
此部分配置项为表级配置项,只能在创建数据表时使用WITH(mars3options='a=1,b=2,...')
子句配置(MARS2 同样支持的参数compress_threshold
除外,直接使用WITH
子句配置即可),适用于单表,且一旦配置便无法修改。更多信息请见 数据表配置参数。
以下参数用于调节 L0 层 Run 的大小,也可间接控制 L1 层之上的 Run 大小。
参数 | 单位 | 默认值 | 取值范围 | 描述 |
---|---|---|---|---|
rowstore_size | MB | 64 | 8 ~ 1024 | 用于控制 L0 Run 何时切换。当数据大小超过该值,将会切换下一个 Run |
以下参数用于设置压缩阈值,可用于调节压缩效果和改善读取效率,如果配置过低压缩效果不明显,配置过高消耗内存较多。
参数 | 单位 | 默认值 | 取值范围 | 描述 |
---|---|---|---|---|
compress_threshold | 元组 | 1200 | 1 ~ 100000 | 压缩阈值。用于控制单表每一列的多少元组(Tuple)进行一次压缩,是同一个单元中压缩的 Tuple 数上限 |
以下参数用于指定数据在 MARS3 中的加载模式。
参数 | 单位 | 默认值 | 取值范围 | 描述 |
---|---|---|---|---|
prefer_load_mode | normal | normal / bluk | 数据加载模式。normal 表示正常模式,新写入数据先写到 L0 层的行存 Run 中,积累到 rowstore_size 之后,落至 L1 层的列存 Run,相对于 bulk 模式会多一次 I/O,列存转换由同步变成了异步,但适用于 I/O 能力充足且对延迟敏感的高频次小批量写入场景;bluk 表示批量加载模式,适用于低频大批量写入场景,直接写至 L1 层的列存 Run,相对于 normal 模式,减少了一次 I/O,列存转换由异步变成了同步,适用于 I/O 能力不足且对延迟不敏感的低频大批量的数据写入 |
以下参数用于指定 Level 尺寸的放大系数。
参数 | 单位 | 默认值 | 取值范围 | 描述 |
---|---|---|---|---|
level_size_amplifier | 8 | 1 ~ 1000 | Level 尺寸的放大系数。Level 触发合并操作的阈值,计算方式为:rowstore_size * (level_size_amplifier ^ level) 。其值越大,读速越慢,写速越快。可以根据具体场景信息(写多读少/读多写少、压缩率等)来决定具体值。注意:确保每个 Level 的 run 数量不要过多,否则会影响查询性能,甚至阻止新数据插入 |
配置示例:
=# CREATE TABLE metrics (
ts timestamp,
dev_id bigint,
power float,
speed float,
message text
) USING MARS3
WITH (compress_threshold=1200,mars3options='rowstore_size=64')
ORDER BY (dev_id,ts);
1.2.3 工具函数
matrixts_internal.mars3_level_stats
:查看 MARS3 表每一个 Level 层级的状态,据此可以判断 MARS3 表的健康度,例如 Run 有没有按预期的进行合并,其个数是否符合预期等;matrixts_internal.mars3_files
:查看 MARS3 表文件状态,可以用来查看 MARS3 表的扩展文件和增量文件(Data 文件、Delta 文件、Index 文件等)是不是符合预期;matrixts_internal.mars3_info_brin
:查看 MARS3 表某个 Brin 索引的状态。
2 MARS2 概述
MARS2 存储引擎主要是面向数据高速加载和查询而设计的,其内部采用有序存储省却搜索。
2.1 内部原理
MARS2 表与 MARS3 相同,也采用 LSM Tree 结构存储。
MARS2 内部原理图如下:
我们将以概念逐层剖析的形式来解读上图。
2.1.1 排序键
- MARS2 中,数据是有序存储的,在创建表时,需要通过创建索引的方式制定排序的顺序。 这个排序顺序涉及的字段,称为排序键。
- 排序键只能指定一次,不能修改,不能删除。
- 为了最大化利用顺序性带来的性能提升,最好选择经常使用且过滤效果好的字段作为排序键。比如设备监控表,可以采用事件时间戳和设备 ID 作为排序键。
- 如果排序键是文本类型,且能接受按照字节顺序排序,那么在这个列采用
COLLATE C
能够加速排序。
2.1.2 Run 和元信息
- 根据排序键,MARS2 中存储的数据是有序的,一段连续有序的数据称为 Run。
- 为了能够找到 Run 存储在哪里,会记录 Run 的存储位置,称为 Run 的元信息。
- 同时,元信息还记录 Run 的最小最大值,这样可以在查询时进行过滤。
- 插入数据时,会先在内存中对数据进行排序,因此 Run 的大小受制于排序内存大小。
2.1.3 合并
- Run 中如果数据范围有重叠,则会造成读放大,降低查询效率。因此,当磁盘上的 Run 数量超过一定值时,MARS2 会将磁盘上的多个 Run 加载到内存,进行排序,最后输出为一个 Run。这个过程称为合并。
- 合并过程中,数据依然可读可写:
- 读数据时,只会读合并的输入文件
- 写数据时,合并过程不会读新写入的数据
- 读,写,合并三者之间不会互相阻塞
2.1.4 Level
- 为了让合并的输入文件尺寸相近(避免超大文件与小文件合并),Run 被组织到 Level 中,有 3 层:L0,L1,L2。
- 当数据合并完超过一定大小就会升级到更高的 Level 去,过程如下:
- 1.新插入的数据在 L0,当 Run 数量达到一定数量时(可配置,见配置参数
mars2_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。 - 5.到达 L2 之后的 Run 不再合并。
- 1.新插入的数据在 L0,当 Run 数量达到一定数量时(可配置,见配置参数
2.1.5 列存
- MARS2 采用列存方式存储数据,访问磁盘时只需要访问用到的列,这样减少了 I/O。
- 同一列数据类型相同,更容易进行压缩,节省磁盘空间。
- 另外,列式存储适合于向量化执行,可以大幅加速查询执行速度,具体见向量化执行引擎。
2.1.6 MINMAX 过滤
- 前面提到了 Run 的元信息存储了最小最大值,用于在查询时进行过滤。
- 因为是通过元信息进行过滤,不需要加载数据本身,相比于顺序扫描(先访问数据本身,再进行过滤)其 I/O 开销更低,能更快的进行过滤。
- 使用 MARS2 建表时不会默认记录最小最大值,需要显式声明。
首先创建 matrixts
扩展:
=# CREATE EXTENSION matrixts ;
显式声明记录最小最大值。
=# CREATE TABLE metrics (
ts timestamp ENCODING(minmax),
dev_id bigint ENCODING(minmax),
power float,
speed float,
message text
) USING MARS2;
创建 mars2_btree
索引。
=# CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);
- 排序键和非排序键都可以定义 MINMAX,定义在排序键上时效果更为显著,表达式中 MINMAX 全部大写或全部小写均可。
- 另外,最多只能给 32 个列定义 MINMAX。
2.1.7 MARS2 Btree 索引
- 除了 MINMAX,MARS2 还内置了一个索引(即建表时创建的索引)。
- 目前只能定义一个索引,即只能有一个全局排序。
- 与普通 Btree 索引不同,MARS2 Btree 索引:
- 是稀疏的,因此尺寸更小
- 由于数据本身是有序的,因此不存在索引扫描产生的随机 I/O
2.1.8 压缩
同 MARS3。
- 默认情况下,所有的数据列,采用 lz4 进行压缩。
- 支持手动指定编码链压缩算法,可以整个表指定,也可以单个列指定。
2.1.9 数据写入
- 数据通过
INSERT
写入到内存中,之后经历这些过程,生成一个 Run。 - 如果插入数据量较大,超过了排序内存,则会生成多个 Run。
- 排序内存的大小是可以配置的,详见 "2.2.2 配置项"。
2.1.10 更新和删除
- MARS2 暂时不支持更新和删除。
- 更新操作可以用 Unique Mode 代替更新操作。
- 删除操作只能通过删除或者
TRUNCATE
分区表完成。
2.2 MARS2 使用
2.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);
2.2.2 配置项
注意!
下述内容中表级配置项,指的是只能在创建数据表时使用WITH
子句配置,适用于单表,且一旦配置便无法修改的配置项。全局配置项指的是可在会话或系统级别配置,系统级别的修改需要执行mxstop -u
才能生效的配置项。更多信息请见 数据表配置参数。
以下几个参数用于控制合并(Merge),其影响合并的方式见上文 Level 部分。
合并控制参数 | 单位 | 默认值 | 取值范围 | 描述 |
---|---|---|---|---|
mars2_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 效率不高。此时可以调高 mars2_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 数上限,为表级配置项 |
以下参数用于内存控制。这部分参数控制插入数据时,使用的排序内存的大小,当有多张分区表被插入时,每个分区表会分配 mars2_sort_mem_core
配置的内存;如果不够用则扩大,但总量不会超过 mars2_sort_mem
。
排序内存参数 | 单位 | 默认值 | 取值范围 | 描述 |
---|---|---|---|---|
mars2_sort_mem | KB | 2097152KB(2GB) | 128KB - 2147483647KB (~ 2048GB) | 控制每一个单个插入的排序内存大小,如果插入目标表是分区表,它们将共享这个大小,为全局配置项 |
mars2_sort_mem_core | KB | 16384KB(16MB) | 128KB - 2147483647KB (~ 2048GB) | 控制每一个单个分区表至少分配多少排序内存,为全局配置项 |
表配置项示例:
=# CREATE TABLE metrics (
ts timestamp,
dev_id bigint,
power float,
speed float,
message text
)
USING MARS2
WITH (compress_threshold=1200,level0_merge_threshold=32);
=# CREATE INDEX ON metrics
USING mars2_btree (ts, dev_id);
全局配置项在会话级别修改值示例:
=# SET mars2_sort_mem TO 2097152;
全局配置项在系统级别修改值示例:
=# gpconfig -c mars2_sort_mem -v 2097152
=# \q
$ mxstop -u
3 HEAP 概述
HEAP 是 YMatrix 的默认存储引擎,又称作堆存储,从 PostgreSQL 继承而来,只支持行存储,不支持列存储及压缩。它基于 MVCC 机制实现,适用于有大量更新、删除需求的场景。
3.1 使用 MVCC 机制
在 MVCC 机制影响下,HEAP 表在处理更新和删除操作时,并没有真正删除数据,而只是依靠数据版本信息屏蔽了老的数据(控制了数据的可见性)。因此,HEAP 表大量进行更新或删除操作,占用的物理空间会不断增大,需要你有计划地定期使用 VACUUM
清理老数据。
3.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);
4 AO 概述
以 AOCO、AORO 为存储引擎的表统称为 AO(Append-Optimized)表,又称作追加优化表,可以支持插入、更新与删除操作,支持压缩。
AORO 支持行存储,AOCO 支持列存储。
AO 表无论是在表的逻辑结构还是物理结构上,都与 HEAP 表存在很大的不同。如上节所述 HEAP 表使用 MVCC 机制控制更新及删除操作后数据的可见性,而 AO 表则使用一个附加的 bitmap 表来实现,这个表的的内容就是表示 AO 表中哪些数据是可见的。
对于有大量更新及删除操作的 AO 表,同样需要计划地定期清理老数据,不过在 AO 表中,清理数据工具 vacuum
需要对 bitmap 进行重置并压缩物理文件,因此通常比 HEAP 进度慢。
注意!
MARS2、HEAP、AO 存储引擎详细信息、使用及最佳实践请见表设计最佳实践。