动笔写这篇文章的原因,是起源于一个网友的问题。这位 MM 在晚上 23:00 点还在积极的思考程序问题,这份热情着实打动了我。

问题的现象是这样的,当通过 MUTATIONS 操作执行 DELETE 时
代码语言:javascript复制ALTER TABLE xxx DELETE WHERE xxx出现了如下异常:
首先是提示无法分配 xx Gib 硬盘空间
代码语言:javascript复制Cannot reserve xxx, not enough space ...
接着连带提示没有足够的空间给正在做 mutating 动作的分区 part
代码语言:javascript复制Not enough space for mutating part ..
异常错误涉及到的分区 part 大小有140多G,而硬盘剩余空间200G。
这里就有一点反直觉的意思了,删除数据不应该是释放空间吗?为什么还提示我硬盘空间不足呢?
我在《ClickHouse原理解析与应用实践》这本书中曾描述过 MUTATIONS 部分的内容,但是不够深入,这篇文章也算是对这个知识点的一点补充吧。
接下来我会用一个简单的示例说明 MUTATIONS 的执行逻辑,首先新建一张测试表
代码语言:javascript复制CREATE TABLE test_9 (
user_id UInt64,
score UInt64,
create_time DateTime
)ENGINE= MergeTree()
PARTITION BY toYYYYMM(create_time)
ORDER BY user_id接着写入两批不同分区的数据,第一批属于 201905
代码语言:javascript复制INSERT INTO test_9 SELECT number,abs(number - 100),'2019-05-10 00:00:00' FROM `system`.numbers LIMIT 1000000
第二批数据属于 201906
代码语言:javascript复制INSERT INTO test_9 SELECT number,abs(number - 100),'2019-06-10 00:00:00' FROM `system`.numbers LIMIT 5,95此时我们查看磁盘目录
代码语言:javascript复制[root@ch5 test_9]# du -h ./
4.0K ./detached
52K ./201906_2_2_0
7.7M ./201905_1_1_0
7.8M ./会看到两个分区目录,201905_1_1_0 有7.7M,201906_2_2_0有52K。
现在我们来看一看 MUTATIONS 操作是如何工作的
以 UPDATE 为例,执行下面的修改语句
代码语言:javascript复制ALTER TABLE test_9 UPDATE score = 10000 WHERE user_id = 1;完成后,我们再去看磁盘目录,会看到
代码语言:javascript复制[root@ch5 test_9]# du -h ./
7.7M ./201905_1_1_0_3
4.0K ./detached
52K ./201906_2_2_0_3
4.0K ./201906_2_2_0
3.9M ./201905_1_1_0
12M ./总的目录大小由 7.8M 变成了 12M。
而分区 201905_1_1_0 和 201906_2_2_0 也都各自生成了一个以 mutation version 为后缀的新分区。
因为当前的 mutation version 是 3,所以它们分别是 201905_1_1_0_3 和 201906_2_2_0_3。(对于什么是 mutation version 我在书中有详细解释, 这里不再赘述)
在执行了 UPDATE 之后,如果和原始目录的大小进行对比,你会发现有些奇怪。例如 201905_1_1_0_3 和 201906_2_2_0_3 分别是 7.7M 和 52K,它们取代了原来的目录;而 201906_2_2_0 变成了 4K。
这是为什么呢?别急,跟我过一遍 MUTATIONS 的执行逻辑你就大致能够明白了。
其核心的逻辑如下:
首先,CH 会使用我们执行语句中附带的 WHERE user_id = 1 条件,
分别对 201905_1_1_0 和 201906_2_2_0 两个分区进行 count 查询,用以判断该分区内是否包含需要更新的数据。
在我们的这个示例中,user_id = 1 的数据在 201905_1_1_0 分区,所以你会在 CH 日志找到如下的查询信息:
代码语言:javascript复制<Debug> default.test_9 (SelectExecutor): Key condition: (column 0 in [1, 1])
<Debug> default.test_9 (SelectExecutor): MinMax index condition: unknown
<Debug> default.test_9 (SelectExecutor): Selected 1 parts by date, 1 parts by key, 1 marks to read from 1 ranges
<Trace> MergeTreeSelectProcessor: Reading 1 ranges from part 201905_1_1_0, approx. 8192 rows starting from 0
<Trace> InterpreterSelectQuery: FetchColumns -> Complete
<Trace> Aggregator: Aggregated. 0 to 1 rows (from 0.000 MiB) in 0.000 sec. (0.000 rows/sec., 0.000 MiB/sec.)接着,执行逻辑有两个分支:
对于包含修改范围的分区,CH 会进行 MUTATIONS 操作,其大致逻辑是:
第一,会创建一个 tmp_mut_ 为前缀、mutation version 为后缀的临时分区目录。
在我们这个示例中,临时目录为
代码语言:javascript复制tmp_mut_201905_1_1_0_3后缀 _3 表示当前的 mutation version 是 3。
第二,对于需要修改的列,会在 tmp_mut 目录中生成全新的 .bin 和.mrk 文件,例如这个示例中的 score 字段。
第三,对于无需修改的列,则直接和原始分区目录中相应的文件建立硬链接。
最后,将 tmp 目录重命名为正式目录
代码语言:javascript复制Renaming temporary part tmp_mut_201905_1_1_0_3 to 201905_1_1_0_3.再执行完了 UPDATE 之后,我们可以查看磁盘文件验证一番,进入到 201905_1_1_0_3 目录
代码语言:javascript复制[root@ch5 test_9]# cd 201905_1_1_0_3
[root@ch5 201905_1_1_0_3]# ll
total 7880
-rw-r-----. 1 clickhouse clickhouse 416 Sep 17 16:44 checksums.txt
-rw-r-----. 1 clickhouse clickhouse 92 Sep 17 16:44 columns.txt
-rw-r-----. 2 clickhouse clickhouse 7 Sep 17 16:44 count.txt
-rw-r-----. 2 clickhouse clickhouse 18042 Sep 17 16:44 create_time.bin
-rw-r-----. 2 clickhouse clickhouse 2976 Sep 17 16:44 create_time.mrk2
-rw-r-----. 2 clickhouse clickhouse 8 Sep 17 16:44 minmax_create_time.idx
-rw-r-----. 2 clickhouse clickhouse 4 Sep 17 16:44 partition.dat
-rw-r-----. 2 clickhouse clickhouse 992 Sep 17 16:44 primary.idx
-rw-r-----. 1 clickhouse clickhouse 4004940 Sep 17 16:44 score.bin
-rw-r-----. 1 clickhouse clickhouse 2976 Sep 17 16:44 score.mrk2
-rw-r-----. 2 clickhouse clickhouse 4004915 Sep 17 16:44 user_id.bin
-rw-r-----. 2 clickhouse clickhouse 2976 Sep 17 16:44 user_id.mrk2注意看左边的数字,它表示这个文件的inode被连接的次数,如果该数字大于1,则表示建立了硬链接。
因为 score 是修改的字段,所以 score.bin 和 score.mrk2 是新生成的文件,它们的连接数是 1; 而其他文件的连接数是 2,说明它们和 201905_1_1_0 内的相应文件一一建立了硬链接。
对于不包含修改范围的分区,也就是不涉及数据修改的分区 part,CH 会走 clone 流程。
重点来了, 在进行后续动作之前,CH 首先会判断当前磁盘的剩余空间是否充足。如果空间充足,则逻辑继续;如果不足,就会抛出异常
代码语言:javascript复制Cannot reserve xxx, not enough space ...而需要的剩余空间是该 part 大小的两倍。
在我们的这个示例中,201906_2_2_0 不涉及数据修改,所以它会进入 clone 流程:
首先,会判断磁盘的剩余空间是否满足 201906_2_2_0 大小的两倍。这里没有问题,所以继续。
接着, CH 会建立一个以 tmp_clone 为前缀的临时分区目录
代码语言:javascript复制tmp_clone_201906_2_2_0_3然后,将 201906_2_2_0 内的文件全部硬链接到 tmp_clone_201906_2_2_0_3目录
代码语言:javascript复制Cloning part /chbase/data/data/default/test_9/201906_2_2_0/ to /chbase/data/data/default/test_9/tmp_clone_201906_2_2_0_3最后,将 tmp_clone_201906_2_2_0_3 重命名为正式目录
代码语言:javascript复制Renaming temporary part tmp_clone_201906_2_2_0_3 to 201906_2_2_0_3.同样的,我们可以到 201906_2_2_0_3 目录进行验证
代码语言:javascript复制[root@ch5 test_9]# cd ./201906_2_2_0_3
[root@ch5 201906_2_2_0_3]# ll
total 48
-r--r-----. 2 clickhouse clickhouse 395 Sep 17 16:44 checksums.txt
-r--r-----. 2 clickhouse clickhouse 92 Sep 17 16:44 columns.txt
-r--r-----. 2 clickhouse clickhouse 2 Sep 17 16:44 count.txt
-r--r-----. 2 clickhouse clickhouse 40 Sep 17 16:44 create_time.bin
-r--r-----. 2 clickhouse clickhouse 48 Sep 17 16:44 create_time.mrk2
-r--r-----. 2 clickhouse clickhouse 8 Sep 17 16:44 minmax_create_time.idx
-r--r-----. 2 clickhouse clickhouse 4 Sep 17 16:44 partition.dat
-r--r-----. 2 clickhouse clickhouse 16 Sep 17 16:44 primary.idx
-r--r-----. 2 clickhouse clickhouse 412 Sep 17 16:44 score.bin
-r--r-----. 2 clickhouse clickhouse 48 Sep 17 16:44 score.mrk2
-r--r-----. 2 clickhouse clickhouse 412 Sep 17 16:44 user_id.bin
-r--r-----. 2 clickhouse clickhouse 48 Sep 17 16:44 user_id.mrk2可以看到,对于不用修改 part 分区目录,它们文件的 inode 全部是 2,都建立了硬链接。
对于 ALTER TABLE xxx DELETE 操作,它的逻辑和 UPDATE 是一样的,这里就不在赘述了。
回到开篇这位网友的问题,现在你能分析出原因了么?
这位网友执行了 DELETE WHERE 条件删除,恰好那个 140G 最大的 part 分区不在删除的条件范围内,所以这个分区进入了 clone 流程。而该流程首先会判断剩余空间是否大于 part 的两倍,所以就报错了。
如果这篇文章对你有帮助,欢迎 点赞、转发、在看 三连击 :)
欢迎大家扫码关注我的公众号和视频号:
ClickHouse的秘密基地
nauu的奇思妙想
往期精彩推荐:
【专辑】ClickHouse的资讯手札
【专辑】ClickHouse的原理巩固
【专辑】ClickHouse的经验分享


