大多数公司的日志系统检索使用的都是 ELK+Kafka+ES 的架构,在日志数据量不是特别庞大的时候其实这种架构还是挺好的,简单并且也很高效,但是当你的公司日志数据量非常庞大每分钟生产1亿条数据的场景下,这种架构的问题就很明显了,主要会出现下面几个问题:

  • 延迟很高,kafka收集push 的延迟变高
  • ES 插入性能迅速下降,大量插入请求只能排队不然 ES 会被打挂,限流排队也就意味着延迟变得更加高

我们这边文章只讨论数据存储层进行提效,那么就是 CK 来替代 ES 了

clickhouse 适用场景 hbase clickhouse使用场景_spring

 CK 高性能的4个特点上图已经说明很清楚,我们来详细了解下 CK 内部的核心内容

一般想要获得最佳的查询效率不外乎两种做法,一个是缩小查询返回的结果集大小一个是减少查询扫描的数据块。

想让查询变快,最简单有效的方法是减少数据扫描范围和数据存储时的大小。

列式存储是数据压缩的前提,可以有效减少查询时所需扫描的数据量(行式存储需要扫描并不需要的其他字段)并根据每一列具有相同类型和现实语义所以可以提供更高的压缩比。降低 I/O 和存储的压力,并为向量化执行做好铺垫。向量化可以简单看作一项消除程序中循环的优化。

实现向量化执行需要利用 CPU 的 SIMD (Single Instruction Multiple Data)指令用单条指令操作多条数据。ClickHouse 主要利用 SSE4.2 指令集。除此之外,ClickHouse 支持分区使,用多线程来提升在多分支判断场景下的性能表现。支持分片分布式计算,并使用多主架构(HDFS、Spark、HBase、ElasticSearch 是单主架构)来简化系统架构,天然回避了单点故障的问题,适合用于多数据中心、异地多活的场景。

MergeTree

在介绍具体方案之前先简单了解一下 MergeTree 的存储格式。MergeTree 是 ClickHouse 最主要使用的存储引擎,当创建表时可以通过 PARTITION BY 语句指定以某一个或多个字段作为分区字段,数据在磁盘上的目录结构类似如下形式:

$ ls -l /var/lib/clickhouse/data/<database>/<table>
drwxr-xr-x  2 test  test    64B Mar  8 13:46 202102_1_3_0
drwxr-xr-x  2 test  test    64B Mar  8 13:46 202102_4_6_1
drwxr-xr-x  2 test  test    64B Mar  8 13:46 202103_1_1_0
drwxr-xr-x  2 test  test    64B Mar  8 13:46 202103_4_4_0

以 202102_1_3_0 为例,202102 是分区的名称,1 是最小的数据块编号,3 是最大的数据块编号,0 是 MergeTree 的深度。可以看到 202102 这个分区不止一个目录,这是因为 ClickHouse 每次在写入的时候都会生成一个新的目录,并且一旦写入以后就不会修改(immutable)。每一个目录称作一个「part」,当 part 逐渐变多以后 ClickHouse 会在后台对多个 part 进行合并(compaction),通常的建议是不要保留过多 part,否则会影响查询性能。

每个 part 目录内部又由很多大大小小的文件组成,这里面既有数据,也有一些元信息,一个典型的目录结构如下所示:

$ ls -l /var/lib/clickhouse/data/<database>/<table>/202102_1_3_0
-rw-r--r--  1 test  test     ?? Mar  8 14:06 ColumnA.bin
-rw-r--r--  1 test  test     ?? Mar  8 14:06 ColumnA.mrk
-rw-r--r--  1 test  test     ?? Mar  8 14:06 ColumnB.bin
-rw-r--r--  1 test  test     ?? Mar  8 14:06 ColumnB.mrk
-rw-r--r--  1 test  test     ?? Mar  8 14:06 checksums.txt
-rw-r--r--  1 test  test     ?? Mar  8 14:06 columns.txt
-rw-r--r--  1 test  test     ?? Mar  8 14:06 count.txt
-rw-r--r--  1 test  test     ?? Mar  8 14:06 minmax_ColumnC.idx
-rw-r--r--  1 test  test     ?? Mar  8 14:06 partition.dat
-rw-r--r--  1 test  test     ?? Mar  8 14:06 primary.idx

其中比较重要的文件有:

  • primary.idx:这个文件包含的是主键信息,但不是当前 part 全部行的主键,默认会按照 8192 这个区间来存储,也就是每 8192 行存储一次主键。
  • ColumnA.bin:这是压缩以后的某一列的数据,ColumnA 只是这一列的代称,实际情况会是真实的列名。压缩是以 block 作为最小单位,每个 block 的大小从 64KiB 到 1MiB 不等。
  • ColumnA.mrk:这个文件保存的是对应的 ColumnA.bin 文件中每个 block 压缩后和压缩前的偏移。
  • partition.dat:这个文件包含的是经过分区表达式计算以后的分区 ID。
  • minmax_ColumnC.idx:这个文件包含的是分区字段对应的原始数据的最小值和最大值。

数据的操作是面向块对象进行的,并采用了流的形式。块对象由数据对象(列对象)、数据类型(DataType)和列名称(字符串)组成。列对象负责数据读取操作,使用泛化的设计思路。内存中一列由一个列对象表示, ~定义~ 了插入、分页以及过滤的 ~方法~ ,具体实现则由具体的对象实现。对于某个具体数值(列中的一行)的操作需要使用字段对象,字段对象使用聚合的设计模式,按数据类型提供不同的处理逻辑。DataType 负责序列化工作。覆盖常用类型并与列对象的设计思路一致,具体方法由对应的数据类型实例进行承载。

不过,块没有直接聚合列对象和 DataType 对象,而是通过 ColumnWithTypeAndName 对象进行间接引用。块流对象也使用了泛化的设计模式, 定义了读取运算以及输出的接口

表引擎(而不是表对象)由 IStorage 接口定义DDL、读写方法。不同的表引擎由不同的子类实现,表引擎负责根据 ATS 查询语句返回指定列的原始数据,后续数据处理由解释器对象完成。解释器对象负责解释 AST 并创建查询管道,串联整个查询流程:分析器将一条 SQL 语句根据 DDL 类型交由不同分析器分析成 AST 语法树,解释器会根据类型聚合所需资源,完成业务逻辑,最终返回块对象,以线程的形式建立起查询执行管道

Clickhouse常见应用场景

  • 电信行业用于存储数据和统计数据使用;
  • 新浪微博用于用户行为数据记录和分析工作;
  • 用于广告网络和RTB,电子商务的用户行为分析;
  • 日志分析;
  • 检测和遥感信息的挖掘;
  • 商业智能;
  • 网络游戏以及物联网的数据处理和价值数据分析;
  • 最大的应用来自于Yandex的统计分析服务Yandex.Metri ca。