Compaction
对于基于 LSM 树的数据库,压缩是极其关键的。它将重叠的碎片化 SST 文件合并成一个有序的文件,丢弃已删除的数据,同时显著提高查询性能。
从 v0.9.1 版本开始,GreptimeDB 提供了控制 SST 文件如何压缩的策略:时间窗口压缩策略(TWCS)和严格窗口压缩策略(SWCS)。
概念
让我们从 GreptimeDB 中压缩的核心概念开始介绍。
SST 文件
当内存表刷新到持久存储(如磁盘和对象存储)时,会生成排序的 SST 文件。
在 GreptimeDB 中,SST 文件中的数据行按tag 列和时间戳组织,如下所示。每个 SST 文件覆盖特定的时间范围。当查询指定一个时间范围时,GreptimeDB 只检索可能包含该范围内数据的相关 SST 文件,而不是加载所有已持久化的文件。
通常,在实时写入工作负载中,SST 文件的时间范围不会重叠。然而,由于数据删除和乱序写入等因素,SST 文件可能会有重叠的时间范围,这会影响查询性能。
时间窗口
时间序列工作负载呈现出显著的“窗口”特征,即最近插入的行更有可能被读取。因此,GreptimeDB 将时间轴逻辑上划分为不同的时间窗口,我们更关注压缩那些落在同一时间窗口内的 SST 文件。
特定表的时间窗口参数通常是从最新 flush 到存储的 SST 文件推断出来的,或者如果选择了 TWCS,您可以在建表时手动指定时间窗口。
GreptimeDB 预设了一组窗口大小,它们是:
- 1 小时
- 2 小时
- 12 小时
- 1 天
- 1 周
- 1 年
- 10 年
如果未指定时间窗口大小,GreptimeDB 将在第一次压缩时推断窗口,通过从上述集合中选择能够覆盖所有要压缩文件的整个时间跨度的,最小的时间窗口作为时间窗口大小。
例如,在第一次压缩期间,所有输入 SST 文件的时间跨度为 4 小时,那么 GreptimeDB 将选择 12 小时作为该表的时间窗口,并将此参数持久化以便后续的压缩中使用。
GreptimeDB 将包含最近插入时间戳的窗口视为活跃窗口(active window),而将之前的那些窗口视为非活跃窗口(inactive window)。
有序组
有序组(sorted runs)是一个包含已排序且时间范围不重叠的 SST 文件的集合。
例如,一个表包含 5 个 SST,时间范围如下(全部包括在内):[0, 10]
, [12, 23]
, [22, 25]
,[24, 30]
,[26,33]
,我们可以找到 2 个有序组:
有序组的数量往往能够反映 SST 文件的有序性。更多的有序组通常会导致查询性能变差,因为特定时间范围的查询往往会命中多个重叠的文件。压缩的主要目标是减少有序组的数量。
层级
基于 LSM 树的数据库常常有多个层级,数据的键(key)会逐层进行合并。GreptimeDB 只有两个层级,分别是 0(未压缩)和 1(已压缩)。
压缩策略
GreptimeDB 提供了上述两种压缩策略,但在创建表时只能选择时间窗口压缩策略(TWCS)。严格窗口(SWCS)仅在执行手动压缩时可用。
时间窗口压缩策略(TWCS)
TWCS 主要旨在减少压缩过程中的读 / 写放大。
它将要压缩的文件分配到不同的时间窗口。对于每个窗口,TWCS 会识别有序组。如有序组的数量超过了允许的最大值,TWCS 会找到一个解决方案,在考虑合并惩罚的同时将其减少到阈值以下。如果有序组的数量没有超过阈值,TWCS 会检查是否存在过多的文件碎片,并在必要时合并这些碎片文件,因为 SST 文件数量也会影响查询性能。
对于窗口分配,SST 文件可能跨越多个时间窗口。为了确保不受陈旧数据影响,TWCS 根据 SST 的最大时间戳来进行分配。在时间序列工作负载中,无序写入很少发生,即使发生了,最近数据的查询性能也比陈旧数据更为重要。
TWCS 提供了 5 个参数供调整:
max_active_window_runs
: 活跃窗口中最大允许存在的有序组数量(默认为 4)max_active_window_files
: 活跃窗口中最大允许存在的文件数量(默认为 4)max_inactive_window_runs
: 非活跃窗口中最大允许存在的有序组数量(默认为 1)max_inactive_window_files
: 非活跃窗口中最大允许存在的文件数量(默认为 1)max_output_file_size
: compaction 产生文件的最大大小(默认无限制)
您可以为活跃窗口和非活跃窗口设置不同的阈值。这很重要,因为乱序写入通常发生在活跃窗口中。通过允许更多重叠文件存在于活跃窗口,TWCS 在数据摄取过程中减少了写放大,并在活跃窗口变为非活跃时合并所有这些文件。
以下图表显示了当最大活跃窗口允许的有序组数量为 1 时,活跃窗口中的文件如何被压缩:
- 在 A 中,有两个 SST 文件
[0, 3]
和[5, 6, 9]
,但由于这两个文件的时间范围不重叠,因此只有一个有序组。 - 在 B 中,一个新的 SST 文件
[1, 4]
被刷新,因此形成了两个有序组。然后将[0, 3]
和[1, 4]
合并为[0, 1, 3, 4]
. - 在 C 中,一个新的 SST 文件
[9, 10 ]
被刷新,并且它将与[5, 6, 10 ]
合并以创建[5, 6, 9, 10]
. - D 是最终状态,两个压缩后的文件中形成一个有序组。
指定 TWCS 参数
用户可以在创建表时指定前述的 TWCS 参数,例如:
CREATE TABLE monitor (
host STRING,
ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP() TIME INDEX,
cpu FLOAT64 DEFAULT 0,
memory FLOAT64,
PRIMARY KEY(host))
WITH (
'compaction.type'='twcs',
'compaction.twcs.max_active_window_runs'='4',
'compaction.twcs.max_active_window_files'='8',
'compaction.twcs.max_inactive_window_runs'='1',
'compaction.twcs.max_inactive_window_files'='2',
'compaction.twcs.max_output_file_size'='500MB'
);
严格窗口压缩策略(SWCS)和手动压缩
与 TWCS 根据 SST 文件的最大时间戳为每个窗口分配一个 SST 文件不同的是,严格窗口策略(SWCS)将 SST 文件分配给所有与此文件的时间范围重叠的窗口,正如其名称所示。因此,一个 SST 文件可能会包含在多个压缩输出中。由于其在压缩期间的高读取放大率,SWCS 并不是默认的压缩策略。然而,当用户需要手动触发压缩以重新组织 SST 文件布局时,它是有用的,特别是当单个 SST 文件跨越较大的时间范围而显著减慢查询速度时。GreptimeDB 提供了一个简单的 SQL 函数来触发手动压缩:
ADMIN COMPACT_TABLE(
<table_name>,
<strategy_name>,
[<strategy_parameters>]
);
<strategy_name>
参数可以是 twcs
或 swcs
(大小写不敏感),分别指定时间窗口压缩策略和严格窗口压缩策略。
对于 swcs
策略, <strategy_parameters>
指定用于拆分 SST 文件的窗口大小(以秒为单位)。例如:
ADMIN COMPACT_TABLE(
"monitor",
"swcs",
"3600"
);
+--------------------------------------------------------------------+
| ADMIN compact_table(Utf8("monitor"),Utf8("swcs"),Utf8("3600")) |
+--------------------------------------------------------------------+
| 0 |
+--------------------------------------------------------------------+
1 row in set (0.01 sec)
执行此语句时,GreptimeDB 会将每个 SST 文件按 1 小时(3600 秒)的时间跨度拆分成多个分块,并将这些分块合并为一个输出文件,确保没有重叠的文件。
下图展示了一次 SWCS 压缩的过程:
在图 A 中,有 3 个重叠的 SST 文件,分别是 [0, 3]
(也就是包含 0、1、2、3 的时间戳)、[3, 8]
和 [8, 10]
。
严格窗口压缩策略会将覆盖了窗口 0、4、8 的文件 [3, 8]
分别分配给 3 个窗口,从而分别和 [0, 3]
以及 [8, 10]
合并。
图 B 给出了最终的压缩结果,分别有 3 个文件: [0, 3]
、[4, 7]
和 [8, 10]
,它们彼此互相不重叠。