Clickhouse 架构设计
Column 与 Field
Column 和 Field 是 ClickHouse 数据最基础的映射单元。作为一款百分之百的列式存储数据库,
ClickHouse 按列存储数据,内存中的一列数据由一个 Column 对象表示。
Column 对象分为接口和实现 两个部分,在 IColumn
接口对象中,定义了对数据进行各种关系运算的方法,例如插入数据的 insertRangeFrom 和 insertFrom 方法、用于分页的 cut,以及用于过滤的 filter 方法等。而这些方法的具 体实现对象则根据数据类型的不同,由相应的对象实现,例如 ColumnString、ColumnArray 和 ColumnTuple 等。在大多数场合,ClickHouse 都会以整列的方式操作数据,但凡事也有例外。如果需要 操作单个具体的数值 ( 也就是单列中的一行数据 ),则需要使用 Field 对象,Field 对象代表一个单值。 与 Column 对象的泛化设计思路不同,Field 对象使用了聚合的设计模式。在 Field 对象内部聚合了 Null、 UInt64、String 和 Array 等 13 种数据类型及相应的处理逻辑。 一个列中的数据一般是以文件单独存储的
DataType
数据的序列化和反序列化工作由 DataType 负责。IDataType 接口定义了许多正反序列化的方法,它们
成对出现,例如 serializeBinary 和 deserializeBinary、serializeTextJSON 和 deserializeTextJSON 等,涵盖 了常用的二进制、文本、JSON、XML、CSV 和 Protobuf 等多种格式类型。IDataType 也使用了泛化的 设计模式,具体方法的实现逻辑由对应数据类型的实例承载,例如 DataTypeString、DataTypeArray 及 DataTypeTuple 等。
BloCK 与 BloCK 流
ClickHouse 内部的数据操作是面向 BloCK 对象进行的,并且采用了流的形式。虽然 Column 和 Filed
组成了数据的基本映射单元,但对应到实际操作,它们还缺少了一些必要的信息,比如数据的类型及 列的名称。于是 ClickHouse 设计了 BloCK 对象,BloCK 对象可以看作数据表的子集。BloCK 对象的本 质是由数据对象、数据类型和列名称组成的三元组,即 Column、DataType 及列名称字符串。Column 提供了数据的读取能力,而 DataType 知道如何正反序列化,所以 BloCK 在这些对象的基础之上实现了 进一步的抽象和封装,从而简化了整个使用的过程,仅通过 BloCK 对象就能完成一系列的数据操作。 在 具 体 的 实 现 过 程 中 , BloCK 并 没 有 直 接 聚 合 Column 和 DataType 对 象 , 而 是 通 过 ColumnWithTypeAndName 对象进行间接引用。
有了 BloCK 对象这一层封装之后,对 BloCK 流的设计就是水到渠成的事情了。流操作有两组顶层
接口:IBloCKInputStream 负责数据的读取和关系运算,IBloCKOutputStream 负责将数据输出到下一环 节。BloCK 流也使用了泛化的设计模式,对数据的各种操作最终都会转换成其中一种流的实现。 IBloCKInputStream 接口定义了读取数据的若干个 read 虚方法,而具体的实现逻辑则交由它的实现类来 填充。
IBloCKInputStream 接口总共有 60 多个实现类,它们涵盖了 ClickHouse 数据摄取的方方面面。这
些实现类大致可以分为三类:第一类用于处理数据定义的 DDL 操作,例如 DDLQueryStatusInputStream 等;第二类用于处理关系运算的相关操作,例如 LimitBloCKInput-Stream、JoinBloCKInputStream 及 AggregatingBloCKInputStream 等;第三类则是与表引擎呼应,每一种表引擎都拥有与之对应的 BloCKInputStream 实 现 , 例 如 MergeTreeBaseSelect-BloCKInputStream ( MergeTree 表 引 擎 ) 、 TinyLogBloCKInputStream ( TinyLog 表引擎 ) 及 KafkaBloCKInputStream ( Kafka 表引擎 ) 等。 IBloCKOutputStream 的设计与 IBloCKInputStream 如出一辙。IBloCKOutputStream 接口同样也定义了若干写入数据的 write 虚方法。它的实现类比 IBloCKInputStream 要少许多,一共只有 20 多种。这些实现 类 基 本 用 于 表 引 擎 的 相 关 处 理 , 负 责 将 数 据 写 入 下 一 环 节 或 者 最 终 目 的 地 , 例 如 MergeTreeBloCKOutputStream 、TinyLogBloCKOutputStream 及 StorageFileBloCK-OutputStream 等。
Table
在数据表的底层设计中并没有所谓的 Table 对象,它直接使用 IStorage 接口指代数据表。表引擎是
ClickHouse 的一个显著特性,不同的表引擎由不同的子类实现,例如 IStorageSystemOneBloCK ( 系统 表 )、StorageMergeTree ( 合并树表引擎 ) 和 StorageTinyLog ( 日志表引擎 ) 等。IStorage 接口定义了 DDL ( 如 ALTER、RENAME、OPTIMIZE 和 DROP 等 ) 、read 和 write 方法,它们分别负责数据的定 义、查询与写入。在数据查询时,IStorage 负责根据 AST 查询语句的指示要求,返回指定列的原始数 据。后续对数据的进一步加工、计算和过滤,则会统一交由 Interpreter 解释器对象处理。对 Table 发起 的一次操作通常都会经历这样的过程,接收 AST 查询语句,根据 AST 返回指定列的数据,之后再将数 据交由 Interpreter 做进一步处理。
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 对象,以线程的形式建立起一个查询执行管道。
分片与副本
ClickHouse 的集群由分片 ( Shard ) 组成,而每个分片又通过副本 ( Replica )
组成。这种分层的概念,在一些流行的分布式系统中十分普遍。例如,在 Elasticsearch 的概念中,一个索引由分片和副本组成, 副本可以看作一种特殊的分片。如果一个索引由 5 个分片组成,副本的基数是 1,那么这个索引一共会 拥有 10 个分片 ( 每 1 个分片对应 1 个副本 )。
如果你用同样的思路来理解 ClickHouse 的分片,那么很可能会在这里栽个跟头。ClickHouse 的某些设
计总是显得独树一帜,而集群与分片就是其中之一。这里有几个与众不同的特性。
ClickHouse 的 1 个节点只能拥有 1 个分片,也就是说如果要实现 1 分片、1 副本,则至少需要部署 2
个服务节点。
分片只是一个逻辑概念,其物理承载还是由副本承担的。