美文网首页clickhouse收藏内容
ClickHouse MergeTree变得更像LSM Tree

ClickHouse MergeTree变得更像LSM Tree

作者: LittleMagic | 来源:发表于2020-09-14 22:18 被阅读0次

    前言

    笔者在之前的文章中已经提到过,MergeTree引擎族是ClickHouse强大功能的基础。MergeTree这个名词是在我们耳熟能详的LSM Tree之上做减法而来——去掉了MemTable和Log。也就是说,向MergeTree引擎族的表插入数据时,数据会不经过缓冲而直接写到磁盘。官方文档中有如下的描述:

    MergeTree is not an LSM tree because it doesn’t contain "memtable" and "log": inserted data is written directly to the filesystem. This makes it suitable only to INSERT data in batches, not by individual row and not very frequently – about once per second is ok, but a thousand times a second is not. We did it this way for simplicity’s sake, and because we are already inserting data in batches in our applications.

    但是在最近的ClickHouse新版本中,上述情况发生了巨大的改变。社区通过#8290#10697两个PR实现了名为Polymorphic Parts的特性,使得MergeTree引擎能够更好地处理频繁的小批量写入,但同时也标志着MergeTree的内核开始向真正的LSM Tree靠拢。本文就来介绍一下这个似乎并不引人注目的重要特性,采用的ClickHouse版本为20.6.4。

    Wide/Compact Part Storage

    先来创建一张测试表,并写入两批次数据。

    CREATE TABLE test.test_event_log (
      event_time DateTime,
      user_id UInt64,
      event_type String,
      site_id UInt64
    ) ENGINE = MergeTree()
    PARTITION BY toYYYYMMDD(event_time)
    ORDER BY (user_id,site_id)
    SETTINGS index_granularity = 8192;
    
    INSERT INTO test.test_event_log VALUES
    ('2020-09-14 12:00:00',12345678,'appStart',16789),
    ('2020-09-14 12:00:01',12345679,'appStart',26789);
    INSERT INTO test.test_event_log VALUES
    ('2020-09-14 13:00:00',22345678,'openGoodsDetail',16789),
    ('2020-09-14 13:00:01',22345679,'buyNow',26789);
    

    利用tree命令观察该表的数据目录,可以发现形成了两个part目录,每个part目录中都存在每一列的数据文件(bin)和索引标记文件(mrk2),老生常谈了。

    ├── 20200914_1_1_0
    │   ├── checksums.txt
    │   ├── columns.txt
    │   ├── count.txt
    │   ├── event_time.bin
    │   ├── event_time.mrk2
    │   ├── event_type.bin
    │   ├── event_type.mrk2
    │   ├── minmax_event_time.idx
    │   ├── partition.dat
    │   ├── primary.idx
    │   ├── site_id.bin
    │   ├── site_id.mrk2
    │   ├── user_id.bin
    │   └── user_id.mrk2
    ├── 20200914_2_2_0
    │   ├── ......
    

    当写入特别频繁时,短时间内生成的part目录过多,后台的merger线程合并不过来,就会出现Too many parts的异常,所以官方才会建议不要执行超过一秒钟一次的写入操作。

    下面修改表参数min_rows_for_wide_part,当然也可以在建表时的SETTINGS中指定。

    ALTER TABLE test.test_event_log MODIFY SETTING min_rows_for_wide_part = 5;
    

    然后再写入一批次2条数据(SQL就略去了),观察数据目录。

    ├── 20200914_3_3_0
    │   ├── checksums.txt
    │   ├── columns.txt
    │   ├── count.txt
    │   ├── data.bin
    │   ├── data.mrk3
    │   ├── minmax_event_time.idx
    │   ├── partition.dat
    │   └── primary.idx
    

    可以发现,新生成的part目录中不再有每一列的bin和mrk2文件了,而是作为整体存储在一个文件中,即data.bin/mrk3。

    重复实验可知,只有当写入批次中的数据行数达到或超过min_rows_for_wide_part规定的阈值时,part目录中的存储结构才会像之前一样“正常”,否则所有数据就会存储在data.bin/mrk3中。ClickHouse将每列数据分开存储的形式称为“Wide”(宽的),而将整体存储的形式称为“Compact”(压缩的),这也正是Polymorphic(多型的)一词的含义。

    在system.parts系统表中,也增加了part_type列来描述part的存储形式。

    SELECT partition,name,part_type,active FROM system.parts
    WHERE table = 'test_event_log';
    
    ┌─partition─┬─name───────────┬─part_type─┬─active─┐
    │ 20200914  │ 20200914_1_1_0 │ Wide      │      0 │
    │ 20200914  │ 20200914_1_4_1 │ Wide      │      1 │
    │ 20200914  │ 20200914_2_2_0 │ Wide      │      0 │
    │ 20200914  │ 20200914_3_3_0 │ Compact   │      0 │
    │ 20200914  │ 20200914_4_4_0 │ Compact   │      0 │
    └───────────┴────────────────┴───────────┴────────┘
    

    上面是已经发生过merge的parts信息,可以发现Wide part和Compact part是能够合并在一起的,且合并的结果part的存储形式仍然遵循min_rows_for_wide_part的阈值。

    除了min_rows_for_wide_part参数之外,还有另外一个参数min_bytes_for_wide_part与它共同作用。顾名思义,它是part数据以Wide形式存储的大小阈值。当两个条件满足其一时,part数据就会以Wide形式存储。当然这两个参数默认都为0,表示禁用Compact存储。

    min_bytes_for_wide_part参数已经应用在了会被频繁写入的系统日志表中,例如查询日志表system.query_log:

    SHOW CREATE TABLE system.query_log\G
    
    Row 1:
    ──────
    statement: CREATE TABLE system.query_log
    (
        `type` Enum8('QueryStart' = 1, 'QueryFinish' = 2, 'ExceptionBeforeStart' = 3, 'ExceptionWhileProcessing' = 4),
        `event_date` Date,
        `event_time` DateTime,
        -- 略去……
    )
    ENGINE = MergeTree
    PARTITION BY toYYYYMM(event_date)
    ORDER BY (event_date, event_time)
    SETTINGS min_bytes_for_wide_part = '10M', index_granularity = 8192  -- 10MB的Wide part阈值
    

    由于Compact存储形式大大减少了文件的数量,在生成大量小part时可以有效降低磁盘的iops,从而降低merge的压力。

    In-Memory Part & Write-Ahead Log

    到这里似乎还不能明显地看出MergeTree向LSM Tree靠拢的迹象,顶多是像LSM Tree一样更适合小批量写入而已。但是ClickHouse在实现Polymorphic Parts的同时,还把原版MergeTree中没有的预写日志(WAL)补了回来,而WAL的初衷正是为了防止内存中的MemTable丢失的,说明MergeTree引擎也引入了MemTable。下面进行介绍。

    仍然用例子来说话,修改表参数min_rows_for_compact_part

    ALTER TABLE test.test_event_log MODIFY SETTING min_rows_for_compact_part = 3;
    

    插入一批次2条数据,可以看到并没有生成新的part目录,但是在表目录下生成了一个全局的wal.bin文件,即预写日志文件,说明刚才写入的数据存在了MemTable中。注意ClickHouse代码内并没有MemTable的概念,而是将其称为In-Memory parts。

    ├── 20200914_1_4_1
    │   ├── ...
    ├── 20200915_5_5_0
    ├── ...
    ├── 20200915_7_7_0
    │   ├── ...
    ├── detached
    ├── format_version.txt
    └── wal.bin
    

    用clickhouse-compressor工具看不到wal.bin具体的内容,只能作罢。

    反复插入一两行的小批次数据,可以发现始终不会形成新的part目录,但wal.bin的大小在增长,说明这些数据都留在了内存中。如果此时执行OPTIMIZE语句触发merge(自动触发同理),就会发现生成了形如20200915_8_12_1的part,说明内存中的数据在merge的同时被flush到了磁盘——也就是说在启用了WAL的情况下,ClickHouse的flush是和merge一起进行的,而不是像一般的LSM Tree引擎一样是分别处理的。

    ├── 20200914_1_4_1
    │   ├── ...
    ├── 20200915_8_12_1
    │   ├── ...
    ├── detached
    ├── format_version.txt
    └── wal.bin
    

    min_rows_for_compact_part就是In-Memory part与Compact part之间的行数阈值,一次写入的数据行数大于此值,就会按照传统方式直接向磁盘flush形成Compact part(或者Wide part),不保存在内存中,也不会写WAL。反之,则会将数据保留成In-Memory part,并同时写入WAL,在下一次发生merge时再进行flush。同理,也存在min_bytes_for_compact_part参数,即In-Memory part与Compact part之间的大小阈值。这两个参数默认也都为0,表示禁用In-Memory part和WAL。

    当然,WAL的大小也不是无限增长的,write_ahead_log_max_bytes参数用于限制wal.bin的大小,默认值为1G。上面的这三个参数目前是试验性的,在生产环境中仍然要谨慎使用。

    In-Memory part和WAL的引入使得MergeTree的写入有了更强的缓冲,也更加趋近于LSM Tree-based引擎的机制。这也意味着在读取分区数据时,必须将In-Memory part和Wide/Compact part的数据进行合并,可能会牺牲读取性能,需要我们在之后的实践中评估其影响。

    The End

    通过上面的介绍,可以得知MergeTree的Polymorphic Parts实际上就是以写入优化为最终目的,借鉴LSM Tree的思想,将part的存储按照In-Memory→Compact→Wide的形式组织起来,弥补小批量写入性能不足的短板。不过照这样发展下去,ClickHouse有没有可能像Greenplum一样由OLAP引擎变成HTAP引擎呢?社区好像还没有这方面的roadmap,拭目以待吧。

    民那晚安晚安。

    相关文章

      网友评论

        本文标题:ClickHouse MergeTree变得更像LSM Tree

        本文链接:https://www.haomeiwen.com/subject/vkqoektx.html