图片

本文字数:4606;估计阅读时间:12 分钟

作者:Tom Schreiber

本文在公众号【ClickHouseInc】首发

图片

TL;DR

CREATE TABLE staging CLONE AS prod;

→ 在不复制任何一个字节的情况下,瞬间创建一个克隆表。

它本质上就像给表做一次 Git fork:初始状态完全一致,在写入时才会分叉 (copy-on-write) 。非常适合进行破坏性测试。

一种安全的实验方式

假设你在生产环境中有一张体量巨大的 ClickHouse 表。

现在你想在预发布环境中测试一些破坏性操作:删除、更新、表结构调整,甚至是激进的优化。

但你绝对不希望对生产数据造成任何风险。

如果你可以瞬间、可重复地创建这张巨表的一个完全一致的副本,然后在上面随意进行各种破坏性测试,那该有多好?既不触碰生产数据,也无需复制任何一个字节。

在 ClickHouse 中,这完全可以实现。

这是那种乍一看像魔法一样的功能。但实际上,它只是 ClickHouse 数据存储机制带来的一个非常自然的结果。

虽然这个功能在 ClickHouse 官方文档中已有说明,但在实际使用中却出乎意料地少见。

在 ClickHouse Cloud 中,这种模式还能进一步扩展。

你可以克隆生产表,并在拥有隔离计算资源的独立服务上运行测试。

在这篇文章中,我们将深入了解表克隆在底层是如何实现的,以及为什么“不可变数据 Part”这一设计使它成为可能。

想快速了解一下?

Mark 录制了一段关于表克隆的简短讲解视频:https://youtu.be/y_9Q_Us-yhA

提醒:不可变数据 Part 模型

每次向 ClickHouse 表插入数据,都会在磁盘上生成一个全新的、自包含的、不可变的数据 Part。

为了说明这一点,我们使用下面这张 orders 表,用于跟踪每位客户的总收入:

CREATE TABLE orders
(
    order_id UInt32,
    customer String,
    total UInt32
)
ENGINE = MergeTree
ORDER BY order_id;

对于向 orders 表执行的这次插入:

INSERT INTO orders VALUES
    (1001, 'Liam', 31000),
    (1002, 'Ben',   7500),
    (1003, 'Anna', 12000);

​​​​​

ClickHouse 会在磁盘上创建一个新的 Part:

图片

在底层实现上,这个 Part 实际上是磁盘上的一个目录,其中包含按列存储的压缩文件,每一列对应一个文件:order_id、customer 和 total。Part 内的行会按照表的排序键进行物理排序,在本例中为 order_id。

数据 Part 是完全自包含的。它们包含了解析自身内容所需的全部元数据,而无需依赖中心元数据目录。虽然在上图中未展示,但每个 Part 还包含额外的元数据文件,例如稀疏主索引、二级数据跳过索引、列统计信息、校验和、最小-最大索引 (如果使用了分区) 等等。

当新数据写入时,ClickHouse 从不会就地修改已有 Part,而是始终生成新的 Part。

在后台,系统会将多个 Part 合并为更大的 Part,以控制 Part 数量并整合数据,但即便是合并操作,本质上也会生成全新的 Part。

(这种设计也使 ClickHouse 能够实现极高的插入吞吐量:数据可以作为彼此独立的 Part 写入,无需全局同步,然后在后台合并阶段再进行整合。)

同样地,删除行……

DELETE FROM orders WHERE order_id = 1001;

……或者更新行……

UPDATE orders
SET total = 3600
WHERE order_id = 36043;

……都是通过定位包含受影响行的 Part,生成一个应用了变更的新 Part (或 Part 片段) ,随后再移除旧 Part 来完成的。

正是这种严格的不可变性架构特性,使得瞬间、零拷贝的表克隆成为可能。

从不可变 Part 到瞬间克隆

当你执行……

CREATE TABLE staging CLONE AS production;

……ClickHouse 并不会复制生产表中的任何数据。

没有数据被复制

由于数据 Part 是不可变的,并且从不会被原地修改,因此它们的内容可以安全共享。

ClickHouse 不会去复制数据,而是为克隆表中的每一个源表已有的 Part 创建一个对应的 Part 目录。如果源表有 142 个 Part,那么克隆表同样也会有 142 个 Part。

克隆表的 Part 目录中的文件,会通过硬链接直接指向源表 Part 目录中的对应文件 (在本地文件系统上使用 POSIX 硬链接,在如 S3 这样的对象存储上则通过元数据间接引用实现) 。

这种机制适用于 Part 目录中的所有文件,包括列数据文件以及各种元数据文件,例如稀疏主索引、二级数据跳过索引、列统计信息、校验和以及最小-最大索引。

为了便于说明,下文将重点以列文件为例来解释其工作机制。实际上,硬链接的行为适用于 Part 目录中的每一个文件。

克隆的行为与源表完全一致

在克隆完成的那一刻,新表拥有与源表完全相同数量的数据 Part,并且每个数据 Part 中的列文件和索引也一模一样。

因此,克隆表的行为与原表完全一致:查询、mutation、后台合并以及存储布局都按同样的方式运行。不存在所谓的“轻量级”或降级模式。从创建的第一刻起,克隆就是一张功能完整的表。

如果你熟悉 Git,这本质上就是一次 fork:克隆表最初与原表完全一致,只有在发生修改时才会逐渐分叉。

通过不可变性实现安全共享

之所以可以在生产表与克隆表之间共享列文件,是因为任何修改 (插入、删除或更新) 都会生成包含新列文件的新数据 Part。

如果你修改的是克隆表:

ClickHouse 只会为克隆表写入新的 Part。原始生产表仍然引用那些未发生变化的 Part。

反过来,如果修改的是生产表:

系统同样会生成包含新列文件的新 Part,而此前已被引用的列文件会继续保留,只要克隆表中的数据 Part 仍然在引用它们 (如下所述,只有当没有任何表再引用这些列文件时,它们才会被删除) 。

写时复制与存储效率

因此,只要两个表都没有对共享数据进行修改,克隆几乎不会为数据本身占用额外的磁盘空间。两个表对应的数据 Part 目录实际上共享同一份底层列文件。

只有当其中某个表的数据发生修改时,才会写入新的 Part,而且也仅限于受影响的数据范围。

Part 级别的写时复制

即便是在一个 PB 级别规模的表中,更新一行数据也只会重写包含该行的那个数据 Part,而不会重写整个数据集。其他所有 Part 都保持共享状态,不受影响。

这是该存储设计的一个核心特性:数据修改是在 Part 级别进行隔离的,而不是在整张表的层面。

需要特别说明的是,这种 Part 级别的写时复制并不是克隆专属的机制。它是 ClickHouse 所有数据修改操作的基础机制,包括源表自身的修改。

如前所述,数据修改的实现方式是:定位包含受影响行的 Part,写入一个应用了变更的新 Part,然后在适当的时候移除旧 Part。

这就是 Part 级别的写时复制。克隆并没有引入任何新的写入路径,而是复用了支撑所有数据修改的同一套底层存储机制。

源表不再具有特殊地位

由于各个 Part 是独立共享的,完成克隆之后,源表本身就不再具有任何“特殊”地位。它只是另一张引用相同列文件的表而已。你甚至可以直接删除源表,只要克隆表中的数据 Part 仍然在引用这些列文件,它们就会继续保留在磁盘上。只有当没有任何表 (以及没有正在运行的查询) 再引用这些列文件时,底层列文件才会真正被删除。

瞬间克隆,与表大小无关

克隆所需的时间几乎与表的大小无关。

无论源表包含数百万行、数万亿行,还是达到 PB 级别的数据规模,克隆操作都几乎是瞬间完成的。

从技术角度来看,克隆时间与数据 Part 的数量成正比 (而不是与行数或数据字节数成正比) ,因为 ClickHouse 只需要创建对应的 Part 目录,并为现有文件建立硬链接。这些都是非常轻量级的文件系统操作,因此即使面对超大规模表,克隆在实际环境中依然极其迅速。

为了更直观地理解这一过程,我们用图示来一步步演示。

写时复制的过程演示 (可视化 walkthrough) 

下图展示了前文提到的 Part 级写时复制行为。

第一张图说明了:克隆表中各个数据 Part 内的列文件,是如何通过硬链接指向源表数据 Part 中对应列文件的。

图片

为简化说明,图中只标出了列文件。实际上,一个数据 Part 还包含多种元数据文件,例如稀疏主索引、二级数据跳过索引、列统计信息、校验和以及最小-最大索引。Part 目录中的整套文件都会被硬链接共享。

当克隆表发生更新时……

UPDATE cloned_orders
SET total = 3600
WHERE order_id = 36043;

……就会触发一次写时复制操作:

图片

正如前文所解释的,这种重写发生在 Part 级别:只有包含受影响行的那个数据 Part 会被替换,其他所有 Part 仍然保持共享状态,不受影响。

在克隆表中,受影响的 Part 会被重写为一个新的物理 Part,其中包含更新后的数据行。此时,克隆表将引用这个新生成的 Part 目录,而不再引用之前那个与源表通过硬链接共享列文件的目录。

克隆表原先的 Part 目录 (其中的列文件通过硬链接指向源表对应 Part 的列文件) 会被删除,因为它已经被新写入的 Part 所替换。

与此同时,原始生产表仍然继续引用自己未发生变化的 Part 目录。

由于原表依然引用该未变化的 Part 目录 (其列文件曾与克隆表的旧 Part 目录通过硬链接共享) ,这些底层文件仍然会保留在磁盘上。

只有当没有任何表再依赖这些列文件时,它们才会被清理删除。

这正是 ClickHouse 表克隆背后的核心机制。

不可变性保证了共享的安全性,而 Part 级写时复制则在无需重写整张表的前提下,实现了严格的隔离。

了解了底层原理之后,我们再来看看它在实际场景中能带来什么。

整体来看

ClickHouse 的表克隆功能既强大又实用,而且往往被低估。

每当你需要一张表的精确副本时——

  • 用于预发布环境

  • 进行表结构 (schema) 或索引实验

  • 执行数据回填

  • 进行迁移操作

  • 测试破坏性变更

你都可以瞬间创建这样一张副本,而无需对生产数据承担任何风险。

克隆表从一个完全一致的副本开始,随后可以独立演进。只要不对数据进行修改,它几乎不会额外占用存储空间。

克隆看起来像魔法。

但本质上,它只是 ClickHouse 存储架构的自然结果:不可变的 Part 可以安全共享,再结合 Part 级写时复制机制。

因为 Part 从不原地修改,所以可以被重复利用;因为所有修改都会生成新的 Part,所以隔离性天然得到保障。

最终带来的是一个简单、安全、存储高效,并且几乎不受表规模影响的功能。

在 ClickHouse Cloud 中,克隆表还可以选择在隔离的计算服务上执行查询,从而确保生产数据和计算资源都保持不受干扰。

征稿启示

面向社区长期正文,文章内容包括但不限于关于 ClickHouse 的技术研究、项目实践和创新做法等。建议行文风格干货输出&图文并茂。质量合格的文章将会发布在本公众号,优秀者也有机会推荐到 ClickHouse 官网。请将文章稿件的 WORD 版本发邮件至:Tracy.Wang@clickhouse.com

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐