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