动笔写这篇文章的原因,是起源于一个网友的问题。这位 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的经验分享