ClickHouse 将数据按 粒度(granule) 组织,每个粒度包含约 8192 行数据
问题:一个granule 中的数据,是物理有序还是逻辑有序?
问题二:如果part 都是逻辑有序,那么为什么还是合并part?直接记录part的文件、索引等信息不是也可以实现逻辑有序吗?
关于 ClickHouse 的 granule
组织方式和 Part 合并机制,需要从存储结构、查询优化、写入性能三个层面深入解析:
答案:既是物理有序,也是逻辑有序,但需区分不同层面的实现:
-
逻辑层面
同一 Granule 内的数据严格按 主键(PRIMARY KEY) 排序,这是 ClickHouse 保证查询性能的基础。例如:
CREATE TABLE users (
user_id UInt64,
name String,
age UInt8
) ENGINE = MergeTree
ORDER BY user_id;
插入数据时,ClickHouse 会自动按 user_id
对数据进行排序,确保同一 Granule 内的记录逻辑有序。
-
物理层面
在 单个数据文件(data.bin
)内部,同一 Granule 的数据按逻辑顺序 连续存储(即物理有序)。这是因为 ClickHouse 在写入时,会将排序后的数据顺序写入磁盘。例如:
# data.bin 文件内部结构(简化)
┌─────────────────────────────────────┐
│ Granule 0 │
│ ├─ Row 0: user_id=1, name="Alice" │
│ ├─ Row 1: user_id=2, name="Bob" │
│ └─ ... │
├─────────────────────────────────────┤
│ Granule 1 │
│ ├─ Row 8192: user_id=8193, ... │
│ └─ ... │
└─────────────────────────────────────┘
关键点:
- 同一 Granule 内的数据在物理上连续存储,确保顺序读取效率。
- 不同 Granule 之间可能因文件系统分配策略而不连续,但通过索引(
primary.idx
和 marks.mrk2
)实现逻辑有序访问。
答案:合并 Part 的核心目的是 优化存储效率、提升查询性能、减少维护开销,即使 Part 本身逻辑有序,合并仍不可或缺:
- 问题:频繁写入会生成大量小 Part(例如每秒生成 10 个 Part),导致文件系统 inode 耗尽、元数据查询变慢。
- 合并效果:将 100 个小 Part 合并为 1 个大 Part,减少文件数量,提升文件系统操作效率。
- 原理:ClickHouse 按列存储并压缩数据,相邻行相似性越高,压缩率越好。合并前的不同 Part 可能来自不同时间窗口,数据相似性低;合并后的数据按主键重新排序,相邻行相关性增强,压缩率显著提升(通常可节省 30-50% 存储空间)。
- 问题:每个 Part 都有独立的主键索引和标记文件,查询时需遍历多个索引。例如:查询
WHERE user_id = 1000
可能需扫描 10 个 Part 的索引。
- 合并效果:合并后仅需扫描一个索引,减少随机读次数。例如:
# 合并前(多个小 Part)
┌───────────┐ ┌───────────┐ ┌───────────┐
│ Part 1 │ │ Part 2 │ │ Part 3 │
│ idx:1-100 │ │ idx:101-200 │ │ idx:201-300 │
└───────────┘ └───────────┘ └───────────┘
# 合并后(一个大 Part)
┌───────────────────┐
│ Part 1+2+3 │
│ idx:1-300 │
└───────────────────┘
- 删除 / 更新机制:ClickHouse 的 DELETE/UPDATE 是通过标记(Tombstone)实现的,真正的物理删除发生在合并时。合并过程中,被标记的数据会被丢弃,释放磁盘空间。
- TTL 机制:通过
TTL date_column + INTERVAL 30 DAY
定义数据过期规则,合并时自动删除过期数据。
答案:若不合并 Part,会导致以下问题:
-
查询性能崩溃
假设表中有 1000 个小 Part,每个查询需遍历所有 Part 的索引,即使使用二分查找,时间复杂度也会从 O (log N) 恶化为 O (1000*log N),导致查询延迟显著增加。
-
资源消耗激增
- 每个 Part 的索引和标记文件占用内存(约 1-5% 数据量),1000 个小 Part 会消耗大量内存。
- 文件系统需维护更多 inode 和目录项,影响元数据操作效率。
-
写入性能下降
写入时需同时维护多个 Part 的索引,锁竞争加剧,导致写入吞吐量下降。
-
数据一致性风险
过多小 Part 会增加后台任务(如合并、复制)的复杂度,可能导致数据副本不一致。
理解合并机制后,可通过以下策略优化性能:
-
调整合并参数
-
使用 SSD 加速合并
合并操作涉及大量随机读写,SSD 的高 IOPS 可显著提升合并效率。
-
分区表隔离合并范围
-
控制写入频率
避免高频小批量写入,通过批量写入(如单次 10 万行以上)减少小 Part 生成。
-
Granule 的有序性:
- 同一 Granule 内的数据物理和逻辑均有序,不同 Granule 间通过索引保证逻辑有序。
-
合并的必要性:
- 减少文件数量、提升压缩率、优化索引、清理无效数据,是 ClickHouse 高性能的关键保障。
-
权衡与优化:
- 合理配置合并策略,在写入性能、存储效率、查询延迟之间找到平衡点。
理解这些原理后,可针对性地优化 ClickHouse 集群,充分发挥其 LSM-Tree 架构的优势。