ClickHouse 作为一款 PB 级的交互式分析数据库,最初是由号称 “ 俄罗斯 Google ” 的 Yandex 公司开发,主要作为世界第二大 Web 流量分析平台 Yandex.Metrica(类 Google Analytic、友盟统计)的核心存储,为 Web 站点、移动 App 实时在线的生成流量统计报表。

一、ClickHouse 架构

        Clickhouse只有1个组件,每个组件即可以接受用户请求进行查询的分发,也可以进行查询的执行。 Clickhouse通过分片和副本实现高可靠,数据通过一定的规则均匀分散到各个Shard中。Clickhouse如果需要支持副本功能,则需要搭建Zookeeper来完成数据的复制。

ClickHouse 部署架构图 clickhouse架构原理_分布式

ClickHouse 采用典型的分组式的分布式架构,具体集群架构如上图所示:

  • Shard:集群内划分为多个分片或分组(Shard 0 … Shard N),通过 Shard 的线性扩展能力,支持海量数据的分布式存储计算。
  • Node:每个 Shard 内包含一定数量的节点(Node,即进程),同一 Shard 内的节点互为副本,保障数据可靠。ClickHouse 中副本数可按需建设,且逻辑上不同 Shard 内的副本数可不同。
  • ZooKeeper Service:集群所有节点对等,节点间通过 ZooKeeper 服务进行分布式协调。

        Clickhouse的每个节点都是一个类似mysql的数据库实例,可以单独提供local表的读写服务。 Clickhouse没有实现分布式元信息管理,实现的是手动元信息管理,每个节点的元信息可能不一样,因此查询需要发给特定的Clickhouse节点。ClickHouse的分布式查询里,请求节点会将查询改写并转发给 所有shard,各shard在做完数据计算后把结果反馈给请求节点,最后在请求节点上再做数据的merge并返回给用户。Clickhouse目前的查询引擎是二阶段汇聚模型,不支持Shuffer的Join和Aggrgation。

二、数据模型

       ClickHouse 采用经典的表格存储模型,属于结构化数据存储系统。我们分别从面向用户的逻辑数据模型和面向底层存储的物理数据模型进行介绍。

1. 逻辑数据模型

        从用户使用角度看,ClickHouse 的逻辑数据模型与关系型数据库有一定的相似:一个集群包含多个数据库,一个数据库包含多张表,表用于实际存储数据。

与传统关系型数据库不同的是,ClickHouse 是分布式系统,如何创建分布式表呢?

        ClickHouse 的设计是:先在每个 Shard 每个节点上创建本地表(即 Shard 的副本),本地表只在对应节点内可见;然后再创建分布式表,映射到前面创建的本地表。这样用户在访问分布式表时,ClickHouse 会自动根据集群架构信息,把请求转发给对应的本地表。

创建分布式表的具体样例如下:

CREATE TABLE IF NOT EXISTS user_cluster ON CLUSTER cluster_3shards_1replicas
(
    id Int32,
    name String
)ENGINE = Distributed(cluster_3shards_1replicas, default, user_local,id);

Distributed表引擎的定义形式如下所示

Distributed(cluster_name, database_name, table_name[, sharding_key])

各个参数的含义分别如下:

  • cluster_name:集群名称,与集群配置中的自定义名称相对应。
  • database_name:数据库名称
  • table_name:表名称
  • sharding_key:可选的,用于分片的key值,在数据写入的过程中,分布式表会依据分片key的规则,将数据分布到各个节点的本地表。

        创建分布式表是读时检查的机制,也就是说对创建分布式表和本地表的顺序并没有强制要求
同样值得注意的是,在上面的语句中使用了ON CLUSTER分布式DDL,这意味着在集群的每个分片节点上,都会创建一张Distributed表,这样便可以从其中任意一端发起对所有分片的读、写请求。

        创建完成上面的分布式表时,在每台机器上查看表,发现每台机器上都存在一张刚刚创建好的表。接下来就需要创建本地表了,在每台机器上分别创建一张本地表:

CREATE TABLE IF NOT EXISTS user_local 
(
    id Int32,
    name String
)ENGINE = MergeTree()
ORDER BY id
PARTITION BY id
PRIMARY KEY id;

其中部分关键概念介绍如下,分区、数据块、排序等概念会在物理存储模型部分展开介绍:

  • MergeTree:ClickHouse 中使用非常多的表引擎,底层采用 LSM Tree 架构,写入生成的小文件会持续 Merge。
  • Distributed:ClickHouse 中的关系映射引擎,它把分布式表映射到指定集群、数据库下对应的本地表上。

更直观的,ClickHouse 中的逻辑数据模型如下:

ClickHouse 部署架构图 clickhouse架构原理_字段_02

 2. 物理存储模型

ClickHouse 部署架构图 clickhouse架构原理_字段_03

接下来,我们来介绍每个分片副本内部的物理存储模型,具体如下:

  • 数据分区:每个分片副本的内部,数据按照 PARTITION BY 列进行分区,分区以目录的方式管理,本文样例中表按照时间进行分区。
  • 列式存储:每个数据分区内部,采用列式存储,每个列涉及两个文件,分别是存储数据的 .bin 文件和存储偏移等索引信息的 .mrk2 文件。
  • 数据排序:每个数据分区内部,所有列的数据是按照 ORDER BY 列进行排序的。可以理解为:对于生成这个分区的原始记录行,先按 ORDER BY 列进行排序,然后再按列拆分存储。
  • 数据分块:每个列的数据文件中,实际是分块存储的,方便数据压缩及查询裁剪,每个块中的记录数不超过 index_granularity,默认 8192。
  • 主键索引:主键默认与 ORDER BY 列一致,或为 ORDER BY 列的前缀。由于整个分区内部是有序的,且切割为数据块存储,ClickHouse 抽取每个数据块第一行的主键,生成一份稀疏的排序索引,可在查询时结合过滤条件快速裁剪数据块。

三、表引擎

1. 表引擎的使用

        表引擎是 ClickHouse 的一大特色。可以说, 表引擎决定了如何存储表的数据。包括: 数据的存储方式和位置,写到哪里以及从哪里读取数据。

  • 支持哪些查询以及如何支持。
  • 并发数据访问。
  • 索引的使用(如果存在)。
  • 是否可以执行多线程请求。
  • 数据复制参数。 表引擎的使用方式就是必须显式在创建表时定义该表使用的引擎,以及引擎使用的相关参数。

2. TinyLog

        以列文件的形式保存在磁盘上,不支持索引,没有并发控制。一般保存少量数据的小表, 生产环境上作用有限。可以用于平时练习测试用。

create table t_tinylog ( id String, name String) engine=TinyLog;

3. Memory

        内存引擎,数据以未压缩的原始形式直接保存在内存当中,服务器重启数据就会消失。 读写操作不会相互阻塞,不支持索引。简单查询下有非常非常高的性能表现(超过 10G/s)。 一般用到它的地方不多,除了用来测试,就是在需要非常高的性能,同时数据量又不太大(上限大概 1 亿行)的场景。

4. MergeTree

        ClickHouse 中最强大的表引擎当属 MergeTree(合并树)引擎及该系列(*MergeTree) 中的其他引擎,支持索引和分区,地位可以相当于 innodb 之于 Mysql。而且基于 MergeTree,还衍生出了很多其他的聚合模型,也是非常有特色的引擎。

create table t_order_mt(
   id UInt32,
   sku_id String,
   total_amount Decimal(16,2),
   create_time Datetime
) engine =MergeTree
partition by toYYYYMMDD(create_time) primary key (id)
order by (id,sku_id);

        MergeTree 其实还有很多参数(绝大多数用默认值即可),但是三个参数是更加重要的, 也涉及了关于 MergeTree 的很多概念。

4.1 partition by

        学过 hive 的应该都不陌生,分区的目的主要是降低扫描的范围,优化查询速度。如果不填,只会使用一个分区。

        MergeTree 是以列文件+索引文件+表定义文件组成的,但是如果设定了分区那么这些文件就会保存到不同的分区目录中。 分区后,面对涉及跨分区的查询统计,ClickHouse 会以分区为单位并行处理。

        任何一个批次的数据写入都会产生一个临时分区,不会纳入任何一个已有的分区。写入后的某个时刻(大概 10-15 分钟后),ClickHouse 会自动执行合并操作(等不及也可以手动 通过 optimize 执行),把临时分区的数据,合并到已有分区中。

optimize table xxxx final;

Clickhouse支持两级分区:首先数据分成若干shard,然后每个shard下可以按照时间列进行分区。

        Clickhouse建立shard时需要指定cluster(某表这个集群有多少个节点),shard的个数就是cluster定义 的clickhosue节点数,每个shard对应1个cluster节点。用户需要在每台clickhouse节点上的config.xml 中配置cluster。Clickhouse建表时可以设置hash分桶或者random分桶,以及分桶的权重。Clickhosue 的shard级别不做分区裁剪,在查询时数据会发往所有的shard。Clickhouse加shard比较方便,只需修改config.xml中即可,手动修改config.xml后元信息会实时生效,但是减shard比较麻烦。

        每个shard下可以建立分区,通常以时间列作为分区。每个分区下会有若干的parts文件。Clickhouse的 分区支持TTL,分区过期后可以删除分区数据。另外Clickhouse支持分区级别的分区裁剪。

4.2 primary key 主键(可选)

        ClickHouse 中的主键,和其他数据库不太一样,它只提供了数据的一级索引,但是却不是唯一约束。这就意味着是可以存在相同 primary key 的数据的。主键的设定主要依据是查询语句中的 where 条件。

        根据条件通过对主键进行某种形式的二分查找,能够定位到对应的index granularity,避 免了全表扫描。

index granularity: 直接翻译的话就是索引粒度,指在稀疏索引中两个相邻索引对应数 据的间隔。ClickHouse 中的 MergeTree 默认是 8192。官方不建议修改这个值,除非该列存在 大量重复值,比如在一个分区中几万行才有一个不同数据。

稀疏索引:

ClickHouse 部署架构图 clickhouse架构原理_分布式_04

        稀疏索引的好处就是可以用很少的索引数据,定位更多的数据,代价就是只能定位到索 引粒度的第一行,然后再进行进行一点扫描。

4.3 order by(必选)

        order by 设定了分区内的数据按照哪些字段顺序进行有序保存。order by 是 MergeTree 中唯一一个必填项,甚至比 primary key 还重要,因为当用户不设置主键的情况,很多处理会依照 order by 的字段进行处理(比如后面会讲的去重和汇总)。

要求:主键必须是 order by 字段的前缀字段。比如 order by 字段是 (id, sku_id) 那么主键必须是 id 或者(id, sku_id)

4.4 二级索引

        目前在 ClickHouse 的官网上二级索引的功能在 v20.1.2.4 之前是被标注为实验性的,在这个版本之后默认是开启的。

create table t_order_mt2(
    id UInt32,
    sku_id String,
    total_amount Decimal(16,2), 
    create_time Datetime,
    INDEX a total_amount TYPE minmax GRANULARITY 5
) engine =MergeTree
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id, sku_id);

        其中 GRANULARITY N 是设定二级索引对于一级索引粒度的粒度。二级索引能够为非主键字段的查询发挥作用。

4.5 数据 TTL

        TTL 即 Time To Live,MergeTree 提供了可以管理数据表或者列的生命周期的功能。

create table t_order_mt3(
    id UInt32,
    sku_id String,
    total_amount Decimal(16,2) TTL create_time+interval 10 SECOND, 
    create_time Datetime
) engine =MergeTree
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id, sku_id);

下面的这条语句是数据会在 create_time 之后 10 秒丢失

alter table t_order_mt3 MODIFY TTL create_time + INTERVAL 10 SECOND;

涉及判断的字段必须是 Date 或者 Datetime 类型,推荐使用分区的日期字段。

5. ReplacingMergeTree

        ReplacingMergeTree 是 MergeTree 的一个变种,它存储特性完全继承 MergeTree,只是 多了一个去重的功能。 尽管 MergeTree 可以设置主键,但是 primary key 其实没有唯一约束 的功能。如果你想处理掉重复的数据,可以借助这个 ReplacingMergeTree。

1. 去重时机

        数据的去重只会在合并的过程中出现。合并会在未知的时间在后台进行(一般写入后10-15分钟),所以你无法预先作出计划。有一些数据可能仍未被处理。


2. 去重范围

        如果表经过了分区,去重只会在分区内部进行去重,不能执行跨分区的去重。所以 ReplacingMergeTree 能力有限, ReplacingMergeTree 适用于在后台清除重复的数据以节省空间,但是它不保证没有重复的数据出现。

6. SummingMergeTree

        对于不查询明细,只关心以维度进行汇总聚合结果的场景。如果只使用普通的 MergeTree 的话,无论是存储空间的开销,还是查询时临时聚合的开销都比较大。ClickHouse 为了这种场景,提供了一种能够“预聚合”的引擎 SummingMergeTree。SummingMergeTree使用时需要注意:

create table t_order_smt(
    id UInt32,
    sku_id String,
    total_amount Decimal(16,2) ,
    create_time Datetime
) engine =SummingMergeTree(total_amount)
partition by toYYYYMMDD(create_time)
primary key (id)
order by (id,sku_id );
  • 以SummingMergeTree()中指定的列作为汇总数据列
  • 可以填写多列必须数字列,如果不填,以所有非维度列且为数字列的字段为汇总数
    据列
  • 以 order by 的列为准,作为维度列
  • 其他的列按插入顺序保留第一行
  • 不在一个分区的数据不会被聚合
  • 只有在同一批次插入(新版本)或分片合并时才会进行聚合

建设:设计聚合表的话,唯一键值、流水号可以去掉,所有字段全部是维度、度量或者时间戳。

问题:能不能直接执行以下 SQL 得到汇总值

select total_amount from XXX where province_name=’’ and create_date=’xxx’

        不行,可能会包含一些还没来得及聚合的临时明细。如果要是获取汇总值,还是需要使用 sum 进行聚合,这样效率会有一定的提高,但本身 ClickHouse 是列式存储的,效率提升有限,不会特别明显。

四、ClickHouse 核心特性

        ClickHouse 为什么会有如此高的性能,获得如此快速的发展速度?下面我们来从 ClickHouse 的核心特性角度来进一步介绍。

1. 列存储

ClickHouse 采用列存储,这对于分析型请求非常高效。

        一个典型且真实的情况是:如果我们需要分析的数据有 50 列,而每次分析仅读取其中的 5 列,那么通过列存储,我们仅需读取必要的列数据。相比于普通行存,可减少 10 倍左右的读取、解压、处理等开销,对性能会有质的影响。这是分析场景下,列存储数据库相比行存储数据库的重要优势。

行存储:从存储系统读取所有满足条件的行数据,然后在内存中过滤出需要的字段,速度较慢。

 列存储:仅从存储系统中读取必要的列数据,无用列不读取,速度非常快。

2. 向量化执行

        在支持列存的基础上,ClickHouse 实现了一套面向向量化处理的计算引擎,大量的处理操作都是向量化执行的。

相比于传统火山模型中的逐行处理模式,向量化执行引擎采用批量处理模式,可以大幅减少函数调用开销,降低指令、数据的 Cache Miss,提升 CPU 利用效率。并且 ClickHouse 可利用 SIMD 指令进一步加速执行效率。这部分是 ClickHouse 优于大量同类 OLAP 产品的重要因素。

        以商品订单数据为例,查询某个订单总价格的处理过程,由传统的按行遍历处理的过程,转换为按 Block 处理的过程。

ClickHouse 部署架构图 clickhouse架构原理_分布式_05

3. 编码压缩

        由于 ClickHouse 采用列存储,相同列的数据连续存储,且底层数据在存储时是经过排序的,这样数据的局部规律性非常强,有利于获得更高的数据压缩比。

        此外,ClickHouse 除了支持 LZ4、ZSTD 等通用压缩算法外,还支持 Delta、DoubleDelta、Gorilla 等专用编码算法,用于进一步提高数据压缩比。其中 DoubleDelta、Gorilla 是 Facebook 专为时间序数据而设计的编码算法,理论上在列存储环境下,可接近专用时序存储的压缩比,详细可参考 Gorilla 论文。

ClickHouse 部署架构图 clickhouse架构原理_数据_06

        在实际场景下,ClickHouse 通常可以达到 10:1 的压缩比,大幅降低存储成本。同时,超高的压缩比又可以降低存储读取开销、提升系统缓存能力,从而提高查询性能。

4. 多索引

        列存用于裁剪不必要的字段读取,而索引则用于裁剪不必要的记录读取。ClickHouse 支持丰富的索引,从而在查询时尽可能的裁剪不必要的记录读取,提高查询性能。

        ClickHouse 中最基础的索引是主键索引。前面我们在物理存储模型中介绍,ClickHouse 的底层数据按建表时指定的 ORDER BY 列进行排序,并按 index_granularity 参数切分成数据块,然后抽取每个数据块的第一行形成一份稀疏的排序索引。用户在查询时,如果查询条件包含主键列,则可以基于稀疏索引进行快速的裁剪。

这里通过下面的样例数据及对应的主键索引进行说明:

ClickHouse 部署架构图 clickhouse架构原理_主键_07

        样例中的主键列为 CounterID、Date,这里按每 7 个值作为一个数据块,抽取生成了主键索引 Marks 部分。

        当用户查询 CounterID equal ‘h’ 的数据时,根据索引信息,只需要读取 Mark number 为 6 和 7 的两个数据块。

ClickHouse 支持更多其他的索引类型,不同索引用于不同场景下的查询裁剪,具体汇总如下,更详细的介绍参考 ClickHouse 官方文档:

5. 物化视图(Cube/Rollup)

        物化视图是查询结果集的一份持久化存储,所以它与普通视图完全不同,而非常趋近于表。“查询结果集”的范围很宽泛,可以是基础表中部分数据的一份简单拷贝,也可以是多表join之后产生的结果或其子集,或者原始数据的聚合指标等等。所以,物化视图不会随着基础表的变化而变化,所以它也称为快照(snapshot)。如果要更新数据的话,需要用户手动进行,如周期性执行SQL,或利用触发器等机制。

        产生物化视图的过程就叫做“物化”(materialization)。广义地讲,物化视图是数据库中的预计算逻辑+显式缓存,典型的空间换时间思路。所以用得好的话,它可以避免对基础表的频繁查询并复用结果,从而显著提升查询的性能。它当然也可以利用一些表的特性,如索引。

ClickHouse 部署架构图 clickhouse架构原理_数据_08

        Clickhouse的物化视图的导入和查询数据流如上图所示:实线为导入流程,虚线为查询流程。基表数据 导入数据后,会像触发器一样将数据传递并应用到物化视图。查询时需要指定是查物化视图还是查base 表。用户可以直接向物化视图导入数据,此时基表数据不会有变化。Clickhouse数据导入不保证原子 性,同一批数据可能会依次生效,如果导入中途失败,此时数据库已经生效部分数据,此时需要用户重 新导入数据。

6. 其他特性

        除了前面所述,ClickHouse 还有非常多其他特性,抽取列举如下,更多详细内容可参考 ClickHouse官方文档:

  • SQL 方言:在常用场景下,兼容 ANSI SQL,并支持 JDBC、ODBC 等丰富接口。
  • 权限管控:支持 Role-Based 权限控制,与关系型数据库使用体验类似。
  • 多机多核并行计算:ClickHouse 会充分利用集群中的多节点、多线程进行并行计算,提高性能。
  • 近似查询:支持近似查询算法、数据抽样等近似查询方案,加速查询性能。
  • Colocated Join:数据打散规则一致的多表进行 Join 时,支持本地化的 Colocated Join,提升查询性能。

 五、ClickHouse 的不足

        前面介绍了大量 ClickHouse 的核心特性,方便读者了解 ClickHouse 高性能、快速发展的背后原因。当然,ClickHouse 作为后起之秀,远没有达到尽善尽美,还有不少需要待完善的方面,典型代表如下:

1. 分布式管控

分布式系统通常包含三个重要组成部分:

  • 存储引擎
  • 计算引擎
  • 分布式管控层

        ClickHouse 有一个非常突出的高性能存储引擎,但在分布式管控层显得较为薄弱,使得运营、使用成本偏高。主要体现在:

1.1 分布式表

        ClickHouse 对分布式表的抽象并不完整,在多数分布式系统中,用户仅感知集群和表,对分片和副本的管理透明,而在 ClickHouse 中,用户需要自己去管理分片、副本。

例如前面介绍的建表过程:用户需要先创建本地表(分片的副本),然后再创建分布式表,并完成分布式表到本地表的映射。

1.2 弹性伸缩

        ClickHouse 集群自身虽然可以方便的水平增加节点,但并不支持自动的数据均衡。例如,当包含 6 个节点的线上生产集群因存储或计算压力大,需要进行扩容时,我们可以方便的扩容到 10 个节点。但是数据并不会自动均衡,需要用户给已有表增加分片或者重新建表,再把写入压力重新在整个集群内打散,而存储压力的均衡则依赖于历史数据过期。

        ClickHouse在弹性伸缩方面的不足,大幅增加了业务在进行水平伸缩时运营压力。基于 ClickHouse 的当前架构,实现自动均衡相对复杂,导致相关问题的根因在于 ClickHouse 分组式的分布式架构:同一分片的主从副本绑定在一组节点上。更直接的说,分片间数据打散是按照节点进行的,自动均衡过程不能简单的搬迁分片到新节点,会导致路由信息错误。

而创建新表并在集群中进行全量数据重新打散的方式,操作开销过高。

ClickHouse 部署架构图 clickhouse架构原理_分布式_09

1.3 故障恢复

        与弹性伸缩类似,在节点故障的情况下,ClickHouse 并不会利用其它机器补齐缺失的副本数据。需要用户先补齐节点后,然后系统再自动在副本间进行数据同步。

2. 计算引擎

        虽然 ClickHouse 在单表性能方面表现非常出色,但是在复杂场景仍有不足,缺乏成熟的 MPP 计算引擎和执行优化器。

        例如:多表关联查询、复杂嵌套子查询等场景下查询性能一般,需要人工优化;缺乏 UDF 等能力,在复杂需求下扩展能力较弱等。

这也和 OLAP 系统第三方评测的结果相符。这对于性能如此出众的存储引擎来说,非常可惜。

3. 实时写入

        ClickHouse 采用类 LSM Tree 架构,并且建议用户通过批量方式进行写入,每个批次不少于 1000 行 或 每秒钟不超过一个批次,从而提高集群写入性能。实际测试情况下,32 vCPU&128G 内存的情况下,单节点写性能可达 50 MB/s~200 MB/s,对应 5w~20w TPS。

        但 ClickHouse 并不适合实时写入,原因在于 ClickHouse 并非典型的 LSM Tree 架构,它没有实现 Memory Table 结构,每批次写入直接落盘作为一棵 Tree(如果单批次过大,会拆分为多棵 Tree),每条记录实时写入会导致底层大量的小文件,影响查询性能。

这使得 ClickHouse 不适合有实时写入需求的业务,通常需要在业务和 ClickHouse 之间引入一层数据缓存层,实现批量写入。