导图社区 ClickHouse系统介绍
本图整理了ClickHouse系统介绍,包括ClickHouse的含义、核心特性、不足、使用场景、内部机制、如何使用等。
编辑于2021-11-15 08:44:13ClickHouse
概念
定义
ClickHouse是一款MPP架构、列式存储、面向OLAP、性能优异的DBMS
MPP:MPP是将任务并行地分散到多个服务器和节点上,在每个节点上计算完成后,将各自部分的结果汇总在一起得到最终的结果(与Hadoop相似)
核心特性
完备的DBMS功能
DDL
可动态增删改数据库、表和视图
DML
可动态增删改数据
权限控制
可以按照用户粒度设置数据库或表的操作权限,保障数据的安全性
数据备份与恢复
提供了数据备份导出与导入恢复机制
分布式管理
提供集群模式,能够自动管理多个数据库节点
列式存储与数据压缩
列式存储
列式存储,可按需扫描、读取所需的列,而不是行式存储的扫描、读取整行后再取出所需的列,故列式存储可有效减少所需扫描的数据量
数据压缩
同一列数据,拥有相同的数据类型和语义,重复的可能性更高,故列式存储具有更好的数据压缩能力
数据中的重复项越多,则压缩率越高;压缩率越高,则数据体量越小;而数据体量越小,则占用更少的存储空间,且数据在网络中传输越快,对网络带宽和磁盘IO的压力也就越小
压缩的本质是按照一定步长对数据进行匹配扫描,当发现重复部分的时候就进行编码转换
向量化执行
利用CPU的SIMD指令在CPU寄存器层面实现数据的并行操作
SIMD的全称是Single Instruction Multiple Data,即用单条指令操作多条数据
关系模型与SQL查询
ClickHouse使用关系模型描述数据并提供了传统数据库的概念(数据库、表、视图和函数等)。同时ClickHouse完全使用SQL作为查询语言(支持 GROUP BY/ORDER BY/JOIN/IN 等大部分标准SQL)
多样化的表引擎
与MySQL类似,ClickHouse也将存储部分进行了抽象,把存储引擎作为一层独立的接口。ClickHouse目前拥有MergeTree、内存、文件、接口和其它6大类20多种表引擎。每一种表引擎有各自的特点,用户可根据实际业务场景的要求,选择合适的表引擎使用
多线程与分布式
多线程
SIMD不适合用于带有较多分支判断的场景,ClickHouse也大量使用了多线程技术以实现提速,以此和向量化执行形成互补
分布式
ClickHouse在数据存取方面,支持通过分片进行分布式横向扩展,也支持通过副本实现高可用和集群负载均衡
预先将数据分布到各服务器,将数据的计算查询直接下推到数据所在的服务器
多主架构
ClickHouse采用Multi-Master多主架构,集群中的每个节点角色对等,客户端访问任意一个节点都能得到相同的效果
多主架构有许多优势,如对等的角色使系统架构变得更简单,不用再区分主控节点、数据节点和计算节点,集群中的所有节点功能相同。故它天然规避了单点故障的问题
在线查询
ClickHouse即便在复杂查询的场景下,也能够做到极快响应,且无须对数据进行任何预处理加工
Vertica这类商用软件价格高昂;SparkSQL与Hive这类系统无法保障90%的查询在1秒内返回,在大数据量下的复杂查询可能需要分钟级的响应时间;而ElasticSearch这类搜索引擎在处理亿级数据聚合查询时则捉襟见肘
数据分片与分布式查询
ClickHouse提供了本地表(Local Table)与分布式表(Distributed Table)的概念。一张本地表等同于一份数据的分片。而分布式表本身不存储任何数据,它是本地表的访问代理,其作用类似分库中间件。借助分布式表,能够代理访问多个数据分片,从而实现分布式查询
数据有序存储
ClickHouse支持在建表时,指定将数据按照某些列进行sort by。排序后,保证了相同sort key的数据在磁盘上连续存储,且有序摆放。在进行等值、范围查询时,where条件命中的数据都紧密存储在一个或若干个连续的Block中,而不是分散的存储在任意多个Block, 大幅减少需要IO的block数量。另外,连续IO也能够充分利用操作系统page cache的预取能力,减少page fault
主键索引
ClickHouse支持主键索引,它将每列数据按照index granularity(默认8192行)进行划分,每个index granularity的开头第一行被称为一个mark行。主键索引存储该mark行对应的primary key的值。对于where条件中含有primary key的查询,通过对主键索引进行二分查找,能够直接定位到对应的index granularity,避免了全表扫描从而加速查询
值得注意的是,ClickHouse的主键索引与MySQL等数据库不同,它并不会去重,即便主键相同的行,也可以同时存在于数据库中。要想实现去重效果,需要结合具体的表引擎ReplacingMergeTree、CollapsingMergeTree、VersionedCollapsingMergeTree实现
DML较弱
Clickhouse是个分析型数据库。分析型数据库中数据一般是不变的,因此Clickhouse对update、delete的支持是比较弱的,实际上并不支持标准的update、delete操作。Clickhouse通过alter方式实现更新、删除,它把update、delete操作叫做mutation(突变)。标准SQL的更新、删除操作是同步的,即客户端要等服务端反回执行结果(通常是int值);而Clickhouse的update、delete是通过异步方式实现的,当执行update语句时,服务端立即返回,但是实际上此时数据还没变,而是排队等着
Mutation过程
首先,使用where条件找到需要修改的分区;然后,重建每个分区,用新的分区替换旧的分区,分区一旦被替换,就不可回退;对于每个分区,可以认为是原子性的;但对于整个mutation,如果涉及多个分区,则不是原子性的
按照官方说明,update/delete 的使用场景是一次更新大量数据,也就是where条件筛选的结果应该是一大片数据。那能否一次只更新一条数据呢?当然也可以,但频繁这样操作,可能会对服务器造成压力:更新的单位是分区,若只更新一条数据,那么需要重建一个分区;若更新100条数据,而这100条数据落在3个分区上,则需重建3个分区;相对来说一次更新一批数据的整体效率远高于一次更新一行。对于频繁单条更新的这种场景,建议使用ReplacingMergeTree引擎来变相解决
不足
不支持事务
这是大部分OLAP数据库的通用缺点
不擅长根据主键按行粒度查询
但支持这种操作
不擅长按行删除数据
但支持这种操作
使用场景
内部机制
集群架构
典型的分组式的分布式架构
Shard :集群划分为多个分片(Shard),通过 Shard 的线性扩展能力,支持海量数据的分布式存储计算Node :每个 Shard 内包含一定数量的节点(Node),同一 Shard 内的节点互为副本,保障数据高可靠ZooKeeper Service :集群所有节点对等,节点间通过 ZooKeeper 服务进行分布式协调
单机结构
Cluster与Replication
ClickHouse的集群由分片(Shard)组成,而每个分片又通过副本(Replica)组成
这种分片、副本的概念,在一些流行的分布式系统中十分普遍
ClickHouse的1个节点只能拥有1个分片,即整个节点作为分片
这类似于MongoDB
ParserInterpreterStorages
Parser分析器负责创建AST对象;而Interpreter解释器则负责解释AST,并进一步创建查询的执行管道。它们与IStorage一起,串联起了整个数据查询的过程。Parser分析器可以将一条SQL语句以递归下降的方法解析成AST语法树的形式。不同的SQL语句,会经由不同的Parser实现类解析。Interpreter解释器的作用就像Service服务层一样,起到串联整个查询过程的作用,它会根据解释器的类型,聚合它所需要的资源。首先它会解析AST对象;然后执行"业务逻辑" ( 例如分支判断、设置参数、调用接口等 );最终返回IBlock对象,以线程的形式建立起一个查询执行管道
Block与Block流
ClickHouse内部的数据操作是面向Block对象进行的,并且采用了流的形式。Block对象可以看作数据表的子集。Block对象的本质是由数据对象、数据类型和列名称组成的三元组,即Column、DataType及列名称字符串。仅通过Block对象就能完成一系列的数据操作。Block流操作有两组顶层接口:IBlockInputStream负责数据的读取和关系运算,IBlockOutputStream负责将数据输出到下一环节
ClickHouse使用Block作为数据处理的核心抽象,表示在内存中的多个列的数据,其中列的数据在内存中也采用列存格式进行存储。示意图如左所示:其中header部分包含block相关元信息,而id UInt8、name String、_date Date则是三个不同类型列的数据表示
Column与Field
内存中的一列数据由一个Column对象表示。在IColumn接口对象中,定义了对数据进行各种关系运算的方法。在大多数场合,ClickHouse都会以整列的方式操作数据。如果需要操作单个具体的数值(即单列中的一行数据),则需要使用Field对象,Field对象代表某行某列的一个单值(可类比HBase Cell)
DataType
DataType负责数据的序列化和反序列化
Functions
ClickHouse主要提供两类函数:普通函数和聚合函数
存储格式
ClickHouse采用列存格式作为单机存储,并采用了类LSM tree的结构来进行组织与合并
默认情况下,分布式表写入是异步转发的。DistributedBlockOutputStream将Block按照建表DDL中指定的规则(如hash或random)切分为多个分片,每个分片对应本地的一个子目录,将对应数据落盘为子目录下的.bin文件,写入完成后就返回client成功。随后分布式表的后台线程,扫描这些文件夹并将.bin文件推送给相应的分片server
.bin文件的存储格式如左所示
一张MergeTree本地表,其磁盘文件构成如左图所示
本地表的数据被划分为多个Data PART,每个Data PART对应一个磁盘目录。Data PART在落盘后,就是immutable的,不再变化。ClickHouse后台会调度MergerThread将多个小的Data PART不断合并起来,形成更大的Data PART,从而获得更高的压缩率、更快的查询速度。当每次向本地表中进行一次insert请求时,就会产生一个新的Data PART,也即新增一个目录。如果insert的batch size太小,且insert频率很高,可能会导致目录数过多进而耗尽inode,也会降低后台数据合并的性能,这也是为什么ClickHouse推荐使用大batch进行写入且每秒不超过1次的原因
在Data PART内部存储着各个列的数据,由于采用了列存格式,所以不同列使用完全独立的物理文件。每个列至少有2个文件构成,分别是.bin 和 .mrk文件。其中.bin是数据文件,保存着实际的data;而.mrk是元数据文件,保存着数据的metadata。此外,ClickHouse还支持primary index、skip index等索引机制,所以也可能存在着对应的pk.idx,skip.idx文件
在数据写入过程中,数据被按照index_granularity切分为多个颗粒(granularity),默认值为8192行对应一个颗粒。多个颗粒在内存buffer中积攒到了一定大小(由参数min_compress_block_size控制,默认64KB),会触发数据的压缩、落盘等操作,形成一个block。每个颗粒会对应一个mark,该mark主要存储着2项信息:1)当前block在压缩后的物理文件中的offset,2)当前granularity在解压后block中的offset。所以Block是ClickHouse与磁盘进行IO交互、压缩/解压缩的最小单位,而granularity是ClickHouse在内存中进行数据扫描的最小单位
在查找时,如果query包含主键索引条件,则首先在pk.idx中进行二分查找,找到符合条件的颗粒mark,并从mark文件中获取block offset、granularity offset等元数据信息,进而将数据从磁盘读入内存进行查找操作。类似的,如果条件命中skip index,则借助于index中的minmax、set等信息,定位出符合条件的颗粒mark,进而执行IO操作。借助于mark文件,ClickHouse在定位出符合条件的颗粒之后,可以将颗粒平均分派给多个线程进行并行处理,最大化利用磁盘的IO吞吐和CPU的多核处理能力
如何使用
java
1.maven依赖<dependency> <groupId>com.github.housepower</groupId> <artifactId>clickhouse-native-jdbc</artifactId> <version>2.5.2</version></dependency>2.编码Class.forName("com.github.housepower.jdbc.ClickHouseDriver");Connection connection = DriverManager.getConnection("jdbc:clickhouse://192.168.60.131:9000");Statement statement = connection.createStatement();statement.executeQuery("create table test.example(day Date, name String, age UInt8) Engine=Log");PreparedStatement pstmt = connection.prepareStatement("insert into test.example values(?, ?, ?)");// insert 10 recordsfor (int i = 0; i < 10; i++) { pstmt.setDate(1, new Date(System.currentTimeMillis())); pstmt.setString(2, "panda_" + (i + 1)); pstmt.setInt(3, 18); pstmt.addBatch();}pstmt.executeBatch();Statement statement = connection.createStatement();String sql = "select * from test.jdbc_example";ResultSet rs = statement.executeQuery(sql);while (rs.next()) { // ResultSet 的下标值从 1 开始,不可使用 0,否则越界,报 ArrayIndexOutOfBoundsException 异常 System.out.println(rs.getDate(1) + ", " + rs.getString(2) + ", " + rs.getInt(3));}
SQL
FAQ
子主题