跳到主要内容
跳到主要内容

Part 合并

ClickHouse 中的 part 合并(part merges)是什么?


ClickHouse 之所以快速,不仅体现在查询上,也体现在写入上,这要归功于其存储层,其工作方式类似于 LSM trees

① 向 MergeTree 引擎家族的表插入数据时,会创建已排序、不可变的 data parts(数据片段)。

② 所有数据处理都被下放给后台 part 合并(part merges)

从而使数据写入既轻量又高度高效

为了控制每张表中的 ^^parts^^ 数量并实现上述第 ② 点,ClickHouse 会在后台持续地将较小的 ^^parts^^(按分区)合并为更大的 ^^parts^^,直到其压缩后的大小大约达到 ~150 GB

下图展示了这一后台合并过程的示意:

PART MERGES

每当发生一次额外合并,对应 part 的 merge level(合并层级)就会加一。0 表示该 part 是新的,尚未被合并。已被合并进更大 ^^parts^^ 的 ^^parts^^ 会被标记为非活动,并在一段可配置的时间后(默认 8 分钟)最终被删除。随着时间推移,这会形成一个由合并 ^^parts^^ 构成的。因此才称为 MergeTree 表。

监控合并

什么是表 parts 示例中,我们展示了,ClickHouse 会在 parts 系统表中跟踪所有表的 ^^parts^^(部件)。我们使用了以下查询来获取示例表中每个活动部件的合并级别以及存储的行数:

SELECT
    name,
    level,
    rows
FROM system.parts
WHERE (database = 'uk') AND (`table` = 'uk_price_paid_simple') AND active
ORDER BY name ASC;

先前文档中展示的 查询结果表明,该示例表包含四个活动的 ^^parts^^,每个都是由最初插入的 ^^parts^^ 经单次合并生成的:

   ┌─name────────┬─level─┬────rows─┐
1. │ all_0_5_1   │     1 │ 6368414 │
2. │ all_12_17_1 │     1 │ 6442494 │
3. │ all_18_23_1 │     1 │ 5977762 │
4. │ all_6_11_1  │     1 │ 6459763 │
   └─────────────┴───────┴─────────┘

运行 该查询现在显示,这四个 ^^part^^ 已经合并为单个最终 part(前提是该表中没有后续的插入操作):

   ┌─name───────┬─level─┬─────rows─┐
1. │ all_0_23_2 │     2 │ 25248433 │
   └────────────┴───────┴──────────┘

在 ClickHouse 24.10 中,一个新的 合并仪表板 被添加到了内置的监控仪表板中。通过 /merges HTTP 处理程序,该仪表板在 OSS 和 Cloud 中均可使用,用来可视化我们示例表的所有 part 合并:

PART MERGES

上面展示的仪表板捕获了整个过程,从最初的数据写入到最终合并为单个 part:

① 活跃 ^^parts^^ 的数量。

② part 合并,以方框形式进行可视化展示(方框大小反映 part 的大小)。

写放大(Write amplification)

并发合并

单个 ClickHouse 服务器会使用多个后台合并线程来执行对 ^^parts^^ 的并发合并:

PART 合并

每个合并线程会执行如下循环:

① 决定接下来要合并哪些 ^^parts^^,并将这些 ^^parts^^ 加载到内存中。

② 在内存中将这些 ^^parts^^ 合并为一个更大的 part。

③ 将合并后的 part 写入磁盘。

然后回到 ①

请注意,增加 CPU 核心数量和内存容量可以提高后台合并的吞吐量。

内存优化合并

ClickHouse 不一定会像在前一个示例中示意的那样,一次性将所有要合并的 ^^parts^^ 全部加载到内存中。根据若干因素,并为了降低内存消耗(以牺牲合并速度为代价),所谓的垂直合并会将这些 ^^parts^^ 按块分段依次加载并合并,而不是一次性全部处理。

合并机制

下图展示了在 ClickHouse 中,由单个后台合并线程在默认情况下(未启用纵向合并)对 ^^parts^^ 执行合并的过程:

PART 合并

part 的合并大致分为以下几个步骤:

① 解压与加载:将待合并 ^^parts^^ 中的压缩二进制列文件解压并加载到内存中。

② 合并:将数据合并为更大的列文件。

③ 索引:为合并后的列文件生成新的稀疏主索引

④ 压缩与存储:对新的列文件和索引进行压缩,并将其保存到一个新的目录中,用于表示合并后的数据 part。

数据 part 中的其他元数据,例如二级数据跳过索引、列统计信息、校验和以及最小-最大索引,也会基于合并后的列文件重新生成。为简化说明,我们在此省略了这些细节。

第 ② 步的具体机制取决于所使用的MergeTree 引擎,不同引擎的合并方式有所不同。例如,行可能会被聚合,或在数据过时时被替换。如前所述,这种方式将所有数据处理卸载到后台合并中,从而通过保持写入操作轻量高效,实现超快写入

接下来,我们将简要概述 ^^MergeTree^^ 家族中各个具体引擎的合并机制。

标准合并

下图展示了标准 MergeTree 表中 ^^数据分片(parts)^^ 是如何合并的:

PART MERGES

上图中的 DDL 语句创建了一个带有 ^^排序键^^ (town, street)MergeTree 表,这意味着 磁盘上的数据会按这些列进行排序,并相应生成稀疏主索引。

① 将已解压且预排序的表列,② 在保持由表的 ^^排序键^^ 定义的全局排序顺序的前提下进行合并,③ 生成新的稀疏主索引,④ 将合并后的列文件和索引压缩后作为一个新的数据分片存储到磁盘上。

替换合并

ReplacingMergeTree 表中的数据部分(part)合并与标准合并类似,但只保留每一行的最新版本,较旧的版本会被丢弃:

数据部分合并

上图中的 DDL 语句创建了一个 ReplacingMergeTree 表,使用 ^^sorting key^^ (town, street, id),这意味着磁盘上的数据会按照这些列排序,并相应生成稀疏主键索引。

② 合并操作与标准 MergeTree 表类似,会在保持全局排序顺序的前提下,合并解压后的预排序列。

不过,ReplacingMergeTree 会移除具有相同 ^^sorting key^^ 的重复行,并仅根据其所在数据部分的创建时间戳保留最新的一行。


求和合并

SummingMergeTree 表的 ^^parts^^ 合并过程中,数值数据会自动汇总:

分片合并

上图中的 DDL 语句定义了一个 SummingMergeTree 表,使用 town 作为 ^^sorting key^^。这意味着磁盘上的数据会按该列排序,并据此创建稀疏主键索引。

在步骤 ② 的合并过程中,ClickHouse 会将所有具有相同 ^^sorting key^^ 的行合并为一行,同时对数值列的值进行求和。

聚合合并

上文中的 SummingMergeTree 表示例是 AggregatingMergeTree 表的一种专用变体,允许在数据分片(part)合并期间,通过应用任意90+ 个聚合函数,实现自动增量数据转换

PART MERGES

上图中的 DDL 语句创建了一个以 town 作为 ^^sorting key^^(排序键)的 AggregatingMergeTree 表,确保磁盘上的数据按照该列排序,并生成相应的稀疏主键索引。

在 ② 合并阶段,ClickHouse 会将所有具有相同 ^^sorting key^^ 的行合并为一行,该行存储部分聚合状态(例如 avg() 所对应的 sumcount)。这些状态通过增量后台合并确保结果的准确性。