ClickHouse 将数据按 粒度(granule) 组织,每个粒度包含约 8192 行数据

问题:一个granule 中的数据,是物理有序还是逻辑有序?

问题二:如果part 都是逻辑有序,那么为什么还是合并part?直接记录part的文件、索引等信息不是也可以实现逻辑有序吗?

关于 ClickHouse 的 granule 组织方式和 Part 合并机制,需要从存储结构、查询优化、写入性能三个层面深入解析:

问题一:一个 Granule 中的数据,是物理有序还是逻辑有序?

答案:既是物理有序,也是逻辑有序,但需区分不同层面的实现:

 

  1. 逻辑层面
    同一 Granule 内的数据严格按 主键(PRIMARY KEY) 排序,这是 ClickHouse 保证查询性能的基础。例如:
    CREATE TABLE users (
      user_id UInt64,
      name String,
      age UInt8
    ) ENGINE = MergeTree
    ORDER BY user_id;  -- 主键决定排序顺序
    插入数据时,ClickHouse 会自动按 user_id 对数据进行排序,确保同一 Granule 内的记录逻辑有序。
  2. 物理层面
    在 单个数据文件(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,会导致以下问题:
  1. 查询性能崩溃
    假设表中有 1000 个小 Part,每个查询需遍历所有 Part 的索引,即使使用二分查找,时间复杂度也会从 O (log N) 恶化为 O (1000*log N),导致查询延迟显著增加。
  2. 资源消耗激增
    • 每个 Part 的索引和标记文件占用内存(约 1-5% 数据量),1000 个小 Part 会消耗大量内存。
    • 文件系统需维护更多 inode 和目录项,影响元数据操作效率。
  3. 写入性能下降
    写入时需同时维护多个 Part 的索引,锁竞争加剧,导致写入吞吐量下降。
  4. 数据一致性风险
    过多小 Part 会增加后台任务(如合并、复制)的复杂度,可能导致数据副本不一致。

四、合并策略优化实践

理解合并机制后,可通过以下策略优化性能:
  1. 调整合并参数
    -- 创建表时指定合并策略
    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;  -- 控制单次合并大小
    
     
  2. 使用 SSD 加速合并
    合并操作涉及大量随机读写,SSD 的高 IOPS 可显著提升合并效率。
  3. 分区表隔离合并范围
    -- 按月分区,减少单个分区内的合并压力
    PARTITION BY toYYYYMM(create_date)
    
     
  4. 控制写入频率
    避免高频小批量写入,通过批量写入(如单次 10 万行以上)减少小 Part 生成。

五、总结

  1. Granule 的有序性:
    • 同一 Granule 内的数据物理和逻辑均有序,不同 Granule 间通过索引保证逻辑有序。
  2. 合并的必要性:
    • 减少文件数量、提升压缩率、优化索引、清理无效数据,是 ClickHouse 高性能的关键保障。
  3. 权衡与优化:
    • 合理配置合并策略,在写入性能、存储效率、查询延迟之间找到平衡点。

 

理解这些原理后,可针对性地优化 ClickHouse 集群,充分发挥其 LSM-Tree 架构的优势。
 posted on 2025-07-28 11:06  xibuhaohao  阅读(22)  评论(0)    收藏  举报