4. 写入链路优化

在时序场景中,写入链路往往比查询链路更容易成为瓶颈。ClickHouse 的 MergeTree 家族以“每次写入生成一个 part、后台异步合并”为核心机制,这意味着写入吞吐并不取决于单条 INSERT 的速度,而取决于每次写入形成的 part 数量、大小与合并压力。本节围绕批量写入、异步插入、Buffer 表、Kafka 引擎接入与吞吐调优五个抓手,梳理一条可落地的写入优化路径。

一、核心原理:为什么“写得快”不等于“写得多”

ClickHouse 的写入本质是“追加 + 排序 + 落盘”。每次 INSERT 都会在分区目录下生成一个新的 part,后台线程再按规则合并小 part。若客户端逐条写入,就会产生海量微小 part,触发 Too many parts 报错,同时合并线程被拖垮,磁盘 IO 与 CPU 被大量消耗在合并而非有效写入上。因此写入优化的第一性原理是:在客户端或服务端把数据攒大,减少 part 数量,让每次写入尽量接近一个“理想 part”。

理想 part 的经验区间:单 part 压缩前 100MB~1GB,行数 10 万~100 万;单表活跃 part 数控制在几百以内,避免频繁触发 merge 限流。

二、方法流程:五级写入优化手段

从客户端到服务端,写入优化可以按“攒批—缓冲—异步—接入—调参”的顺序逐级推进:

  1. 批量写入:客户端按行数或字节数攒批,使用单条 INSERT 携带多行 VALUES,或使用 Native/CSV 格式批量导入。
  2. 异步插入:开启 async_insert=1,由服务端在内存中聚合小批量写入,再统一落盘,适合高并发小包场景。
  3. Buffer 表:在目标表前挂 Buffer 引擎表,内存中缓冲写入,按阈值刷入底层表,适合写入抖动明显的场景。
  4. Kafka 引擎接入:用 Kafka 表引擎消费消息,配合物化视图写入目标表,实现削峰填谷与解耦。
  5. 吞吐调优:调整 max_insert_block_size、min_insert_block_size_rows、parts_to_delay_insert 等参数,并监控合并队列。
写入链路五级优化:从客户端到 MergeTree 客户端攒批 行数/字节阈值 异步插入 async_insert=1 Buffer 表 内存缓冲刷盘 Kafka 引擎 消费+物化视图 参数调优 合并队列监控 MergeTree 目标表:每次写入生成 part → 后台合并 part 数量与大小决定写入吞吐上限 小 part 过多 Too many parts 理想 part 100MB~1GB 合并压力 IO / CPU 消耗

三、关键手段与配置示例

批量写入是最基础也最有效的手段。客户端应避免逐条 INSERT,改为按 1 万~10 万行或 10MB~100MB 攒批。使用 clickhouse-client 时可用 --input_format_parallel_parsing 提升解析速度:

clickhouse-client --query "INSERT INTO metrics FORMAT CSV" \
  --input_format_parallel_parsing=1 < metrics.csv

异步插入适合无法在客户端攒批的高并发小包场景。开启后服务端会按 async_insert_max_data_size 与 async_insert_busy_timeout_ms 聚合:

SET async_insert = 1;
SET wait_for_async_insert = 1;
SET async_insert_max_data_size = 10485760;
SET async_insert_busy_timeout_ms = 1000;

Buffer 表在内存中缓冲数据,按行数、字节数或时间阈值刷入目标表,适合写入速率波动大的采集场景:

CREATE TABLE metrics_buffer AS metrics
ENGINE = Buffer(currentDatabase(), metrics, 16,
  10, 60, 10000, 1000000, 10485760, 10000000);

Kafka 引擎将消息队列作为写入入口,配合物化视图把数据落到 MergeTree,实现削峰与解耦:

CREATE TABLE kafka_metrics (
  ts DateTime, name String, value Float64
) ENGINE = Kafka
SETTINGS kafka_broker_list = 'kafka:9092',
         kafka_topic_list = 'metrics',
         kafka_group_name = 'ch_metrics',
         kafka_format = 'JSONEachRow';

CREATE MATERIALIZED VIEW mv_metrics TO metrics AS
SELECT ts, name, value FROM kafka_metrics;

四、场景案例:从 2 万行/秒到 80 万行/秒

某监控平台初期使用逐条 INSERT 写入,QPS 约 2 万行/秒,频繁出现 Too many parts。优化路径如下:

阶段手段关键参数吞吐
初始逐条 INSERT无2 万行/秒
第一步客户端攒批 5 万行batch=5000015 万行/秒
第二步开启异步插入async_insert=135 万行/秒
第三步Kafka 接入 + 物化视图kafka_group_name60 万行/秒
第四步调优合并参数max_insert_block_size80 万行/秒

调优阶段重点调整以下参数,并观察 system.merges 与 system.parts:

SET max_insert_block_size = 1048576;
SET min_insert_block_size_rows = 1048576;
SET min_insert_block_size_bytes = 268435456;
SET parts_to_delay_insert = 300;
SET parts_to_throw_insert = 600;

五、误区与复盘

误区一:异步插入一定更快。异步插入会引入延迟,且 wait_for_async_insert=0 时可能丢数据,需根据业务容忍度选择。

误区二:Buffer 表可以无限缓冲。Buffer 表数据在内存中,节点重启会丢失,且刷盘阈值设置不当会造成底层表 part 突增。

误区三:Kafka 引擎直接查询即可。Kafka 表引擎不支持重复消费,必须配合物化视图落盘,否则数据只读一次。

复盘要点:写入优化不是单点调参,而是“客户端攒批 + 服务端缓冲 + 队列削峰 + 合并监控”的组合拳。上线前用 system.parts 观察 part 数量与大小分布,用 system.merges 观察合并队列长度,用 system.asynchronous_insert_log 观察异步插入聚合效果,才能把吞吐稳定在目标水位。

综上,写入链路优化的核心是控制 part 的生成节奏与规模。批量写入解决“攒”的问题,异步插入与 Buffer 表解决“缓”的问题,Kafka 引擎解决“削峰”的问题,参数调优解决“稳”的问题。四者协同,才能让 ClickHouse 在时序高吞吐写入场景下保持稳定与高效。