概述
ClickHouse 是一款 MPP 架构的列式存储数据库,拥有完备的管理功能,所以他称得上是一个 DBMS 数据库管理系统,而不仅仅是一个数据库。
如果你想学习的话,那就一起来吧。
完备的 DBMS 功能
作为 DBMS 具备以下基本功能:
DDL(数据定义语言):可以动态地创建、修改和删除数据库、表和视图,而无需重启服务。DML(数据操作语言):可以动态查询、插入、修改和删除数据。- 权限控制:可以按照用户粒度设置数据库或者表的操作权限,保障数据的安全性。
- 数据备份与恢复:提供了数据备份导出与导入机制,满足生产环境的需求。
- 分布式管理:提供集群模式,能够自动管理多个数据库节点。
列式存储与数据压缩
列式存储和数据压缩对于一款高性能数据库来说是必不可少的特性。
按列存储相比按行存储的另一个优势就是对数据压缩的友好性。并且由于压缩的本质是按照一定步长对数据进行匹配扫描,当发现重复部分的时候就进行编码转换。而在同一列的数据中,他们会拥有相同的数据类型和现实语义,重复项的可能性自然更高。
向量化执行引擎
能用钱解决的问题,千万别花时间。这句话是虽然是一句玩笑话,但是在实际中硬件层面的优化是最直接、最高效的提升性能途径之一。当然向量化执行就是这种方式的代表,寄存器硬件层面的特性,为上层应用程序的性能带来了指数级的提升。
为了实现向量化执行,需要利用 CPU 的 SIMD 命令。SIMD 的全称是 Single Instruction Multiple Data,即用单条指令操作多条数据。在现代计算机概念中,通过数据并行以提高性能的一种实现方式,原理即在 CPU 寄存器层面实现数据的并行操作。ClickHouse 目前利用 SSE4.2 指令集实现向量化执行。
关系模型和 SQL 查询
ClickHouse 完全使用 SQL 作为查询语言(支持 GROUP BY, ORDER BY, JOIN, IN 等大部分标准 SQL),这会使得它平易近人、容易理解和学习。
在 SQL 解析方面 ClickHouse 是大小写敏感的,这意味着 SELECT a 和 SELECT A 所代表的语义是不同的。ClickHouse 使用了关系模型,所以将构建在传统关系型数据库或者数据仓库上的系统迁移到 ClickHouse 的成本会降低很多,可以直接沿用之前的成果。
多样化的表引擎
ClickHouse 共拥有合并树、内存、文件、接口和其他六大类二十多种表引擎,其中每一种表引擎都有各自的特点,用户可以根据世界业务场景的需求选择合适的表引擎使用。
将表引擎单独设计的好处是显而易见的,通过特定的表引擎支撑特定的场景,十分灵活,对于简单的场景,可直接使用简单的引擎降低成本,而复杂的场景也有合适的引擎。
多线程与分布式
前面提到的向量化执行通过数据并行的方式提高性能,那么多线程处理就是通过线程级并行方式实现了性能的提升。
多主结构
ClickHouse 采用了 Multi-Master 多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果,这种多主的结构有许多优势,例如对等的角色使系统架构变得更加简单,不用再区分主控节点、数据节点和计算节点,集群中的所有节点功能相同。所以他天然规避了单点故障问题,非常适合多数据中心、异地多活的场景。
在线查询
ClickHouse 完美平衡成本和性能,采用了 LSM 树结构,使得数据的插入量可以很大。同时由于 ClickHouse 的内部优化,使得在复杂查询的场景下能够做到极快响应,且无须对数据进行任何预处理加工,即真 在线。
数据分片与分布式查询
ClickHouse 支持分片,而分片依赖集群,每个集群有一个到多个分片组成,而每个分片则对应了 ClickHouse 的一个服务节点,分片的数量上限取决于节点数量(一个分片对应一个服务节点)。ClickHouse 提供了本地表(Local Table)和分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片,而分布式表本身不存储任何数据,仅仅只是本地表的访问代理,其作用类似于分库中间件,借助分布式表,能够代理访问多个数据分片,从而实现分布式查询。
所以在使用单个节点的本地表(单个数据分片)即可满足业务需求的基础上,待业务增长后,再通过新增数据分片的方式分流数据,并通过分布式表实现分布式查询。
不足之处
上面说有这么多的优点,那么当然也是会有缺点的,毕竟人无完人,物无完物。
- 不支持事务。
- 不擅长根据主键按行粒度进行查询(虽然支持)。
- 不擅长按行删除数据(虽然支持)。
架构设计

Column 和 Field
Column 和 Field 是 ClickHouse 数据最基础的映射单元,作为一款百分之百的按列存储数据,内存中的一列数据库由一个 Column 对象表示。Column 对象分为接口和实现两个部分,在 ICloumn 接口对对象中,定义了对数据进行各种关系运算的方法。
在大多数场合下,ClickHouse 都会以整列的方式操作数据,但凡也有例外,如果需要操作单个具体的数值(也就是单列中的一行数据),则需要使用 Field 对象,Field 对象代表一个单值,与 Column 对象的泛化设计思路不同,Field 对象使用了聚合的设计模式,在 Field 对象内部聚合了 Null、UInt64、String等十三种数据结构类型及对应的处理逻辑。
DataType
数据的序列化和反序列化工作由 DataType 负责。IDataType 接口定义了许多正反序列的方法,他们成对出现,例如 serializeBinary 和 deserializeBinary 等,覆盖常见的二进制、文本、JSON、XML 等多种格式类型。DataType 虽然负责序列化相关工作,但它并不直接负责数据的读取,而实际转由从 Column 或 Field 对象中获取数据。在 DataType 的实现类中,聚合了相应数据类型的 Column 对象和 Field 对象。
Block 和 Block 流
ClickHouse 内部的数据操作是面向 Block 对象进行的,并且采用了流的形式。虽然 Column 和 Field 组成了数据的基本映射单元,但对应到实际的操作中,他们还是缺少了一些必要的信息,比如数据类型和列名称。Block 对象可以看作数据表的子集,Block 对象的本质是由数据对象、数据类型和列名称组成的三元组,即 Column、 DataType 和列名称字符串。其中 Column 提供数据的读取能力,而 DataType 知道如何正反序列化,所以 Block 在这些对象的基础上进行进一步的抽象和封装,从而简化并完成一系列的数据操作。
在具体的实际中,Block 并没有直接聚合 Column 和 DataType 对象,而是通过 ColumnWithTypeAndName 对象进行间接引用。
在有了 Block 对象这一层封装之后,在 Block 流的设计就是水到渠成的事情,流操作有两个顶层的接口,其中 IBlockInputStream 负责数据的读取和关系运算,IBlockOutputStream 负责将数据输出到下一环节。同样的 Block 也使用了泛化的设计,对数据的各种操作最终都会转化为之类其中一种流的实现,IBlockInputStream 接口定义读取数据的若干个 read 虚方法都会由具体的实现类来填充。当然 IBlockOutputStream 与之类似同样定义了若干个 write 虚方法,均由实现类来填充具体逻辑。
IBlockInputStream 接口共有六十多个实现类,大致可以分为三类:
- 处理数据定义的
DDL操作。 - 处理关系运算的相关操作。
- 与表引擎相呼应,每一种表引擎都有与之对应的
BlockInputStream实现。
Table
在数据表的底层设计中并没有所谓的 Table 对象,直接使用 IStorage 接口指代数据表。其中 IStorage 接口定义了 DDL、read、write 方法,这些方法分别负责数据的定义、查询与写入。在数据查询时 IStorage 负责根据 AST 查询语句的指示要求,返回数据列的原始数据。后续对数据的进一步加工、计算和过滤,则会统一交由 Interpreter 解释器对象处理。
对 Table 发起的一次操作通常都会经历如此的过程,接收 AST 查询语句,根据 AST 返回指定列的数据,之后再将数据交由 Interpreter 做进一步处理。
Parser 和 Interpreter
Parser 和 Interpreter 是非常重要的两组接口,Parser 分析器负责创建 AST 对象,而 Interpreter 解释器则负责解释 AST,并进一步创建查询的执行管道。他们与 IStorage 一起,串联起整个查询的过程。Parser 分析器可以将一条 SQL 语句以递归下降的方法分解成 AST 语法树的形式,不同的 SQL 会经由不同的 Parser 实现类解析。Interpreter 解释器的作用就像 service 服务层一样,起到串联整个查询过程的作用,他会根据解释器的类型,聚合它所需要的资源。首先他会解析 AST 对象,然后执行 业务逻辑(如分支判断、设置参数、调用接口等),最终返回 IBlock 对象,以线程的形式建立起一个查询执行管道。
Functions 和 Aggregate Functions
ClickHouse 主要提供了两类函数,普通函数和聚合函数。
其中普通函数由 IFunction 接口定义,拥有数十种函数实现,另外普通函数是没有状态的,函数效果作用于每行数据之上,当然在数据具体执行的过程中,并不会一行一行地运算,而是采用向量化的方式直接作用与一整列数据。
聚合函数由 IAggregateFunction 接口定义,相比无状态的普通函数,聚合函数是由状态的,另外聚合函数的状态支持序列化和反序列化,能够在分布式节点之间进行传输,以实现增量计算。
Cluster 和 Replication
ClickHouse 的集群由分片(Shard)组成,而每个分片又通过副本(Replica)组成,这种分层的概念,在一些流行的分布式系统中十分普遍。
在 ClickHouse 中有几个与众不同的特性:
ClickHouse的一个节点只能有一个一个分片,也就是说若要实现一分片、一副本,则至少需要部署两个服务节点。- 分片只是一个逻辑概念,其物理承载还是由副本承担的。
引用
个人备注
此博客内容均为作者学习所做笔记,侵删!
若转作其他用途,请注明来源!