ClickHouse 最近在 MergeTree 中加入了 WAL 预写日志的能力,这无疑又加强了MergeTree的实力,该功能目前已经被合并到主线。
在介绍 WAL 之前,我们首先重温一下 MergeTree 最基本的合并过程。
新建一张 MergeTree:
代码语言:javascript复制CREATE TABLE test20(
ID String,
Price Int32,
Val Float64,
EventTime Date
) engine = MergeTree()
PARTITION BY toYYYYMM(EventTime)
ORDER BY ID
随后依次写入 3 行数据:
代码语言:javascript复制INSERT INTO test20 VALUES('id1',100,50.5,'2020-05-06')
INSERT INTO test20 VALUES('id2',200,50.5,'2020-05-06')
INSERT INTO test20 VALUES('id3',200,50.5,'2020-05-06')
此时,MergeTree 会生成 3 个分区目录:
代码语言:javascript复制total 8
drwxr-xr-x 16 nauu staff 512 10 14 21:56 202005_1_1_0
drwxr-xr-x 16 nauu staff 512 10 14 21:56 202005_2_2_0
drwxr-xr-x 16 nauu staff 512 10 14 21:56 202005_3_3_0
drwxr-xr-x 2 nauu staff 64 10 14 21:56 detached
-rw-r----- 1 nauu staff 1 10 14 21:56 format_version.txt
在合并之后,会生成一个新的合并分区 202005_1_3_1:
代码语言:javascript复制total 8
drwxr-xr-x 16 nauu staff 512 10 14 21:56 202005_1_1_0
drwxr-xr-x 16 nauu staff 512 10 14 21:01 202005_1_3_1
drwxr-xr-x 16 nauu staff 512 10 14 21:56 202005_2_2_0
drwxr-xr-x 16 nauu staff 512 10 14 21:56 202005_3_3_0
drwxr-xr-x 2 nauu staff 64 10 14 21:56 detached
-rw-r----- 1 nauu staff 1 10 14 21:56 format_version.txt
如果你还不理解分区目录的创建原理,可以看看这篇文章 一分钟视频解读ClickHouse MergeTree,或者更体系的看看《ClickHouse原理解析与应用实践》的第六章。
简而言之,只要执行一次 INSERT 语句,MergeTree 就会创建一次分区目录。也正因这个原因,当写入并发过高的时候,就会时常会看到我们的老朋友:
代码语言:javascript复制Too many parts (N). Merges areprocessing significantly slower than inserts.
WAL预写日志解决了这个问题。在 ClickHouse 的新版本中,MergeTree 多了这么几个参数:
代码语言:javascript复制 M(SettingUInt64, min_bytes_for_wide_part, 0, "Minimal uncompressed size in bytes to create part in wide format instead of compact", 0)
M(SettingUInt64, min_rows_for_wide_part, 0, "Minimal number of rows to create part in wide format instead of compact", 0)
M(SettingUInt64, min_bytes_for_compact_part, 0, "Experimental. Minimal uncompressed size in bytes to create part in compact format instead of saving it in RAM", 0)
M(SettingUInt64, min_rows_for_compact_part, 0, "Experimental. Minimal number of rows to create part in compact format instead of saving it in RAM", 0)
M(SettingBool, in_memory_parts_enable_wal, true, "Whether to write blocks in Native format to write-ahead-log before creation in-memory part", 0)
M(SettingUInt64, write_ahead_log_max_bytes, 1024 * 1024 * 1024, "Rotate WAL, if it exceeds that amount of bytes", 0)
其中 in_memory_parts_enable_wal 默认为 true,这说明预写日志默认就是开启状态的。
所以现在 MergeTree 的写入流程发生了一些变化,分区目录首先会在内存中,为了保证内存中的数据不会丢失,也会同步的在WAL日志中写一份。当数据满足阈值条件时,再将数据刷到磁盘。
这么说起来比较抽象,举个例子,还是用刚才那张 MergeTree , 现在给它添加一个 min_rows_for_compact_part 参数:
代码语言:javascript复制CREATE TABLE test20(
ID String,
Price Int32,
Val Float64,
EventTime Date
) engine = MergeTree()
PARTITION BY toYYYYMM(EventTime)
ORDER BY ID
settings min_rows_for_compact_part = 2
min_rows_for_compact_part = 2 的含义表示,数据首先会被写到内存和 WAL中,当触发 Merge 的时候,如果数据大于 2 行,就直接把合并后的分区写到磁盘。
例如现在还是写入 3 行数据:
代码语言:javascript复制INSERT INTO test20 VALUES('id1',100,50.5,'2020-05-06')
INSERT INTO test20 VALUES('id2',200,50.5,'2020-05-06')
INSERT INTO test20 VALUES('id3',200,50.5,'2020-05-06')
因为写入之后还没有触发 Merge 动作,此时我们再去看磁盘目录:
代码语言:javascript复制drwxr-xr-x 2 nauu staff 64 10 14 22:16 detached
-rw-r----- 1 nauu staff 1 10 14 22:16 format_version.txt
-rw-r----- 1 nauu staff 252 10 14 22:16 wal.bin
你会发现 MergeTree 并没有创建分区目录,而是多了一个 wal. bin 日志文件。
此时如果我们触发 Merge:
代码语言:javascript复制optimize TABLE test20
你会看到 MergeTree 直接通过预写日志创建了合并后的分区 202005_1_3_1
代码语言:javascript复制drwxr-xr-x 16 nauu staff 512 10 14 22:17 202005_1_3_1
drwxr-xr-x 2 nauu staff 64 10 14 22:16 detached
-rw-r----- 1 nauu staff 1 10 14 22:16 format_version.txt
-rw-r----- 1 nauu staff 252 10 14 22:16 wal.bin
这就是 MergeTree 在拥有了 WAL 日志后的写入过程。
除此之外,MergeTree 现在也扩充了分区的存储布局,在此之前,MergeTree 只有一种 wide 布局。也就是每个列字段都拥有一组独立的文件(1个bin文件和1个mark文件),例如下图所示:
那么现在,MergeTree 还提供了一种 compact 布局,例如我们在刚才的例子中再添加一个 min_rows_for_wide_part 参数:
代码语言:javascript复制CREATE TABLE test20(
ID String,
Price Int32,
Val Float64,
EventTime Date
) engine = MergeTree()
PARTITION BY toYYYYMM(EventTime)
ORDER BY ID
settings min_rows_for_compact_part = 2,
min_rows_for_wide_part = 10
min_rows_for_wide_part = 10 表示,如果数据大于 10 行,分区就是用 wide模式,否则就使用 compact 模式。
在这个用例中,我们的数据只有 3 行,所以当生成分区目录时,你会看到下图的情况:
所有列的数据写到了同一个 data.bin 文件中,所有列的标记文件也都写到了同一个.mark文件。 是不是很像 log 表引擎了呢?当列字段很多,数据又很少的时候,可以考虑使用这种布局模式的分区。