ClickHouse MergeTree

1. 简介

ClickHouse MergeTree表引擎主要用于海量数据分析,支持数据分区、存储有序、主键索引、稀疏索引、数据TTL等。

1.1. 关于分层合并

MergeTree存储的数据按照增序排序,由多个part组成。part通过启发式算法在后端做merge,不同分区的part不会合并。合并示意图如下:


merge

由此可以发现,MergeTree和LSM结构类似,只不过没有memtable和log,写操作直接操作硬盘,所以适合大批量插入数据,而不是单行的频繁插入。

ClickHouse提供了不同的合并方式,分别对应不同类型的MergeTree:

  • MergeTree
  • CollapsingMergeTree
  • ReplacingMergeTree
  • AggregatingMergeTree
  • SummingMergeTree
  • GraphiteMergeTree
  • VersionedCollapsingMergeTree

1.2. 关于数据更改

另外,MergeTree对UPDATE和DELETE命令没有直接支持,是通过ALTER的变种——突变(mutation)——来间接支持:

ALTER TABLE [db.]table DELETE WHERE filter_expr;
ALTER TABLE [db.]table UPDATE column1 = expr1 [, …] WHERE filter_expr;

更新功能不支持更新有关主键或分区键的列

MergeTree通过重写整个数据部分来执行mutation,该mutation没有原子性,mutation执行开始后,select的执行可以看到已经修改的数据和没有修改的数据。

1.3. 关于主键

MergeTree的主键与通常理解的主键有差异,这里的主键并不提供唯一性保证,主要用于加速查询,并且主键是稀疏索引(默认8192行数据生成一个索引,类似于RocksDB SST 文件的block索引方式)

1.4. 关于part

一个part对应一个目录,是组成MergeTree的基本单位,每一个insert操作就会生成一个part,后台再不断的对part进行merge,每个part的数据按照主键排序。

1.5. 关于skip index

MergeTree目前推出的一个实验特性,支持对指定列建立skip index。有skip index的列会进行稀疏的预计算,当查询条件中有使用该列进行过滤的时候,就可以跳过不相关的列,大幅提升查询效率。

2. 实现分析

2.1. Part数据存储目录

目录名格式如下:

partiiton-id _ min-id _ max-id _ level

举例说明:

CREATE TABLE visits (`VisitDate` Date, `Hour` UInt8, `ClientID` UInt64, INDEX index_id ClientID TYPE minmax GRANULARITY 3) ENGINE = MergeTree() PARTITION BY toYYYYMM(VisitDate) ORDER BY Hour; 

建表如上述语句,然后插入数据若干,如下查看分区情况:

insert into visits values ('2019-01-01',2,22);
insert into visits values ('2019-01-01',10,22);
insert into visits values ('2019-01-01',30,22);

select partition, name, active from system.parts where table = 'visits';

┌─partition─┬─name─────────┬─active─┐
│ 201901    │ 201901_1_1_0 │      1 │
│ 201901    │ 201901_2_2_0 │      1 │
│ 201901    │ 201901_3_3_0 │      1 │
└───────────┴──────────────┴────────┘

partition列可见有1个分区201901,下面有三个可用part,分别对应三行数据。
一次写入会生成一个或多个(和分区数对应)part目录,一个part只能属于一个分区,且只有一个分区的part才可以进行合并。
假如merge操作发起后,分区会变成如下:

┌─partition─┬─name─────────┬─active─┐
│ 201901    │ 201901_1_1_0 │      0 │
│ 201901    │ 201901_1_3_1 │      1 │
│ 201901    │ 201901_2_2_0 │      0 │
│ 201901    │ 201901_3_3_0 │      0 │
└───────────┴──────────────┴────────┘

201901分区老的三个part都成为无效状态,新的有效part包含了失效part的block_id范围。

2.2. part存储数据文件

在上述目录下,文件如下:

  • checksums.txt:所有文件的列表,包括文件的大小和checksum
  • columns.txt:包含所有列及其类型的列表
  • primary.idx:包含主键
  • [Column].bin:包含压缩后的列数据
  • [Column].mrk:标记,允许跳过n*k行进行位置定位
  • count.txt:记录本part的行数
  • partition.txt:记录分区表达式的值
  • minmax_[Column].idx:根据记录分区表达式生成的MinMax 索引(见MergeTreeDataPart::MinMaxIndex class)
  • skip_idx_[index name].idx:skip index的值
  • skip_idx_[index name].mrk2:skip index的索引标记

还是以上述例子为例,其中一个part目录的文件如下:

-rw-r----- 1 clickhouse clickhouse 479 2月  16 14:08 checksums.txt
-rw-r----- 1 clickhouse clickhouse  39 2月  16 14:08 ClientID.bin
-rw-r----- 1 clickhouse clickhouse  48 2月  16 14:08 ClientID.mrk2
-rw-r----- 1 clickhouse clickhouse  85 2月  16 14:08 columns.txt
-rw-r----- 1 clickhouse clickhouse   1 2月  16 14:08 count.txt
-rw-r----- 1 clickhouse clickhouse  29 2月  16 14:08 Hour.bin
-rw-r----- 1 clickhouse clickhouse  48 2月  16 14:08 Hour.mrk2
-rw-r----- 1 clickhouse clickhouse   4 2月  16 14:08 minmax_VisitDate.idx
-rw-r----- 1 clickhouse clickhouse   4 2月  16 14:08 partition.dat
-rw-r----- 1 clickhouse clickhouse   2 2月  16 14:08 primary.idx
-rw-r----- 1 clickhouse clickhouse  39 2月  16 14:08 skp_idx_index_id.idx
-rw-r----- 1 clickhouse clickhouse  24 2月  16 14:08 skp_idx_index_id.mrk2
-rw-r----- 1 clickhouse clickhouse  32 2月  16 14:08 VisitDate.bin
-rw-r----- 1 clickhouse clickhouse  48 2月  16 14:08 VisitDate.mrk2

2.3. 代码走读

类图如下:


MergeTree类图

在单个节点上,一个table对应一个StorageMergeTree实例,MergeTree的类型通过StorageMergeTree.merging_params确定。

每一个MergeTreeDataPart类,内部有一个MergeTreePartition对象用于存储分区信息,不同MergeTreeDataPart实例上的MergeTreePartition实例可能是一样的,也就是说,逻辑上的partition包含多个part,一个part只能属于一个partition。

2.3.1. insert操作

代码执行路径如下:

  • excuteQuery() // called in TCPHandler.cpp
    • executeQueryImpl()
      • parseQuery()
      • interpreter->execute() // InterpreterInsertQuery
        • StorageMergeTree.write // 构造一个MergeTreeBlockOutputStream
    • copyData(in, out) // out就是是MergeTreeBlockOutputStream
      • MergeTreeBlockOutputStream.write()
        • MergeTreeDataWriter.splitBlockIntoParts
        • MergeTreeDataWriter.writeTempPart
          • MergedBlockOutputStream.writePrefix // MergedBlockOutputStream用于写数据到磁盘
          • MergedBlockOutputStream.writeWithPermutation
            • MergedBlockOutputStream.calculateAndSerializeSkipIndices // 写skip indies
          • MergedBlockOutputStream.writeSuffixAndFinalizePart // 落盘
        • StorageMergeTree. renameTempPartAndAdd // min_id 和 max_id一样
  • processInsertQuery()
    • NativeBlockOutputStream.write() // 发送结果到client

2.3.2. select

代码执行路径如下:

  • excuteQuery() // called in TCPHandler.cpp
    • executeQueryImpl()
      • parseQuery()
      • interpreter->execute() // InterpreterSelectQuery
        • InterpreterSelectQuery.executeImpl()
          • executeFetchColumns()
            • StorageMergeTree.readWithProcessors
              • MergeTreeDataSelectExecutor.read
              • MergeTreeDataSelectExecutor.readFromParts // 通过skip索引过滤构建输入流
  • processOrdinaryQuery() // Pull query execution result, if exists, and send it to network.

2.3.3 mutation

代码执行路径如下:
mutate()

  • 实例化MergeTreeMutationEntry对象,创建tmp_mutation_{xxx}.txt,写入command、format version、create time到文件
  • tmp_mutation_{xxx}.txt重命名为mutation_{version}.txt
  • MergeTreeMutationEntry对象放入current_mutations_by_id和current_mutations_by_version
  • 唤醒后台线程 ,执行mutation函数 mergeMutateTask
    • clearOldPartsFromFilesystem
      • 找出outdated状态的part
        • 从文件系统删除
        • 从内存中删除( data_parts_indexes)
        • 写part log
      • clearOldTemporaryDirectories
      • clearOldMutations
      • merge()
      • merge失败则执行tryMutatePart(),否则直接返回
  • 等待线程执行结束

2.3.4 merge

merge()

  • MergeTreeDataMergeMutator.selectPartsToMerge // 选出最适合merge的分区的所有parts
  • FutureMergedMutatedPart.assign // 将所有parts的信息进行合并,初始化将要生成的新part对象
    • block_id合并
    • level加1
  • MergeTreeDataMergeMutator.mergePartsToTemparyPart // 落盘到临时目录
  • MergeTreeDataMergeMutator.renameMergedTemporaryPart // mv到正式目录

附件

@startuml
scale 1.75
together {
    class MergingParams {
        // For Collapsing and VersionedCollapsing mode.
        String sign_column;
        // For Summing mode.
        Names columns_to_sum;
        // For Replacing and VersionedCollapsing mode.
        String version_column;
        // For Graphite mode.
        Graphite::Params graphite_params;
    }

    class MergeTreePartition {
        Row value;
        void load(/*storage*/,
                /*part_path*/);
        void store();
    }

    class MergeTreeDataPart {
        MinMaxIndex minmax_idx;
        MergeTreeIndexGranularity
            index_granularity;
        Columns index;
        MergeTreePartition partition;
    }

    class MergeTreeData {
        Names minmax_idx_columns;
        Names primary_key_columns;
        Names sorting_key_columns;
        ExpressionActionsPtr partition_key_expr;
        ExpressionActionsPtr minmax_idx_expr;
        // Secondary indices for MergeTree skipping
        MergeTreeIndices skip_indices;
        // multi-index container of parts
        DataPartsIndexes data_parts_indexes;
        MergingParams merging_params;

        void alterDataParts();
        void loadDataParts();
    }
}

interface IStorage {
    ColumnsDescription columns;
    IndicesDescription indices;
    ConstraintsDescription constraints;

    BlockOutputStreamPtr write(/*querey*/, /*context*/);
    void mutate(const MutationCommands &, const Context &);
    void alter(const AlterCommands &, const Context &, /*lock holder*/);
    BlockInputStreams read(const Names &, const SelectQueryInfo &, ...);
    Pipes readWithProcessors (const Names &, const SelectQueryInfo &, ...);
}
note left: table is a IStorage

class StorageMergeTree {
    MergeTreeDataSelectExecutor reader;
    MergeTreeDataWriter writer;
    MergeTreeDataMergerMutator merger_mutator;
    BackgroundProcessingPool::TaskHandle
        merging_mutating_task_handle;
    void mutate(const MutationCommands &, const Context &);
    void alter(const AlterCommands &, const Context &, /*lock holder*/);
    BlockOutputStreamPtr write(/*querey*/, /*context*/);
    Pipes readWithProcessors (const Names &, const SelectQueryInfo &, ...);
}

IStorage <|.. MergeTreeData
MergeTreeData <|-- StorageMergeTree

MergeTreePartition o- MergeTreeDataPart : > aggregated by some
MergeTreeDataPart -* MergeTreeData : < have some
MergeTreeData *- MergingParams : have 1 >

@enduml
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容