ClickHouse TOO_MANY_PARTS 异常分析与优化实践

问题现象

线上服务触发告警 TOO_MANY_PARTS (300),提示分片合并速度远低于数据插入速度,异常持续数分钟后自动恢复,MQ重试机制保障了最终数据一致性。


技术排查

初步分析

  1. 流量突增?
    监控显示调用量平稳,排除突发流量导致分片激增的可能。
  2. 集群负载异常?
    DBA核查CPU/内存/磁盘IO指标均正常,排除硬件资源瓶颈。

插入流程深挖

[客户端]
   │ 单条INSERT操作
   ▼
[分布式表] → 哈希计算分片键 → 路由至Shard-2
   ▼
[本地表 t_product_request_log_local]
   │ 生成临时分片 → 原子化提交为正式分片
   ▼
[存储目录]
   └─ 未分区数据池
      ├─ 分片_1_1_0(单次插入产生)
      ├─ 分片_2_2_0
      └─ 分片_3_3_0(持续累积)

关键发现
• 单条插入模式:每次插入生成独立小分片
• 本地表未分区:所有数据堆积在单一逻辑分区


根因定位

分片合并机制失效

ClickHouse通过后台线程合并相邻分片(如202310_1_1_0 + 202310_2_2_0 → 202310_1_2_1),但存在以下瓶颈:

  1. 单分区数据堆积
    未使用PARTITION BY导致所有数据存入同一分区,合并操作集中处理数百个分片。
  2. 小分片洪水效应
    单条插入产生海量微分片,合并线程无法跟上插入速度,触发分片数阈值告警。

优化方案

分区策略优化

问题解决方案预期收益
数据未物理分区新增PARTITION BY toYYYYMMDD(time)数据按天切分,分片压力分散化
全量数据集中合并结合业务周期设置TTL自动清理历史数据,减少分片基数

写入模式改造

# 原写入逻辑(问题代码)
for log in log_stream:
    execute("INSERT INTO table VALUES (...)")
  
# 改造后写入逻辑
batch = []
for log in log_stream:
    batch.append(log)
    if len(batch) >= 1000:  # 批量提交阈值
        execute("INSERT INTO table VALUES (...)", batch)
        batch.clear()

批量写入优势
• 单次RPC提交多数据,减少网络开销
• 单批次生成1个分片 vs 原模式N个分片(分片数降低2个数量级)


实施效果

  1. 分片数量对比
    ![分片数监控曲线图:优化后单个分区分片峰值从300+降至20以下]
  2. 合并效率提升
    后台合并任务耗时从分钟级缩短至秒级,CPU利用率下降40%
  3. 查询性能增益
    因数据物理分区,时间范围查询速度提升5-8倍

经验总结

  1. 设计约束
    • 必须显式定义PARTITION BY(按时间/业务主键)
    • 分布式表与本地表需同步分区策略

  2. 编码规范
    • 禁止单条插入,强制批量提交(建议1000-10000条/批次)
    • 采用async_insert=1参数优化高频小批量场景

  3. 监控体系

    # 分片健康度监测
    SELECT table, partition, count() AS parts 
    FROM system.parts 
    WHERE active GROUP BY 1,2 ORDER BY parts DESC LIMIT 10
    

    配置阈值告警(单个分区分片数>50触发预警)


标题:ClickHouse TOO_MANY_PARTS 异常分析与优化实践
作者:JonLv
地址:http://39.108.183.139:8080/articles/2025/04/03/1743651336737.html