大数据技术

《ClickHouse原理解析与应用实践》读书总结

2021-04-25  本文已影响0人  Caucher

本文是对《ClickHouse原理解析与应用实践》一书的概括性总结,整体章节和结构尊重原文,由于书的出版在2019年,版本较旧,所以对应部分有修正,修正来源于clickhouse官方设计文档。因此本文是该书与clickhouse官方文档的一个互补结合。

第二章

2.1 核心特性

  1. 列式存储:纯列式数据库/数据压缩
  2. 向量化执行/SIMD
  3. 关系模型/标准SQL
  4. 存储引擎抽象/20多种存储引擎
  5. 多线程分布式/分区分片
  6. 多主架构
  7. 数据分片(replica, ditribute table)

第三章

两个有用的小工具:

  1. clickhouse-local 一个单机版的微内核,和标准的clickhouse服务完全隔离开,数据也不共享,适用于小批量数据。
  2. clickhouse-benchmark: echo 'SELECT count(*) from tutorial.visits_v1' | clickhouse-benchmark -i 20000 可以对指定查询做一次benchmark。我在1核2G的服务器上跑了下,得到结果:


    image.png

第四章

4.1 数据类型

clickhouse常量类型推断:最小存储代价

4.1.1 基础类型

各种长度的整数、浮点数,定点数,字符串、定长字符串、32位UUID、三种时间类型(精度最高就到亚秒)

4.1.2 复合类型

其中array和nested两种嵌套类型,可以之后再用SQL(array [left] join)打平成多行数据。

4.1.3 特殊类型

  1. nullable
    这是一种基本类型的修饰符,表示可以为空。但注意,被修饰的列不能被索引。这种修饰符要慎用,它会对nullable的列额外补充一个文件[column].null.bin,这意味着读取时,有双倍的I/O。
  2. IPv4 IPv6
    这也是种特殊的类型,专门存IP的,底层是int32,提供合法性检查,插入时用字符串插入即可。

4.2 DDL

4.2.1 数据库

clickhouse共有5种库引擎:

数据库本质上就是文件目录。

4.2.2 数据表

表也要自选引擎。

4.2.3 默认值表达式

有三种默认值表达式:

4.2.4 临时表

只支持MEMORY表引擎,不属于任何数据库,生命周期和Session绑定,连接断掉,表就废掉。
一般不用,主要是内核用。

4.2.5 分区表

概念同HIVE分区表,但是它只作用于本地,没有什么分布式的概念。
只有MergeTree家族的表引擎才支持分区。
分区可以在不同的表间进行复制迁移,但两个表的结构和分区键必须一致。
第六章详细介绍。

4.2.6 视图

4.3 数据表基本操作

4.4 分区

可以将一个分区内的某列数据清空,设置为初始值。
也可以将分区卸载/装载,本质上,是分区文件夹位置的迁移,不会真正的删除。

4.5 分布式DDL

需要主动声明ON CLUSTER xx_cluster,才会把DDL的SQL语句在某集群内统一执行。
数据块为单位进行操作,在块级别有原子性

4.7 修改和删除

由于列存储,clickhouse的修改和删除非常的重。会把一个表的所有分区的目录copy一次,去掉那些删除的行,直到写一次merge时,原先inactive的数据才会被删除。
而且,异步、非原子性。

第五章 数据字典

存于内存的一个scheme,可以build on top of many externel sources(clickhouse, mysql, linux file...)。
scheme以key-attributes形式存储,key是一个/多个属性,attributes就是一组属性
默认惰性读取(用到时从外部source读进内存),可以改配置为启动时读取。
外部source包括本地文件、远程文件、可执行文件、clickhouse、ODBC、DBMS(mysql/postgres/mongoDB/redis)。
这意味着,传统的ETL功能,很大程度上被代替了。但是实际上数据主要在内存,没有真正落入clickhouse表(虽然我们能以类似表的SQL去访问它)。
在2020年及之后的clickhouse中,诞生了dictionary表引擎,使得访问dictionary和访问普通数据库表的操作,完全一致。

5.2.4 扩展字典的类型

5.2.6 更新策略

一定时间范围内随机定期更新,能增量就增量,不能增量就全量。

第六章 MergeTree原理解析

6.1 MergeTree的创建方式与存储结构

MergeTree在写入数据时,是以block为单位写入的,而且block是immutable的。clickhouse通过后台线程,定期合并这些block,属于同一个分区的block会被合成新的block,因此被称为MergeTree。

6.1.1 创建MergeTree表的重要参数

几个参数:

6.1.2 MergeTree的存储结构

物理存储结构如下:


image.png

6.2 数据分区

如果分区key是整数、日期,则转化为字符串作为分区id,浮点、字符串要哈希之后作为分区id。多个key作为分区key用'-'来连接。

6.2.3 分区目录合并

MergeTree的一个重要特征就是,每次写入的时候,都会根据分区key产生一批新的分区目录,这和原先有的分区目录可能会有重复。比如,同样是'202104'的分区,可能最后产生了多个分区目录,而不是在一个分区目录下追加文件。
然后,这些相同分区的目录,通过后台任务进行合并,称为新的分区目录。旧分区目录延迟一段时间再删除。

每一个分区目录都有三个属性:

image.png image.png

6.3 主索引

一般来说ORDER KEY和PRIMARY KEY都是一致的,他们指定了主索引和数据的排序顺序。
主索引是稀疏索引,默认粒度8192. 由于占用空间极小,所以常驻内存,访问速度极快。
数据文件(.bin)也是按照索引粒度进行数据块的压缩;标记文件也会被索引粒度所影响。
primary.idx按照索引粒度,将相应位置的PRIMARY KEY读出来,按照顺序紧密拼接起来,没有一个多余的字节。多个key之间也不分隔。


image.png
image.png

6.3.4 索引的查询过程

image.png
primary.idx将数据文件划分为若干个等粒度的markRange,这些步长为1的markRange可以进行连续的合并形成更大的markRange,在逻辑上构成一个树形。查询的目标,就是定位到,哪些markRange(步长为1的)可能会含有QUERY值。
image.png
  1. 首先根据查询解析到QUERY的PRIMARY KEY范围;
  2. 从最大的数据范围内开始进行递归查找,如果QUERY范围和数据范围有交集,则划分成8个子区间(可以配置),如果已经不能拆8份了,即数据范围的markrange步长<8,那么就返回;如果没交集,那直接剪枝掉。
  3. 最终把返回的markRange区间合并起来。

6.4 二级索引(data_skipping_index)

二级索引要在建表时候主动声明。merge tree会为每个二级索引,建一个skp_idx_[Column].idx索引文件和skp_idx_[Column].mrk的标记文件。

CREATE TABLE table_name
(
    u64 UInt64,
    i32 Int32,
    s String,
    ...
    INDEX a (u64 * i32, s) TYPE minmax GRANULARITY 3,
    INDEX b (u64 * length(s)) TYPE set(1000) GRANULARITY 4
) ENGINE = MergeTree()
...

注意到,这里的GRANULARITY和主索引中的index_granularity的含义是不一样的。GRANULARITY控制的是每几个index_granularity,在索引文件中,输出一条汇总聚合信息。


image.png

skip_index的种类:

6.5 数据存储

列存储,每个列都有一个.bin数据文件。每列数据都是经过压缩的,目前支持LZ4,ZSTD,Multiple,Delta,数据按照ORDER KEY进行排序,以压缩数据块的格式写入数据文件。

6.5.2 压缩数据块

image.png

数据文件的写入,首先是以批次为单位的,即index_granularity所规定的条数。然而这些条数的size是不确定的,对于这个size,有两个极限值来限制(min_compress_block_size(64KB), max_compress_block_size(1MB))。
考虑三种情况:

6.6 数据标记

.mrk文件为索引文件.idx和数据文件.bin建立联系。
标记文件的逻辑结构如下图所示,每一行都代表了一个index_granularity,文件中记录了granularity在压缩文件中的位置,以及将对应的压缩块解压缩后,在解压缩块中的偏移量。


image.png

标记文件并不常驻内存,而是使用LRU策略缓存。

6.6.2 数据标记的工作方式

  1. 给定index_granularity的标号,去.mrk文件里面读出压缩块偏移量,然后再找到下一个有变化的偏移量,二者之间就是本个granularity的压缩块的位置;
  2. 把压缩块拉到内存里进行解压;
  3. 根据.mrk文件中的解压缩块的偏移量(类似于第一步),扫描相应位置,读到数据。

6.7 MegeTree Summarization

6.7.1 写入

image.png

6.7.2 查询

查询的性能主要取决于WHERE条件的Selectivity是否命中了索引,如果没有索引,也只能顺序扫描全部,可以多线程并行扫描。


image.png

第七章 MergeTree系列表引擎

MergeTree家族是clickhouse最核心的存储引擎。

7.1 MergeTree

本章只讲两个额外的mergeTree特性。

7.1.1 TTL

clickhouse可以对列/表设置TTL,表示清除它们的时间;对列清除是把它们变成默认值,对表清除是把过期的行删掉。
TTL的设置必须依赖于表中已有的一个日期/时间字段,在它的基础上定义过期的绝对时间。
可以在表定义的时候指定TTL,也可以后面再修改。
具体到实现层面,TTL的实现会在每个分区目录下写一个ttl.txt文件,用json格式配置TTL信息:

7.1.2 多路径存储

可以以分区为最小单元,将数据写入多个磁盘目录。

7.2 ReplacingMergeTree

MergeTree允许主键重复,ReplacingMergeTree可以在分区内部保证在充分merge之后(OPTIMIZE命令),数据按照ORDER KEY不重复。

7.3 SummingMergeTree

如果用户只关心该表的汇总数据(SUM),不关心明细数据,而且GROUP BY条件预先都设置好的,则可以用SummingMergeTree。
这其实就是数仓中的DWS层,只不过clickhouse在存储引擎层做了这个事情。

  1. 根据ORDER BY作为聚合数据的最细粒度;
  2. 以分区为单位进行聚合(SUM),不同分区不聚合;
  3. 可以建表时指定需要聚合的列,默认是所有数值列;非聚合列则使用第一行数据;
  4. 嵌套数据也可以内部按照key聚合。

注意在SummingMergeTree和后面的AggregatingMergeTree中,出现一种use case,就是PRIMARY KEY(索引包含字段)和ORDER KEY(数据排列顺序)不一致。
这种不一致只能是PRIMARY KEY是ORDER KEY的前缀。
用户可能想让汇总表按照A、B、C、D四个字段为粒度的汇总,但是从查询过滤的角度来说,只需要过滤A即可,后面的区分度不大,那么可以让数据按ABCD进行ORDER BY排序,主索引只有按A排列。
后面如果业务上不需要按照ABCD为粒度进行汇总了,那么也可以修改为ABC或者AB。

7.4 AggregatingMergeTree

是一个增强版的SummingMergeTree,可以对各个需要聚合的字段在建表时确定聚合函数。
虽然提供了非常强大的DWS功能,但是使用起来,插入/查询十分不便,因为不能指定列名,要显式写特殊函数,因此AggregatingMergeTree通常会作为明细表的物化视图的引擎而出现。插入,正常插入底表,同步至物化视图;查询,则需要特殊函数指定聚合列。

CREATE MATERIALIZED VIEW test.basic
ENGINE = AggregatingMergeTree() PARTITION BY toYYYYMM(StartDate) ORDER BY (CounterID, StartDate)
AS SELECT
    CounterID,
    StartDate,
    sumState(Sign)    AS Visits,
    uniqState(UserID) AS Users
FROM test.visits
GROUP BY CounterID, StartDate;
SELECT
    StartDate,
    sumMerge(Visits) AS Visits,
    uniqMerge(Users) AS Users
FROM test.basic
GROUP BY StartDate
ORDER BY StartDate;

7.5 CollapsingMergeTree

支持行级别的数据修改和删除的引擎。
以增代删,需要指定一个列,作为删除的标记字段。

7.6 VersionedCollapsingMergeTree

通过指定一个version列,保证最大的version可以被保留,使得分区内的修改和删除顺序,是可以保证的,即使是在多线程写入的情况下。

7.7 MergeTree家族关系

MergeTree家族的各个引擎,仅是在Merge的过程中,按照ORDERY KEY排序的过程中各展所长而已(折叠、聚合、去重)。


image.png

除此之外,每个引擎,还可以通过zookeeper广播log entry来实现replicated版本。


image.png

第八章 其他表引擎

8.1 外部存储引擎

clichhouse存储元数据,数据文件由外部提供。

8.2 内存类型

内存引擎都是直接在内存里读数据,在内存里进行查询。实际上,某些引擎也会将数据落盘以防止丢失。数据表加载时再读到内存中。没有主键,order key等。

8.3 日志类型

数据量很小(不到100W行),一次写入多次查询,查询不复杂,才会用日志引擎。不支持索引、分区,不支持并发读写,完全同步,有物理存储。

8.4 接口类型

本身不存储任何数据,作为“胶水”整合其他数据表。

第九章 数据查询

这里只标注一些clickhouse特殊的数据查询形式。

clickhouse执行join(本地)时会把右表当做小表完全拉进内存与左表比较;而且JOIN没有任何缓存,频繁使用的右表,最好都做成JOIN引擎表来进行缓存;clickhouse是大表模式,join非常吃力,如果需要连续补充多个维度,可以将维度表作为数据字典来join。

第十章 副本与分片

10.2 副本

image.png

插入数据时,数据首先写入内存缓冲,然后刷到磁盘tmp目录,全部刷完后,整合进正式分区,然后将日志entry同步至zookeeper。数据写入以block为基本单元和最小粒度(max_insert_block_size),对block的写入保持原子性。
clickhouse副本写入依赖zk,但是查询并不依赖zk。同一个shard写进同一个zk_path,各个副本保持不一样的replica_name。
ch的副本是表级别的,而且是多主架构,这种架构使得副本不仅仅为了容错,每个副本都可以作为读写的入口,用以负载均衡。

10.3 ReplicatedMergeTree原理解析

replicatedMergeTree会在zk_path上为这张表创建一组监听节点,分成以下几类:

LogEntry包含以下信息:

MutationEntry包含以下信息:

需要主副同步的操作主要有INSERT,MERGE,MUTATION,ALTER四种,即数据写入,分区合并,数据修改,元数据修改四种。
对于其他SQL指令,如SELECT,CREATE,DROP,RENAME,ATTACH等,不支持分布式,要不然登录每台机器结点分别执行,要不然用一些trick,后面讲。

10.4 数据分片

clickhouse可以灵活配置多个cluster,在每个cluster,一个表可以由多个shard(水平数据分区),每个shard内部还可以有数据副本(垂直数据冗余)。
有了cluster name的配置后,一些DDL语句可以用ON CLUSTER cluster_name进行分布式执行,原理就是根据配置,在每个replica都执行相同的指令。
其中{replica},{shard}可以以宏的形式写,因为在集群每台机器内,都有表存储了当前机器的replica shard分别是什么值。


image.png

DDL的分布式执行也是借助于zk,task的发布、状态、完成情况都记录在zk,秉着谁发起谁负责的策略,发起者负责监视是否cluster内所有节点任务完成,完成则结束返回,否则要转入后台执行。

10.5 Distributed原理解析

分布式表对应多个本地表,在多个本地shard表提供一个分布式透明性的服务。分布式表的INSERT SELECT直接作用于其管理的本地表,但是CREATE DROP RENAME之类的元数据操作只作用于自身,不作用与本地表。不支持MUTATION。
分布式表要求所有本地表结构一致,命名相同,仅shard和replica参数不同,在读时检查;
分布式表创建时也要指定ON CLUSTER,在集群所有节点创建分布式表,使得整个集群都可以成为读写入口。
分布式表创建时要指定一个sharding key和sharding function进行分区,function最终返回一个整数即可; 每个shard在配置文件中都有一个权重,代表数据流入的比率,clickhouse按照weights将sharding function的值域划分为几个连续区间,承接数据写入。


image.png

10.5.4 DISTRIBUTED写入

DISTRIBUTED表的数据写入一个shard节点,首先是,把本shard内的数据写入分区,然后把其他shard分区的挑出来,分别写到一个固定位置去,形成分区;本shard内有一个对固定位置的监视器,监测到分区目录变化后,会根据目录名,立即与远程shard建立联系,压缩传递数据。秉着谁执行谁负责的原则,写入shard负责确保数据都已经正确写入,结束返回。
写入的过程可以设置同步/异步,异步不用等待远程写完,同步需要设置超时。

上面的过程只考虑了sharding,没有考虑replica,replica的同步写入可以有两种模式,通过配置文件写死配置。第一种可以通过上述的方式,由distribute引擎写入replica,然而这种方式,写入节点要传输和写入的replica太多了,容易造成单点瓶颈;另一种方式是通过Repliacted-MergeTree,利用zk传输日志来进行同步,这样写入节点只需在每个shard选一个replica写入即可,具体选哪个可以根据一个全局计数器errors_count来选择。


image.png

10.5.5 DISTRIBUTED查询

分布式查询,要在每一个shard选择一个replica,这就涉及一个负载均衡算法,由参数控制,有以下四种

可想而知,查询也是谁执行谁负责,谁是入口查询节点,谁就要串联整个查询过程。包括分割分布式查询为本地子查询,选择连接其他shard的节点,传递SQL,收到结果数据,UNION返回结果。

上一篇下一篇

猜你喜欢

热点阅读