ClickHouse架构明白与分布式场景

打印 上一主题 下一主题

主题 1308|帖子 1308|积分 3924

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
近来公司开发用到了ClickHouse,于是打算写一篇博客整理一下ClickHouse相关的内容
特性

先看一个建表语句:
  1. CREATE TABLE user_logs
  2. (
  3.     log_id          UInt64,                -- 日志ID,唯一标识
  4.     user_id         UInt32,                -- 用户ID
  5.     event_type      String,                 -- 事件类型(点击、浏览、购买等)
  6.     event_time      DateTime,               -- 事件发生时间
  7. )
  8. ENGINE = MergeTree
  9. PARTITION BY toYYYYMM(event_time)  -- 按照事件时间的"年月"分区
  10. ORDER BY (user_id, event_time)      -- 主键顺序,优化查询
  11. SETTINGS index_granularity = 8192;   -- 索引粒度(默认 8192)
复制代码


  • 首先是ENGINE,存储引擎的选择,ck支持不同表选择不同引擎,背面详细说说这个引擎
  • PARTITION,这里是分区的概念,指明分区的键,ck中是每一个区为一个文件,一样平常对于大数据利用按天分区,如许扫描某一天数据的时候,只必要找到某一个分区,淘汰了扫描的数据量
  • ORDER BY,这个是建表的时候必填的,他决定了物理存储的顺序,同时order的每一个键也是主键(主键可以多个且没有唯一约束)
  • index_granularity这个是索引的粒度,每一个主键有一个索引,这里是稀疏索引,默认数目是8192个
列式存储和分区

列式存储

在 ClickHouse 里,数据是按照列(Column)而不是按行(Row)存储的,即:
传统行存储:每一行的数据存储在一起,例如:
  1. (1, 'Alice', 23)
  2. (2, 'Bob', 30)
复制代码
如许查询某一列(比如 age)时,必要读取整个表的全部行然后提取 age 列,效率低。
列存储:ClickHouse 把同一列的数据存储在一起,例如:
  1. id:   [1, 2, 3, 4, 5]
  2. name: ['Alice', 'Bob', 'Charlie', 'David', 'Eve']
  3. age:  [23, 30, 29, 35, 28]
复制代码
如许读取 age 时,只必要访问 age 列的数据,极大淘汰了 IO 和计算量。同时也可以高效压缩,不同类型的数据适合不同的压缩算法,并且使得向量化计算(SIMD 指令优化)更加轻易,比如:计算 SUM(age) 时,ClickHouse 直接对 age 列进行批量加法计算,而不用解析整行数据。
分区


  • Partition(分区):由 PARTITION BY 规则决定,数据按照分区存储。
  • Part(数据块):每次插入(不管是一条还是多条)都会天生一个 Part 文件。
  • 合并(Merge):后台 Merge 历程会自动合并 Part,淘汰小文件,进步查询效率。
我们这里举一个例子,假如我们按月分区,建表语句如下:
  1. CREATE TABLE user_events
  2. (
  3.     user_id UInt32,
  4.     event_type String,
  5.     event_time DateTime
  6. ) ENGINE = MergeTree()
  7. PARTITION BY toYYYYMM(event_time)  -- 按月分区
  8. ORDER BY (user_id, event_time);
复制代码
开始插入数据:
  1. INSERT INTO user_events VALUES (1, 'click', '2024-03-10 12:00:00');
  2. INSERT INTO user_events VALUES (2, 'view',  '2024-03-11 14:00:00');
  3. INSERT INTO user_events VALUES (3, 'like',  '2024-03-12 15:00:00');
  4. INSERT INTO user_events VALUES (4, 'share', '2024-04-05 16:00:00');
  5. INSERT INTO user_events VALUES (5, 'comment', '2024-04-06 17:00:00');
复制代码
  在ck中,每一次插入,都会创建一个新的part
  由于是 多次小批量插入,ClickHouse 天生多个 Part,202403说明是3月的数据,背面的数字是第几个part:
  1. user_events/202403_1
  2. user_events/202403_2
  3. user_events/202403_3
  4. user_events/202404_1
  5. user_events/202404_2
复制代码
后台 Merge线程 触发
ClickHouse 自动合并 202403 下的小 Part,最后:
  1. user_events/202403_1_3   (3 条数据)  合并完成
  2. user_events/202404_1  待合并
  3. 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 的 MergeTreeLSM-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. 1️ 客户端 INSERT 数据
  2. 2️ 数据先写入 WAL(写前日志)
  3. 3️ 存入 `Parts`(小文件,每列存一个文件)
  4. 4️ 后台 `Merge` 进程合并 `Parts`
  5. 5️ 查询时,使用 稀疏索引 直接定位数据
复制代码
OrderBy 和 主键

因为数据是按照 ORDER BY 中的列排序的,以是 PRIMARY KEY 只是在这些列的底子上,天生一个 稀疏索引。稀疏索引是指只有一部门数据存储了索引信息(不是每一行数据都有索引),,通过它可以加速数据查找,但不会每次查询都扫描全部数据。
我们再举一个例子:
假设一个 Part 文件的巨细是 10MB,且该文件可以存储 1000 行数据,ClickHouse 会对此中的某些行创建索引。假如每 30 行就存一个索引,如许就会有 4 个索引点。
假设索引是基于 user_id 和 event_time 排序的,那么它就会创建如下的稀疏索引:
  1. (1, 2024-03-10 12:00:00)
  2. (30, 2024-03-11 14:00:00)
  3. (60, 2024-03-12 15:00:00)
  4. (90, 2024-03-13 16:00:00)
复制代码
现在我们假如查询:
  1. 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

配置方式


  • 创建配置文件
  1. <yandex>
  2.     <!-- 配置 Zookeeper -->
  3.     <zookeeper>
  4.         <!-- Zookeeper 集群地址配置 -->
  5.         <node>
  6.             <host>zk1.example.com</host> <!-- Zookeeper 节点1 -->
  7.             <port>2181</port>            <!-- Zookeeper 服务端口 -->
  8.         </node>
  9.         <node>
  10.             <host>zk2.example.com</host> <!-- Zookeeper 节点2 -->
  11.             <port>2181</port>            <!-- Zookeeper 服务端口 -->
  12.         </node>
  13.         <node>
  14.             <host>zk3.example.com</host> <!-- Zookeeper 节点3 -->
  15.             <port>2181</port>            <!-- Zookeeper 服务端口 -->
  16.         </node>
  17.         <!-- 其他 Zookeeper 配置 -->
  18.         <session_timeout>10</session_timeout> <!-- Zookeeper 会话超时 -->
  19.         <operation_timeout>10</operation_timeout> <!-- 操作超时 -->
  20.     </zookeeper>
  21.     <!-- 配置集群的远程服务器(remote_servers) -->
  22.     <remote_servers>
  23.         <!-- 定义集群中的 shard 和 replica -->
  24.         <my_cluster> <!-- 集群名称 -->
  25.             <shard>
  26.                 <replica>
  27.                     <host>node1.example.com</host>
  28.                     <port>9000</port>
  29.                 </replica>
  30.                 <replica>
  31.                     <host>node2.example.com</host>
  32.                     <port>9000</port>
  33.                 </replica>
  34.             </shard>
  35.             <shard>
  36.                 <replica>
  37.                     <host>node3.example.com</host>
  38.                     <port>9000</port>
  39.                 </replica>
  40.                 <replica>
  41.                     <host>node4.example.com</host>
  42.                     <port>9000</port>
  43.                 </replica>
  44.             </shard>
  45.         </my_cluster>
  46.     </remote_servers>
  47.     <!-- Zookeeper 服务端的连接超时配置 -->
  48.     <zookeeper>
  49.         <node>
  50.             <host>zk1.example.com</host>
  51.             <port>2181</port>
  52.         </node>
  53.     </zookeeper>
  54.     <!-- 默认数据库,集群创建时可以选择 -->
  55.     <default_database>default</default_database>
  56.     <!-- 其他配置 -->
  57.     <macros>
  58.         <!-- 在这里可以定义集群宏,在外部可以使用 -->
  59.         <cluster_name>my_cluster</cluster_name>
  60.     </macros>
  61. </yandex>
复制代码


  • 当必要在分布式环境中进行数据存储时,你会利用 分布式表引擎Distributed。分布式结构中,会将数据分布在多个 节点(shards),按照指定键路由 上,每个节点会有独立的 副本(replicas)
  • 在 ClickHouse 中,Zookeeper 是用来管理集群的调和和分布式元数据的。Zookeeper 在 ClickHouse 集群中负责管理分片的副本同步(由zk来做)、分布式表的元数据以及分布式查询的调和。

  • 创建本地表
    在每个节点上,必要为 user_events 创建一个本地表
  1. CREATE TABLE user_events_local
  2. (
  3.     user_id UInt64,
  4.     event_time DateTime,
  5.     event_type String,
  6.     event_data String
  7. ) ENGINE = MergeTree()
  8. PARTITION BY toYYYYMM(event_time)
  9. ORDER BY (user_id, event_time);
复制代码

  • 创建分布式表
    在集群的每个节点上,还必要创建一个 分布式表,它会将查询转发到相应的本地表上。可以在此中一个节点上创建分布式表,就可以在各个节点上查到这个表,并通过它来访问全部节点上的本地表。利用 Distributed 引擎:
  1. CREATE TABLE user_events_distributed
  2. (
  3.     user_id UInt64,
  4.     event_time DateTime,
  5.     event_type String,
  6.     event_data String
  7. ) ENGINE = Distributed('my_cluster', 'default', 'user_events_local', user_id);
  8. ##配置文件中使用了宏(例如 cluster_name),那么可以在查询中动态地使用这些宏
  9. CREATE TABLE user_events_distributed
  10. (
  11.     user_id UInt64,
  12.     event_time DateTime,
  13.     event_type String,
  14.     event_data String
  15. ) 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 的值将数据分配到不同的节点::
  1. INSERT INTO user_events_distributed (user_id, event_time, event_type, event_data)
  2. VALUES
  3. (1, '2025-03-15 10:00:00', 'click', 'button_click'),
  4. (2, '2025-03-15 10:05:00', 'view', 'page_view');
复制代码
在 node1 上,查询本地表:
  1. SELECT * FROM user_events_local;
复制代码
效果:
  1. user_id  | event_time           | event_type | event_data
  2. ---------------------------------------------------------
  3. 1        | 2025-03-15 10:00:00  | click      | button_click
复制代码
在 node2 上,查询本地表:
  1. SELECT * FROM user_events_local;
复制代码
效果:
  1. user_id  | event_time           | event_type | event_data
  2. ---------------------------------------------------------
  3. 2        | 2025-03-15 10:05:00  | view       | page_view
复制代码
查询:
查询 分布式表 时,ClickHouse 会根据查询条件将请求分发到各个节点上的本地表。假设我们查询分布式表 user_events_distributed:
  1. SELECT * FROM user_events_distributed WHERE user_id = 1;
复制代码
ClickHouse 会将查询请求发送到 node1 上的 user_events_local 本地表,得到如下效果:
  1. user_id  | event_time           | event_type | event_data
  2. ---------------------------------------------------------
  3. 1        | 2025-03-15 10:00:00  | click      | button_click
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

星球的眼睛

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表