ClickHouse架构明白与分布式场景
近来公司开发用到了ClickHouse,于是打算写一篇博客整理一下ClickHouse相关的内容特性
先看一个建表语句:
CREATE TABLE user_logs
(
log_id UInt64, -- 日志ID,唯一标识
user_id UInt32, -- 用户ID
event_type String, -- 事件类型(点击、浏览、购买等)
event_time DateTime, -- 事件发生时间
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)-- 按照事件时间的"年月"分区
ORDER BY (user_id, event_time) -- 主键顺序,优化查询
SETTINGS index_granularity = 8192; -- 索引粒度(默认 8192)
[*]首先是ENGINE,存储引擎的选择,ck支持不同表选择不同引擎,背面详细说说这个引擎
[*]PARTITION,这里是分区的概念,指明分区的键,ck中是每一个区为一个文件,一样平常对于大数据利用按天分区,如许扫描某一天数据的时候,只必要找到某一个分区,淘汰了扫描的数据量
[*]ORDER BY,这个是建表的时候必填的,他决定了物理存储的顺序,同时order的每一个键也是主键(主键可以多个且没有唯一约束)
[*]index_granularity这个是索引的粒度,每一个主键有一个索引,这里是稀疏索引,默认数目是8192个
列式存储和分区
列式存储
在 ClickHouse 里,数据是按照列(Column)而不是按行(Row)存储的,即:
传统行存储:每一行的数据存储在一起,例如:
(1, 'Alice', 23)
(2, 'Bob', 30)
如许查询某一列(比如 age)时,必要读取整个表的全部行然后提取 age 列,效率低。
列存储:ClickHouse 把同一列的数据存储在一起,例如:
id:
name: ['Alice', 'Bob', 'Charlie', 'David', 'Eve']
age:
如许读取 age 时,只必要访问 age 列的数据,极大淘汰了 IO 和计算量。同时也可以高效压缩,不同类型的数据适合不同的压缩算法,并且使得向量化计算(SIMD 指令优化)更加轻易,比如:计算 SUM(age) 时,ClickHouse 直接对 age 列进行批量加法计算,而不用解析整行数据。
分区
[*]Partition(分区):由 PARTITION BY 规则决定,数据按照分区存储。
[*]Part(数据块):每次插入(不管是一条还是多条)都会天生一个 Part 文件。
[*]合并(Merge):后台 Merge 历程会自动合并 Part,淘汰小文件,进步查询效率。
我们这里举一个例子,假如我们按月分区,建表语句如下:
CREATE TABLE user_events
(
user_id UInt32,
event_type String,
event_time DateTime
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)-- 按月分区
ORDER BY (user_id, event_time);
开始插入数据:
INSERT INTO user_events VALUES (1, 'click', '2024-03-10 12:00:00');
INSERT INTO user_events VALUES (2, 'view','2024-03-11 14:00:00');
INSERT INTO user_events VALUES (3, 'like','2024-03-12 15:00:00');
INSERT INTO user_events VALUES (4, 'share', '2024-04-05 16:00:00');
INSERT INTO user_events VALUES (5, 'comment', '2024-04-06 17:00:00');
在ck中,每一次插入,都会创建一个新的part
由于是 多次小批量插入,ClickHouse 天生多个 Part,202403说明是3月的数据,背面的数字是第几个part:
user_events/202403_1
user_events/202403_2
user_events/202403_3
user_events/202404_1
user_events/202404_2
后台 Merge线程 触发
ClickHouse 自动合并 202403 下的小 Part,最后:
user_events/202403_1_3 (3 条数据)合并完成
user_events/202404_1待合并
user_events/202404_2待合并
如许就使得ck可以高效插入,并且查询的时候ck还没有合并多个part,ClickHouse 在查询时会并行扫描多个 Part,纵然 Merge 历程还没有合并这些 Part,查询仍旧可以高效执行。
分区中的行式存储和列存储的概念区分:
分区(Partition)只是一个管理单元,用于进步查询效率,分区内部的数据存储仍旧是按列组织的。
在一个分区内,不同列的数据仍旧是分开存储的,只是同一批数据会被批量处理。 详细来说:
分区 = 多个列文件的聚集,并不会把数据按行混合存储。
例如,一个表有 id, name, age 三列,那么一个分区的存储可能是:
/data/table/partition_202403/
├── id.bin
├── name.bin
├── age.bin
├── marks.bin (索引)
每个 .bin 文件都存储着该列的全部数据,而不是存储“整个行”。
Engine
ClickHouse 的 MergeTree 和 LSM-Tree(Log-Structured Merge-Tree) 在存储和数据合并机制上有相似之处,但也有一些关键区别。 我们将这两个存储结构作为对比
区别:
对比项MergeTree(ClickHouse)LSM-Tree(RocksDB)存储方式列式存储(按列存放数据)行式存储(KV 方式)数据写入批量写入 Parts先写 WAL,再刷盘 SSTable查询优化有 稀疏索引,查询快适合 KV 读取后台合并Merge 历程合并 PartsCompaction 合并 SSTable
[*]LSM-Tree 适合高频 KV 读写(如 RocksDB)。
[*]MergeTree 适合大规模分析查询(OLAP),合并过程类似 LSM-Tree。
插入数据后 ClickHouse 如何存储?
ClickHouse MergeTree 数据流(插入 → 存储 → 查询)
1️ 客户端 INSERT 数据
2️ 数据先写入 WAL(写前日志)
3️ 存入 `Parts`(小文件,每列存一个文件)
4️ 后台 `Merge` 进程合并 `Parts`
5️ 查询时,使用 稀疏索引 直接定位数据
OrderBy 和 主键
因为数据是按照 ORDER BY 中的列排序的,以是 PRIMARY KEY 只是在这些列的底子上,天生一个 稀疏索引。稀疏索引是指只有一部门数据存储了索引信息(不是每一行数据都有索引),,通过它可以加速数据查找,但不会每次查询都扫描全部数据。
我们再举一个例子:
假设一个 Part 文件的巨细是 10MB,且该文件可以存储 1000 行数据,ClickHouse 会对此中的某些行创建索引。假如每 30 行就存一个索引,如许就会有 4 个索引点。
假设索引是基于 user_id 和 event_time 排序的,那么它就会创建如下的稀疏索引:
(1, 2024-03-10 12:00:00)
(30, 2024-03-11 14:00:00)
(60, 2024-03-12 15:00:00)
(90, 2024-03-13 16:00:00)
现在我们假如查询:
SELECT * FROM user_events WHERE user_id = 35 AND event_time = '2024-03-11 14:00:00'
ClickHouse 首先查找索引 (30, 2024-03-11 14:00:00),定位到对应的 Part,接下来再找到对应的数据
分布式ck
配置方式
[*]创建配置文件
<yandex>
<!-- 配置 Zookeeper -->
<zookeeper>
<!-- Zookeeper 集群地址配置 -->
<node>
<host>zk1.example.com</host> <!-- Zookeeper 节点1 -->
<port>2181</port> <!-- Zookeeper 服务端口 -->
</node>
<node>
<host>zk2.example.com</host> <!-- Zookeeper 节点2 -->
<port>2181</port> <!-- Zookeeper 服务端口 -->
</node>
<node>
<host>zk3.example.com</host> <!-- Zookeeper 节点3 -->
<port>2181</port> <!-- Zookeeper 服务端口 -->
</node>
<!-- 其他 Zookeeper 配置 -->
<session_timeout>10</session_timeout> <!-- Zookeeper 会话超时 -->
<operation_timeout>10</operation_timeout> <!-- 操作超时 -->
</zookeeper>
<!-- 配置集群的远程服务器(remote_servers) -->
<remote_servers>
<!-- 定义集群中的 shard 和 replica -->
<my_cluster> <!-- 集群名称 -->
<shard>
<replica>
<host>node1.example.com</host>
<port>9000</port>
</replica>
<replica>
<host>node2.example.com</host>
<port>9000</port>
</replica>
</shard>
<shard>
<replica>
<host>node3.example.com</host>
<port>9000</port>
</replica>
<replica>
<host>node4.example.com</host>
<port>9000</port>
</replica>
</shard>
</my_cluster>
</remote_servers>
<!-- Zookeeper 服务端的连接超时配置 -->
<zookeeper>
<node>
<host>zk1.example.com</host>
<port>2181</port>
</node>
</zookeeper>
<!-- 默认数据库,集群创建时可以选择 -->
<default_database>default</default_database>
<!-- 其他配置 -->
<macros>
<!-- 在这里可以定义集群宏,在外部可以使用 -->
<cluster_name>my_cluster</cluster_name>
</macros>
</yandex>
[*]当必要在分布式环境中进行数据存储时,你会利用 分布式表引擎Distributed。分布式结构中,会将数据分布在多个 节点(shards),按照指定键路由 上,每个节点会有独立的 副本(replicas)。
[*]在 ClickHouse 中,Zookeeper 是用来管理集群的调和和分布式元数据的。Zookeeper 在 ClickHouse 集群中负责管理分片的副本同步(由zk来做)、分布式表的元数据以及分布式查询的调和。
[*]创建本地表
在每个节点上,必要为 user_events 创建一个本地表
CREATE TABLE user_events_local
(
user_id UInt64,
event_time DateTime,
event_type String,
event_data String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)
ORDER BY (user_id, event_time);
[*]创建分布式表
在集群的每个节点上,还必要创建一个 分布式表,它会将查询转发到相应的本地表上。可以在此中一个节点上创建分布式表,就可以在各个节点上查到这个表,并通过它来访问全部节点上的本地表。利用 Distributed 引擎:
CREATE TABLE user_events_distributed
(
user_id UInt64,
event_time DateTime,
event_type String,
event_data String
) ENGINE = Distributed('my_cluster', 'default', 'user_events_local', user_id);
##配置文件中使用了宏(例如 cluster_name),那么可以在查询中动态地使用这些宏
CREATE TABLE user_events_distributed
(
user_id UInt64,
event_time DateTime,
event_type String,
event_data String
) ENGINE = Distributed('{cluster_name}', 'default', 'user_events_local', user_id);
‘my_cluster’:定义的集群名称。
‘default’:数据库名称(通常是 default)。
‘user_events_local’:本地表的表名,它会在集群中的每个节点上创建。
user_id:分片键,表示基于 user_id 进行分片,将数据分布到集群中。
分布式表和本地表的协作
分布式表 在 ClickHouse 中充当了一个代理的脚色,全部的查询和插入操作都会通过分布式表进行。分布式表的作用是将请求路由到集群中对应节点的 本地表,从而实现数据的分发与查询的并行化。
插入:
假设我们向分布式表 user_events_distributed 插入数据,此中分片是基于 user_id,ClickHouse 会根据 user_id 的值将数据分配到不同的节点::
INSERT INTO user_events_distributed (user_id, event_time, event_type, event_data)
VALUES
(1, '2025-03-15 10:00:00', 'click', 'button_click'),
(2, '2025-03-15 10:05:00', 'view', 'page_view');
在 node1 上,查询本地表:
SELECT * FROM user_events_local;
效果:
user_id| event_time | event_type | event_data
---------------------------------------------------------
1 | 2025-03-15 10:00:00| click | button_click
在 node2 上,查询本地表:
SELECT * FROM user_events_local;
效果:
user_id| event_time | event_type | event_data
---------------------------------------------------------
2 | 2025-03-15 10:05:00| view | page_view
查询:
查询 分布式表 时,ClickHouse 会根据查询条件将请求分发到各个节点上的本地表。假设我们查询分布式表 user_events_distributed:
SELECT * FROM user_events_distributed WHERE user_id = 1;
ClickHouse 会将查询请求发送到 node1 上的 user_events_local 本地表,得到如下效果:
user_id| event_time | event_type | event_data
---------------------------------------------------------
1 | 2025-03-15 10:00:00| click | button_click
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页:
[1]