使用新的 TTL move,将数据存储在合适的地方

前言

本文翻译自 Altinity 针对 ClickHouse 的系列技术文章。面向联机分析处理(OLAP)的开源分析引擎 ClickHouse,因其优良的查询性能,PB 级的数据规模,简单的架构,被国内外公司广泛采用。

阿里云 EMR-OLAP 团队,基于开源 ClickHouse 进行了系列优化,提供了开源 OLAP 分析引擎 ClickHouse 的云上托管服务。EMR ClickHouse 完全兼容开源版本的产品特性,同时提供集群快速部署、集群管理、扩容、缩容和监控告警等云上产品功能,并且在开源的基础上优化了 ClickHouse 的读写性能,提升了 ClickHouse 与 EMR 其他组件快速集成的能力。访问https://help.aliyun.com/document_detail/212195.html 了解详情。

译者: 何源(荆杭),阿里云计算平台事业部高级产品专家

image.png

(图源Altinity,侵删)

使用新的 TTL move,将数据存储在合适的地方

目录

  • TTL 表达式
  • 设置演示
  • 在不重新启动服务器的情况下添加存储配置
  • 创建使用 TTL move 的表
  • 关于 TTL delete 的题外话
  • 将 TTL move 添加到现有表中
  • 一些关于方式和时机的问题
  • 结论
  • 后续

多卷存储在许多用例中是至关重要的。它有助于降低存储成本,并将最关键的应用数据放在最快的存储设备上,从而提高查询性能。监控数据是一个经典用例。随着时间的推移,数据的价值会迅速下降。前一天、上一周、上一月和上一年的数据在访问模式上截然不同,这又对应于各种不同的存储需求。

因此,根据数据时间来适当放置数据就非常重要了。ClickHouse TTL move 现在提供了一种机制来实现这一点。ClickHouse 基于时间的 TTL move 的最大的优势在于非常直观,直接对应于人们的日历时间概念。TTL move 大幅简化了与业务要求相对应的多卷存储的设置。

多卷功能大大增加了 ClickHouse 服务器的容量。它通过提供存储策略,将磁盘排列成卷,并在它们之间建立关系,从而实现分层存储。然而,存储策略本身并没有提供太多的灵活性来控制 ClickHouse 保存数据的位置。用户要么使用 ALTER TABLE [db.]table MOVE PART|PARTITION 命令手动操作,要么依赖 move factor 参数来实现基于已用空间比例的卷间数据的简单分配。

在本文中,我们将研究新表 TTL move,它允许用户定义表达式,而这些表达式可以自动将数据移动到用户在存储配置中指定的特定磁盘或卷。新 TTL move 大大增强了多卷存储能力,并提供了充分使用多卷存储能力所需的细粒度控制。

TTL 表达式

MergeTree 是目前唯一支持 TTL 表达式的引擎系列。ClickHouse 首先增加了对 TTL 表达式的支持,以启用自动删除突变。TTL 表达式只是一种 SQL 表达式,它必须评估为 Date 或 DateTime 数据类型。这种表达式可以使用显式时间间隔(用到 INTERVAL 关键字)或使用 toInterval 转换函数。例如,

1
2
TTL date_time + INTERVAL 1 MONTH
TTL date_time + INTERVAL 15 HOUR

或者使用 toInterval 转换函数,可以得到以下表达式

1
2
TTL date_time + toIntervalMonth(ttl)
TTL date_time + toIntervalHour(ttl)

或者只是

1
2
TTL date_time + INTERVAL ttl MONTH
TTL date_time + INTERVAL ttl HOUR

我们可以把这些表达式分配给一个表,分配后可称为 表达式。一个表只能有一个表达式用于删除,有多个表达式用于将分片自动移动到磁盘或卷。例如,假设我们有一个存储策略,其中定义了一个卷 slow_volume 和一个磁盘 slow_disk,那么表 TTL 表达式可以如下所示

1
2
3
TTL date_time + INTERVAL 1 MONTH DELETE,
  date_time + INTERVAL 1 WEEK TO VOLUME 'slow_volume',
  date_time + INTERVAL 2 WEEK TO DISK 'slow_disk';

有一点值得注意。因为 ClickHouse 首先增加了对 delete TTL 表达式的支持,如果没有指定 TO DISK 或 TO VOLUME 子句,则假定 DELETE 子句。因此,我们建议务必要明确地使用 DELETE 子句来确定要将哪一个 TTL 表达式用于删除。

设置演示

如果你未研究过多卷存储,或未使用过 TTL delete 或 move 表达式,我们建议你使用最新的 ClickHouse 版本 20.3.2.1。我们将在本文的其余部分使用这一版本。

1
2
3
4
$ clickhouse-client
ClickHouse client version 20.3.2.1 (official build).
Connecting to localhost:9000 as user default.
Connected to ClickHouse server version 20.3.2 revision 54433.

出于演示目的,我们将使用 OnTime 数据库(1987 至 2018 年的美国民用飞行数据)。为了方便,我们将下载并使用预先制作的分区。

1
2
3
4
5
$ curl -O https://clickhouse-datasets.s3.yandex.net/ontime/partitions/ontime.tar
$ tar xvf ontime.tar -C /var/lib/clickhouse # path to ClickHouse data directory
$ # check permissions of unpacked data, fix if required
$ sudo service clickhouse-server restart
$ clickhouse-client --query "select count(*) from datasets.ontime"

当然,如果不使用多卷存储,TTL move 表达式就没有意义了。因此,我们将通过创建不同的文件夹来模拟多个存储设备,这些文件夹将代表具有不同速度和容量的已安装存储设备。

1
2
3
4
/data/fast
/data/med0
/data/med1
/data/slow

在不重新启动服务器的情况下添加存储配置

目前,我们的服务器只使用默认磁盘。我们只要查看 system.disks 表就能发现这一点。

1
2
3
4
5
6
7
8
:) select * from system.disks

SELECT *
FROM system.disks

┌─name────┬─path─────────────────┬──free_space─┬──total_space─┬─keep_free_space─┐
default/var/lib/clickhouse/37705834496468514799616 │               0
└─────────┴──────────────────────┴─────────────┴──────────────┴─────────────────┘

我们需要更多的存储空间,并且可以在不重新启动服务器的情况下添加新磁盘。我们最近在 ClickHouse 中添加了此功能。现在可以先睹为快。通过将 storage.xml 放入我们的 /etc/clickhouse-server/config.d/ 文件夹,可以定义存储配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<yandex>
<storage_configuration>
  <disks>
    <default>
      <keep_free_space_bytes>1024</keep_free_space_bytes>
    </default>
    <fast>
      <path>/data/fast/</path>
    </fast>
    <med0>
        <path>/data/med0/</path>
    </med0>
    <med1>
        <path>/data/med1/</path>
    </med1>
    <slow>
        <path>/data/slow/</path>
    </slow>
  </disks>
  <policies>
    <default>
      <volumes>
        <default>
          <disk>default</disk>
        </default>
        <fast>
          <disk>fast</disk>
        </fast>
        <med>
          <disk>med0</disk>
          <disk>med1</disk>
        </med>
        <slow>
          <disk>slow</disk>
        </slow>
      </volumes>
    </default>
  </policies>
</storage_configuration>
</yandex>

如果我们使用 SYSTEM RELOAD CONFIG 命令重载该配置,那么应该能够在 system.disks 表中看到新磁盘。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
:) SYSTEM RELOAD CONFIG
:) select * from system.disks

┌─name────┬─path─────────────────┬──free_space─┬──total_space─┬─keep_free_space─┐
default/var/lib/clickhouse/37993152512468514799616 │               0
│ fast   │ /data/fast/         │ 37993152512468514799616 │               0
│ med0   │ /data/med0/         │ 37993152512468514799616 │               0
│ med1   │ /data/med1/         │ 37993152512468514799616 │               0
│ slow   │ /data/slow/         │ 37993152512468514799616 │               0
└─────────┴──────────────────────┴─────────────┴──────────────┴─────────────────┘
我们可以进入 system.storage_policies 表检索存储策略。
:) select * from system.storage_policies

┌─policy_name─┬─volume_name─┬─volume_priority─┬─disks───────────┬─max_data_part_size─┬─move_factor─┐
default     │ default     │               1 │ ['default']     │                 0 │         0.1
default     │ fast       │               2 │ ['fast']       │                 0 │         0.1
default     │ med         │               3 │ ['med0','med1'] │                 0 │         0.1
default     │ slow       │               4 │ ['slow']       │                 0 │         0.1
└─────────────┴─────────────┴─────────────────┴─────────────────┴────────────────────┴─────────────┘

创建使用 TTL move 的表

现在我们来看看在创建新表时如何定义 TTL move。我们将使用截至 2010 年的飞行数据。最后三年的数据我们将保留在 fast 卷上,3-5 年之间的数据保留在 med 卷上,5-7 年之间的数据应进入 slow 存储,而更早的数据将删除。我们可以通过下表定义来实现这样的方案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE ontime_chunk
ENGINE = MergeTree()
PARTITION BY Year
ORDER BY FlightDate
TTL FlightDate TO VOLUME 'fast',
  FlightDate + INTERVAL 3 YEAR TO VOLUME 'med',
  FlightDate + INTERVAL 5 YEAR TO VOLUME 'slow',
  FlightDate + INTERVAL 7 YEAR DELETE
AS
SELECT *
FROM datasets.ontime
WHERE Year >= 2010

← Progress: 55.09 million rows, 40.13 GB (472.19 thousand rows/s., 344.00 MB/s.)
██████████████████████████████████████████████████████▌ 96%Ok.
0 rows in set. Elapsed: 116.659 sec. Processed 55.09 million rows, 40.13 GB (472.19 thousand rows/s., 344.00 MB/s.)

请注意,TTL move 不支持 MergeTree 表的旧函数样式语法。

创建并填充表后,我们可以查看表分片的存储位置。你可以通过查看 system.parts 表实现这一点。下面的查询可以给我们一些基本的统计数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SELECT
  partition,
  disk_name,
  count(),
  sum(rows),
  max(bytes_on_disk),
  avg(bytes_on_disk)
FROM system.parts
WHERE active AND (table = 'ontime_chunk')
GROUP BY
  partition,
  disk_name
ORDER BY partition DESC

┌─partition─┬─disk_name─┬─count()─┬─sum(rows)─┬─max(bytes_on_disk)─┬─avg(bytes_on_disk)─┐
2018     │ fast     │       7 │   6033426 │         26450814586241164.71428572
2017     │ fast     │       4 │   5674621 │         274419521 │       138846953.25
2016     │ med1     │       3 │   2169891 │         14747512470784017.66666667
2016     │ med0     │       3 │   3447767 │         316232407112364801.66666667
2015     │ med1     │       2 │   5265093 │         260304509 │         259205244
2015     │ med0     │       1 │   553986 │           54923408 │           54923408
2014     │ slow     │       2 │   5819811 │         315955553 │       288535865.5
2013     │ slow     │       7 │   5089209 │         266864685 │           71928783
2012     │ slow     │       6 │   436874 │           98185207203465.166666667
2011     │ slow     │       2 │     62029 │           5946491 │           2973249
2010     │ slow     │       3 │   113398 │           88384003741370.6666666665
└───────────┴───────────┴─────────┴───────────┴────────────────────┴────────────────────┘

如上所示,ClickHouse 对插入内容应用了 TTL move,分片几乎都处于我们预期的位置。为什么说是几乎?因为 TTL delete 表达式与众不同。ClickHouse 在插入过程中不评估这些,因此我们在 slow 磁盘上看到的仍是我们想要删除的 2013 到 2010 年的数据。

关于 TTL delete 的题外话

ClickHouse 对于 TTL move 和 delete 的处理方式不同。我特意在上面的示例中纳入了一个 TTL delete 表达式来说明这一点。这是因为,TTL delete 会导致代价高昂的变异操作。因此,与仅在磁盘之间复制分片的 TTL move 相比,可能代价更高昂。所以在使用 TTL 时请记住这一点。

但是鉴于大多数用户会同时用到 TTL delete 和 move,因此必须指出的是,ClickHouse 通过“merge_with_ttl_timeout”MergeTree 表设置来控制 TTL delete 的频率。默认设置为 24 小时,并定义了****可重复进行基于 TTL 的合并的最小时间(秒)。此设置实际上意味着,每 24 小时(或者发生了后台合并时)仅在一个分区上执行一次 TTL delete。那么在最坏的情况下,现在 ClickHouse 将最多每 24 小时删除一个与 TTL delete 表达式匹配的分区。

这种行为可能并不理想,所以如果你想让 TTL delete 表达式更快地执行删除,你可以修改表的 merge_with_ttl_timeout 设置。例如,我们可以将其设置为一小时,如下所示。

1
ALTER TABLE [db.]table MODIFY SETTING merge_with_ttl_timeout = 3600

现在你应该看到,ClickHouse 正在根据你的 TTL delete 表达式每小时删除分片。当然,如果你的表不是太大,可以强制使用 OPTIMIZE TABLE [db.]table FINAL 语句。但是对于大表不建议使用。

将 TTL move 添加到现有表中

我们已经看过如何使用预先定义的 TTL move 创建表。但是,如果你已经有了一个表,或者想更改现有的 TTL move 表达式,则必须使用 ALTER TABLE [db.]table MODIFY TTL 命令。

请注意,在修改 TTL 表达式时,必须重新列出所有 TTL。所有 move 表达式和 delete 表达式(如果存在)。

让我们重新使用上表并更改 TTL 表达式。我们现在希望将除了最后三年的数据外的其他数据都放在 slow 卷上,或删除七年之前数据。

1
2
3
4
ALTER TABLE ontime_chunk
  MODIFY TTL FlightDate TO VOLUME 'fast', FlightDate + toIntervalYear(3) TO VOLUME 'slow', FlightDate + toIntervalYear(7)
Ok.
0 rows in set. Elapsed: 0.037 sec.

很快啊!我说停停,有什么数据移动了吗?并没有。新的 TTL 表达式将只分配给新分片,要么是在插入的时候,要么是因为新分片作为后台合并操作的结果而创建。对于现有分片,可以通过使用 ALTER TABLE [db.]table MATERIALIZE TTL 语句实现 TTL 来应用新的 TTL。如果对我们的表执行它,该命令将很快返回。

1
2
3
4
ALTER TABLE ontime_chunk
  MATERIALIZE TTL
Ok.
0 rows in set. Elapsed: 0.044 sec.

这只是重写你可以在分片的文件夹内找到的 ttl.txt 文件。例如,我们可以看看随机分片有什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
$ sudo cat /data/fast/data/default/ontime_chunk/2017_113_113_0/ttl.txt
ttl format version: 1
{
"table": {
  "min":1710457200,
  "max":1710975600
},
"moves":[
    {"expression":"FlightDate","min":1489532400,"max":1490050800},
    {"expression":"plus(FlightDate, toIntervalYear(3))","min":1584226800,"max":1584745200},
    {"expression":"plus(FlightDate, toIntervalYear(5))","min":1647298800,"max":1647817200}
]
}

在你执行 MATERIALIZE TTL 命令后,ClickHouse 会在下一个后台周期开始将分片移动到新位置。在我们的示例中,这没有花很长时间。再来看看 system.parts 表,我发现分片移动到了新位置,有些则因为后台合并而被删除。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
SELECT
  partition,
  disk_name,
  count(),
  sum(rows),
  max(bytes_on_disk),
  avg(bytes_on_disk)
FROM system.parts
WHERE active AND (table = 'ontime_chunk')
GROUP BY
  partition,
  disk_name
ORDER BY partition DESC

┌─partition─┬─disk_name─┬─count()─┬─sum(rows)─┬─max(bytes_on_disk)─┬─avg(bytes_on_disk)─┐
2018     │ fast     │       8 │   6033426 │         372476291 │         75689482.5
2017     │ fast     │       8 │   5674621 │         304683038 │       69514551.875
2016     │ slow     │       3 │   5617658 │         396503243183260415.33333334
2015     │ slow     │       2 │   5819079 │         329783074 │       286661116.5
2014     │ slow     │       7 │   5819811 │         244641752 │           85566643
2013     │ slow     │       5 │   5089209 │         383141486 │       102324330.2
2012     │ slow     │       1 │         0 │                 3 │                 3
2011     │ slow     │       2 │         0 │                 3 │                 3
2010     │ slow     │       3 │         0 │                 3 │                 3
└───────────┴───────────┴─────────┴───────────┴────────────────────┴────────────────────┘

一些关于方式和时机的问题

在了解如何使用 TTL move 表达式之后,让我们看看 ClickHouse 如何以及何时评估 TTL move 表达式。

如何评估 TTL move?

ClickHouse 使用一个专门的后台线程池来评估 TTL move 表达式。该池的行为由以下参数控制,这些参数可以在 config.xml 或 /etc/clickhouse-server/config.d/ 文件夹内的单独配置文件中进行定义。

以下列出了参数及其当前默认值:

  • background_move_pool_size: 8
  • background_move_processing_pool_thread_sleep_seconds: 10
  • background_move_processing_pool_thread_sleep_seconds_random_part: 1.0
  • background_move_processing_pool_thread_sleep_seconds_if_nothing_to_do: 0.1
  • background_move_processing_pool_task_sleep_seconds_when_no_work_min: 10
  • background_move_processing_pool_task_sleep_seconds_when_no_work_max: 600
  • background_move_processing_pool_task_sleep_seconds_when_no_work_multiplier: 1.1
  • background_move_processing_pool_task_sleep_seconds_when_no_work_random_part: 1.0

出于测试目的,我们使用以下配置文件来实现即时移动。

1
2
3
4
<yandex>
  <background_move_processing_pool_thread_sleep_seconds>0.5</background_move_processing_pool_thread_sleep_seconds>
  <background_move_processing_pool_task_sleep_seconds_when_no_work_max>0.5</background_move_processing_pool_task_sleep_seconds_when_no_work_max>
</yandex>

这个简单的配置突出了两个最重要的参数,你需要视情况加以调整。这两个参数是“background_move_processing_pool_task_sleep_seconds_when_no_work_max”和“background_move_processing_pool_thread_sleep_seconds”。其中“background_move_processing_pool_task_sleep_seconds_when_no_work_max”定义了当没有工作(移动)时,线程池可以睡眠的最长时间。默认情况下,ClickHouse 将其设置为 600 秒。这意味着,在触发 TTL move 表达式后,实际的移动可以在 10 分钟内开始。完成移动的时间取决于 ClickHouse 需要移动的分片数量和磁盘的 I/O 性能。“background_move_processing_pool_thread_sleep_seconds”参数定义了工作者线程在接受另一个任务之前睡眠的秒数。

基于这些参数,当后台 move 进程池唤醒时,它会扫描所有分片的 TTL 表达式,并确定是否有任何需要移动的数据。

请注意,将分片移动到一个磁盘或卷时,后台 move 进程池会检查由存储策略定义的限制条件。如果 ClickHouse 不能根据 TTL move 表达式移动某些分片,则会稍后尝试移动。

何时评估 TTL move?

ClickHouse 会在以下情况下评估 TTL move 表达式:

  • 你插入分片时
  • 后台 move 进程池处理任务唤醒时
  • ClickHouse 创建新分片作为后台合并过程的结果后
  • 副本下载新分片时

一些已知的极端情况

没有什么是完美的,所以这里列出了一些已知的与 TTL move 相关的极端情况,请你注意。

  • 没有 SQL 语句来强制执行 TTL move 而不执行分片合并。
  • TTL move 和 delete 之间的行为差异。
  • 由于 I/O 瓶颈,在同一物理磁盘内的大量分片的多线程移动会有损性能。

结论

在本文中,我们研究了新的 TTL move 功能,以及它如何扩展新存储策略的用法。借助 TTL move,ClickHouse 拥有一个强大的工具来管理使用多卷存储情况下的数据存储方式。虽然仍存在一些极端情况,但我们在努力解决。你无需顾虑这些情况,不妨尝试一下,看看存储策略和 TTL move 如何显著降低存储成本,为你节约资金。新的 TTL move 将帮助你把数据存储到合适的地方。

后续

您已经了解了在 ClickHouse 中处理实时更新相关内容,本系列还包括其他内容:

  • 在 ClickHouse 中处理实时更新
  • 使用新的 TTL move,将数据存储在合适的地方(本文)
  • 在 ClickHouse 物化视图中使用 Join
  • ClickHouse 聚合函数和聚合状态
  • ClickHouse 中的嵌套数据结构

原文链接:https://altinity.com/blog/2020/3/23/putting-things-where-they-belong-using-new-ttl-moves

获取更多 EMR ClickHouse 相关信息,可查看产品文档:

https://help.aliyun.com/document_detail/212195.html

钉钉扫描下方二维码加入产品交流群一起参与讨论~