ClickHouse 相关概念

Clickhouse执行过程架构

  • 可以看到目前ClickHouse核心架构由下图构成,主要的抽象模块是Column、DataType、Block、Functions、Storage、Parser与Interpreter。

    简单来说,就是一条sql,会经由Parser与Interpreter,解析和执行,通过调用Column、DataType、Block、Functions、Storage等模块,最终返回数据,下面是各个模块具体的

  • Columns
    表示内存中的列(实际上是列块),需使用 IColumn 接口。该接口提供了用于实现各种关系操作符的辅助方法。几乎所有的操作都是不可变的:这些操作不会更改原始列,但是会创建一个新的修改后的列。

Column对象分为接口和实现两个部分,在IColumn接口对象中,定义了对数据进行各种关系运算的方法,例如插入数据的insertRangeFrom和insertFrom方法、用于分页的cut,以及用于过滤的filter方法等。而这些方法的具体实现对象则根据数据类型的不同,由相应的对象实现,例如ColumnString、ColumnArray和ColumnTuple等。

  • Field
    表示单个值,有时候也可能需要处理单个值,可以使用Field。Field 是 UInt64、Int64、Float64、String 和 Array 组成的联合。与Column对象的泛化设计思路不同,Field对象使用了聚合的设计模式。在Field对象内部聚合了Null、UInt64、String和Array等13种数据类型及相应的处理逻辑。

  • DataType
    IDataType 负责序列化和反序列化:读写二进制或文本形式的列或单个值构成的块。IDataType直接与表的数据类型相对应。比如,有 DataTypeUInt32、DataTypeDateTime、DataTypeString等数据类型。

IDataType与IColumn之间的关联并不大。不同的数据类型在内存中能够用相同的IColumn实现来表示。比如,DataTypeUInt32和DataTypeDateTime都是用ColumnUInt32或ColumnConstUInt32来表示的。另外,相同的数据类型也可以用不同的IColumn实现来表示。比如,DataTypeUInt8既可以使用ColumnUInt8 来表示,也可以使用过ColumnConstUInt8 来表示。

IDataType仅存储元数据。比如,DataTypeUInt8不存储任何东西(除了vptr);DataTypeFixedString仅存储N(固定长度字符串的串长度)。

IDataType具有针对各种数据格式的辅助函数。比如如下一些辅助函数:序列化一个值并加上可能的引号;序列化一个值用于 JSON 格式;序列化一个值作为 XML 格式的一部分。辅助函数与数据格式并没有直接的对应。比如,两种不同的数据格式 Pretty 和 TabSeparated 均可以使用 IDataType 接口提供的 serializeTextEscaped 这一辅助函数。

  • Block
    Block是表示内存中表的子集(chunk)的容器,是由三元组:(IColumn,IDataType,列名)构成的集合。在查询执行期间,数据是按 Block进行处理的。如果我们有一个Block,那么就有了数据(在IColumn对象中),有了数据的类型信息告诉我们如何处理该列,同时也有了列名(来自表的原始列名,或人为指定的用于临时计算结果的名字)。

当我们遍历一个块中的列进行某些函数计算时,会把结果列加入到块中,但不会更改函数参数中的列,因为操作是不可变的。之后,不需要的列可以从块中删除,但不是修改。这对于消除公共子表达式非常方便。

Block用于处理数据块。注意,对于相同类型的计算,列名和类型对不同的块保持相同,仅列数据不同。最好把块数据(block data)和块头(block header)分离开来,因为小块大小会因复制共享指针和列名而带来很高的临时字符串开销。

  • Block Stream
    块流用于处理数据。我们可以使用块流从某个地方读取数据,执行数据转换,或将数据写到某个地方。IBlockInputStream 具有 read 方法,其能够在数据可用时获取下一个块。IBlockOutputStream 具有 write 方法,其能够将块写到某处。

  • 块流负责:
    读或写一个表。表仅返回一个流用于读写块。
    完成数据格式化。比如,如果你打算将数据以Pretty格式输出到终端,你可以创建一个块输出流,将块写入该流中,然后进行格式化。
    执行数据转换。假设你现在有IBlockInputStream并且打算创建一个过滤流,那么你可以创建一个FilterBlockInputStream并用IBlockInputStream 进行初始化。之后,当你从FilterBlockInputStream中拉取块时,会从你的流中提取一个块,对其进行过滤,然后将过滤后的块返回给你。查询执行流水线就是以这种方式表示的。

  • Storage
    IStorage接口表示一张表。该接口的不同实现对应不同的表引擎。比如 StorageMergeTree、StorageMemory等。这些类的实例就是表。

IStorage 中最重要的方法是read和write,除此之外还有alter、rename和drop等方法。read方法接受如下参数:需要从表中读取的列集,需要执行的AST查询,以及所需返回的流的数量。read方法的返回值是一个或多个IBlockInputStream对象,以及在查询执行期间在一个表引擎内完成的关于数据处理阶段的信息。

在大多数情况下,read方法仅负责从表中读取指定的列,而不会进行进一步的数据处理。进一步的数据处理均由查询解释器完成,不由 IStorage 负责。

但是也有值得注意的例外:AST查询被传递给read方法,表引擎可以使用它来判断是否能够使用索引,从而从表中读取更少的数据。有时候,表引擎能够将数据处理到一个特定阶段。比如,StorageDistributed 可以向远程服务器发送查询,要求它们将来自不同的远程服务器能够合并的数据处理到某个阶段,并返回预处理后的数据,然后查询解释器完成后续的数据处理。

  • Parser与Interpreter
    Parser和Interpreter是非常重要的两组接口:Parser分析器负责创建AST对象;而Interpreter解释器则负责解释AST,并进一步创建查询的执行管道。它们与IStorage一起,串联起了整个数据查询的过程。Parser分析器可以将一条SQL语句以递归下降的方法解析成AST语法树的形式。不同的SQL语句,会经由不同的Parser实现类解析。例如,有负责解析DDL查询语句的ParserRenameQuery、ParserDropQuery和ParserAlterQuery解析器,也有负责解析INSERT语句的ParserInsertQuery解析器,还有负责SELECT语句的ParserSelectQuery等。

Interpreter解释器的作用就像Service服务层一样,起到串联整个查询过程的作用,它会根据解释器的类型,聚合它所需要的资源。首先它会解析AST对象;然后执行“业务逻辑”(例如分支判断、设置参数、调用接口等);最终返回IBlock对象,以线程的形式建立起一个查询执行管道。

  • Functions
    函数既有普通函数,也有聚合函数。

普通函数不会改变行数-它们的执行看起来就像是独立地处理每一行数据。实际上,函数不会作用于一个单独的行上,而是作用在以Block 为单位的数据上,以实现向量查询执行。

还有一些杂项函数,比如块大小、rowNumberInBlock,以及跑累积,它们对块进行处理,并且不遵从行的独立性。

ClickHouse 具有强类型,因此隐式类型转换不会发生。如果函数不支持某个特定的类型组合,则会抛出异常。但函数可以通过重载以支持许多不同的类型组合。比如,plus 函数(用于实现+运算符)支持任意数字类型的组合:UInt8+Float32,UInt16+Int8等。同时,一些可变参数的函数能够级接收任意数目的参数,比如concat函数。

实现函数可能有些不方便,因为函数的实现需要包含所有支持该操作的数据类型和IColumn类型。比如,plus函数能够利用C++模板针对不同的数字类型组合、常量以及非常量的左值和右值进行代码生成。

这是一个实现动态代码生成的好地方,从而能够避免模板代码膨胀。同样,运行时代码生成也使得实现融合函数成为可能,比如融合«乘-加»,或者在单层循环迭代中进行多重比较。

由于向量查询执行,函数不会«短路»。比如,如果你写 WHERE f(x) AND g(y),两边都会进行计算,即使是对于 f(x) 为 0 的行(除非f(x)是零常量表达式)。但是如果 f(x) 的选择条件很高,并且计算 f(x) 比计算 g(y) 要划算得多,那么最好进行多遍计算:首先计算 f(x),根据计算结果对列数据进行过滤,然后计算 g(y),之后只需对较小数量的数据进行过滤。

ClickHouse数据存储架构
ClickHouse数据存储架构由分片(Shard)组成,而每个分片又通过副本(Replica)组成。ClickHouse分片有限免两个特点。

ClickHouse的1个节点只能拥有1个分片,也就是说如果要实现1分片、1副本,则至少需要部署2个服务节点。
分片只是一个逻辑概念,其物理承载还是由副本承担的。
下面是cluster拥有1个shard(分片)和2个replica(副本),且副本由192.37.129.6服务节点和192.37.129.7服务节承载。从本质上看,这个配置是是一个分片一个副本,因为分片最终还是由副本来实现,所以这个其中一个副本是属于分片,分片是一个逻辑概念,它指的是其中的一个副本,这个和Elasticsearch中的分片和副本的概念有所不同。

 <ch_cluster>
        <shard>
            <replica>
                <host>192.37.129.6</host>
                <port>9000</port>
            </replica>
            <replica>
                <host>192.37.129.7</host>
                <port>9000</port>
            </replica>
        </shard>
    </ch_cluster>
  1. 引擎
    • MergeTree 系列,处理大规模的数据分析任务
      • 特点:
        • 数据按照主键排序存储在磁盘上,每个数据块都有一个最小值和最大值,方便范围查询
        • 数据按照分区键进行分区,每个分区可以包含多个数据块。分区可以在不同的节点上进行复制和负载均衡。
        • 数据可以按照一定规则进行合并,以减少数据块的数量和提高查询效率。
        • 数据可以设置索引(主键索引、辅助索引、全文索引等),以加速查询过滤条件。
        • 数据可以设置TTL(生存时间),以自动删除过期的数据。
      • 常见的表引擎
        • MergeTree: 基本的表引擎,不支持复制
        • ReplicatedMergeTree:支持复制的表引擎,需要指定一个zookeeper集群来管理元数据和协调复制操作。
        • SummingMergeTree:在合并数据时,可以对某些列进行求和操作,以减少存储空间和提高聚合查询效率。
        • AggregatingMergeTree:在合并数据时,可以对某些列进行聚合函数操作(如avg、min、max等),以实现预计算功能。
        • CollapsingMergeTree:在合并数据时,可以根据某些列的正负符号来抵消相同记录,以实现增量更新或删除功能。
      • 其他引擎
        • Log系列 用于存储小规模且不需要排序或索引的数据。例如TinyLog、StripeLog等。
        • Memory系列:用于存储内存中临时或易变的数据。例如Memory、Set等。
        • File系列:用于从文件中读取或写入数据。例如File、URL等。
        • Integration系列:用于与其他数据库或系统集成。例如MySQL、Kafka等。
        • Distributed:用于创建分布式表,在多个节点上执行查询,并将结果汇总返回
          2.MergeTree
          clickhouse的分区是指将数据按照分区键进行划分,每个分区可以包含多个数据块。分区可以提高查询效率,因为可以在分区键上进行分区裁剪,只查询必要的数据。分区也可以方便数据管理,比如删除、移动、备份等操作。在建表时,可以使用PARTITION BY子句来指定分区键,它可以是任意合法的表达式。例如:
          CREATE TABLE test (
          date Date,
          id UInt64,
          value Float64
          ) ENGINE = MergeTree()
          PARTITION BY toYYYYMM(date)
          ORDER BY id;
          这样就会按照日期的年月进行数据分区。

3 ReplicatedMergeTree
刚刚建立了MergeTree表,满足日常简单使用,但是我们生产的数据是需要高可靠以及高性能的,所以这个时候我们需要用到 ReplicatedMergeTree 这个表引擎,原因如下:

clickhouse的复制是指将相同的数据备份在不同的节点上,以保障数据的可靠性和增加查询并发能力。
复制需要依赖zookeeper集群来管理元数据和协调复制操作。目前支持复制的表引擎是ReplicatedMergeTree系列。在建表时,需要指定一个zookeeper路径和一个副本名称作为参数。例如:

CREATE TABLE test (
date Date,
id UInt64,
value Float64
) ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/test', '{replica}')
PARTITION BY toYYYYMM(date)
ORDER BY id;

这样就会创建一个支持复制的表,在不同的节点上使用不同的{shard}和{replica}值来创建相同结构的表。

4 ReplicatedMergeTree + Distributed
有了ReplicatedMergeTree来做数据复制,从而保障高可靠和高性能,我们还需要一个表来处理数据查询写入,因为用了ReplicatedMergeTree后数据分不到不同的节点上。

查询数据

各个实例之间会交换自己持有的分片的表数据
汇总到同一个实例上返回给用户


ClickHouse分布式表的本质并不是一张表, 而是一些本地物理表(分片)的分布式视图,本身并不存储数据. 分布式表建表的引擎为Distributed.
Distrbuted_table

CREATE TABLE test_distribution (
date Date,
id UInt64,
value Float64
) Distributed('ck_cluster', 'database', 'log', toYYYYMMDD(date));

Distributed引擎需要以下几个参数:

集群标识符-ck_cluster
本地表所在的数据库名称-database
本地表名称-log
分片键(sharding key) - 可选(toYYYYMMDD(date) )
该键与config.xml中配置的分片权重(weight)一同决定写入分布式表时的路由, 即数据最终落到哪个物理表上. 它可以是表中一列的原始数据(如site_id), 也可以是函数调用的结果, 如上面的SQL语句采用了随机值rand(). 注意该键要尽量保证数据均匀分布, 另外一个常用的操作是采用区分度较高的列的哈希值, 如intHash64(user_id).

二 clickhouse 有多快
跟火箭一样快,来横向对比一下。

  • 与ES(Elasticsearch)对比,ClickHouse的查询速度快5-30倍以上,并且占用更少的磁盘空间和内存资源
  • 与MySQL对比,ClickHouse的查询速度快800倍以上,并且可以在单个服务器上每秒处理数百个查询
  • 与Hive对比,ClickHouse的查询速度快200倍以上,并且可以使用tab-separated格式将数据写入到MergeTree表中时,写入速度大约为50到200MB/s
    如果想要更精确地测试ClickHouse的查询性能,可以使用clickhouse-benchmark工具来进行压力测试和性能分析。

哎,到这就有个疑问❓ 凭啥你那么屌?
不同的角度来分析一下

  • 存储引擎

    • 火车跑的快,全靠车头带,车头就是引擎
    • 列式存储:
      • 更好的利用压缩算法、减少数据大小和磁盘IO次数,因为同一列的数据通常具有较高的局部性和重复性,所以可以使用 RLE 编码或者差分编码等方式进行压缩,而且压缩后的数据可以直接进行计算,不需要解压。
      • 列式存储可以更好地支持 OLAP (Online Analytical Processing) 系统。因为 OLAP 系统通常只需要对单列或者少数几列进行过滤、聚合或者统计等操作,所以列式存储可以避免读取不必要的数据,提高查询效率
  • 预排列和索引

    • ClickHouse 将数据按照分区键和主键进行排序,然后分成多个数据块,每个数据块包含若干行记录。
    • ClickHouse 将每个数据块中的同一列的数据保存在一个文件中,不同列的数据保存在不同文件中。这样可以实现按列读取和压缩数据。
    • ClickHouse 为每个数据块生成一个主键索引文件,用于快速定位满足查询条件的数据块。
    • ClickHouse 还为每个分区生成一个稀疏索引文件,用于快速定位满足查询条件的分区。
      最后ClickHouse还会对每个表进行定期的合并操作,将多个小文件合并成一个大文件,并删除重复或过期的数据。这样可以减少磁盘空间占用和查询时需要扫描的文件数量。
  • 压缩

    • ClickHouse支持多种方式的数据压缩,可以根据不同的权衡选择合适的压缩效率和CPU消耗:比如LZ4和ZSTD。LZ4在速度上会更快,但是压缩率较低,ZSTD在速度上会稍慢,但是压缩率较高。
    • ClickHouse可以根据数据类型和分布选择合适的压缩算法,例如对于数值型数据可以使用Gorilla算法,对于字符串可以使用DoubleDelta算法。
    • ClickHouse还提供了针对特定类型数据的专用编解码器,例如Gorilla、DoubleDelta等,可以在牺牲数据精度的情况下提高查询速度 。
    • ClickHouse可以对每个列块进行独立的压缩和解压缩,提高了并发性能和内存利用率。
  • 存储引擎的角度分析

    • 大量的向量化运算


    • 为了实现向量化执行,需要利用CPU的SIMD指令。SIMD的全称是Single Instruction Multiple Data,即用单条指令操作多条数据。现代计算机系统概念中,它是通过数据并行以提高性能的一种实现方式,它的原理是在CPU寄存器层面实现数据的并行操作。ClickHouse目前利用SSE4.2指令集实现向量化执行
      向量化运行:ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。
      在这种设计下,单条Query就能利用整机所有CPU。极致的并行处理能力,极大的降低了查询延时

    • 多线程并行计算

      • ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。
        在这种设计下,单条Query就能利用整机所有CPU。极致的并行处理能力,极大的降低了查询延时
    • 分布式数据
      ClickHouse将数据划分为多个partition,每个partition再进一步划分为多个index granularity,然后通过多个CPU核心分别处理其中的一部分来实现并行数据处理。
      在这种设计下,单条Query就能利用整机所有CPU。极致的并行处理能力,极大的降低了查询延时。

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注