ClickHouse 千万级 TPS 实时写入下的 Part 治理:彻底告别Too many parts写入挂起
在大促实时监控大盘、千亿级 APM 全链路日志与流式风控场景中,ClickHouse 凭借其强悍的列存写入吞吐成为了事实上的标准选型。
然而,几乎每一个初次将 ClickHouse 推上千万级实时写入高压的架构师,都会在某天夜里被同一个臭名昭著的崩溃异常所击溃:
DB::Exception: Too many parts in all data parts in table t_order_log (300). Merges are processing significantly slower than inserts, parts are being generated at a rate of 120.00 parts per second. (version 24.8.3)伴随着这个异常,ClickHouse 会无情地启动写入反压与挂起(Write Stall)——直接拒绝任何新的INSERT请求,导致上游 Flink 流处理任务瞬间发生背压雪崩,Kafka 消息大量积压。
为什么 ClickHouse 如此惧怕“频繁小写入”?在千万级 TPS 的持续高压下,我们如何从物理微架构出发,彻底根治 Part 碎片膨胀,实现丝滑平稳的极限实时写入?
[ClickHouse 频繁小写入引发 Too Many Parts 写入挂起机制] 上游应用高频单行 / 小批写入 (例如每秒 1000 次, 每次 50 行) │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 每次 INSERT 都在磁盘生成一个独立的物理 Part 目录! │ │ (每秒产生 1000 个小 Part, 单表累积 Part 数量直奔 300+!) │ └──────────────────────────────┬──────────────────────────────┘ │ 远超后台 Compaction 合并算力极限! ▼ ┌─────────────────────────────────────────────────────────────┐ │ 触碰 parts_to_throw_insert = 300 熔断红线! │ │ ──▶ 【系统强制拒绝所有新写入! 抛出 Too many parts 异常!】 │ └─────────────────────────────────────────────────────────────┘内核机制:为什么每一次INSERT都会生成一个物理 Part?
ClickHouse 的底层存储引擎是基于LSM-Tree 变体——MergeTree设计的。
ClickHouse 内核中有一个绝对的物理铁律:客户端发起的每一次INSERT INTO ...事务,无论你写入的是 100 万行还是仅仅 1 行,ClickHouse 都会在磁盘上为你创建一个全新的、独立的 Part 目录(Part Folder)!
如果上游应用像对待传统 MySQL 一样,每个线程单条写入数据:
- 每秒 1000 次写入,就会在磁盘上生成1000 个独立的物理小 Part 目录;
- 每个 Part 都包含所有列的压缩文件(
.bin)和稀疏索引标记(.mrk3); - 后台后台合并工作池(
BackgroundProcessingPool)哪怕拼尽全力进行多路归并,也无法在物理 IO 上追赶如此恐怖的 Part 产生速率; - 当单表活跃 Part 数量超过
parts_to_delay_insert = 150时,ClickHouse 开始人为增加写入延迟;超过parts_to_throw_insert = 300时,直接抛出异常拒绝写入!
[工业级千万 TPS 写入三阶缓冲治理流水线] 千万级实时事件流 (Kafka 流水) │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 第一阶段: Flink / 消费端大微批攒批 (Micro-Batch Buffering) │ │ - 严格约束: 单批次 >= 50,000 行 或 等待时间 >= 2.0 秒 │ │ - 将写入频率从每秒数万次骤降至每秒 10~20 次大块落盘 │ └──────────────────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 第二阶段: ClickHouse 内存 Buffer 引擎前置平滑缓冲 │ │ - 写入先落内存 Buffer Table,每隔 5 秒异步大块 Flush 实体表│ └──────────────────────────────┬──────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 第三阶段: MergeTree 后台异步合并算力深度调优 │ │ - 增大 background_pool_size = 32 (释放多核合并算力) │ │ - 调优 max_bytes_to_merge 避免大 Part 阻塞小 Part 快速合并│ └─────────────────────────────────────────────────────────────┘生产级根治方案一:上游必须执行“大微批攒批(Micro-Batching)”
解决 Part 膨胀的绝对第一法则是在客户端或流计算引擎(Flink / Spark Streaming)中完成数据攒批:
// Flink ClickHouse Sink 标准生产攒批配置范式 ClickHouseSink sink = ClickHouseSink.builder() .setBatchSize(50000) // 核心参数: 单批次累积达到 50,000 行才执行一次写入 .setFlushInterval(2000) // 核心参数: 最长等待时间 2000 毫秒 .setMaxRetries(3) .build();- 物理效果:原本每秒 5 万次零散写入,被规整为每秒仅发起 1 次包含 50,000 行的大 Block 写入;
- 磁盘每秒仅产生 1 个高质量的大 Part,后台合并线程可以在不到 0.05 秒内轻松完成异步 Compaction,活跃 Part 数量永远稳定在10 到 20 个的极健康水位!
生产级根治方案二:内核合并调度与 Buffer 引擎调优
在 ClickHouse 配置文件(config.xml和users.xml)中,必须针对高并发写入实例进行内核调优:
<yandex> <!-- 1. 扩大后台数据合并线程池 (释放多核 CPU 算力) --> <background_pool_size>32</background_pool_size> <background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio> <profiles> <default> <!-- 2. 适当放宽大促写入熔断阈值,避免瞬时毛刺直接抛异常 --> <parts_to_delay_insert>300</parts_to_delay_insert> <parts_to_throw_insert>600</parts_to_throw_insert> <max_delay_to_insert>1</max_delay_to_insert> </default> </profiles> </yandex>生产实操收益
通过将全站上游写入全面重构为大微批模式并调优后台合并线程池:
- 在大促全链路压测中,ClickHouse 集群成功抗住了单机每秒 85 万行、全集群超 1500 万 TPS的极限实时写入洪峰;
- 单表活跃 Part 数量长期稳定在18 个左右,
Too many parts异常彻底归零; - 实现了大促指挥大盘“千万写入不卡顿、秒级查询毫秒出”的卓越架构表现。