先说结论
MergeTree 每批写入会形成一个或多个数据片段,小批量高频插入会产生大量 parts,后台合并跟不上后会增加元数据、文件和查询成本,甚至拒绝写入。应由客户端批量写入,或使用异步写入和缓冲层聚合小请求。
批次不是越大越好,过大会增加内存、单次重试成本和写入延迟,需要按行数、字节数和时间窗口共同控制。
优化方向
使用高效格式和压缩,尽量按排序键局部有序写入,减少无效列与复杂默认表达式。控制活跃分区数量,避免同一批次同时打散到大量分区并创建更多 parts。
小 part 为什么会拖垮系统
每个 part 包含列文件、mark、元数据和索引。若每秒几十次小 INSERT,每次几百行,后台必须不断把小 part 合并成大 part;合并跟不上时 part 数增加,查询要打开更多文件、启动更多读取任务,副本复制与恢复也更慢,最终可能触发 parts 限制拒绝写入。
小批 INSERT -> part 数增长 -> merge 队列积压
-> 磁盘/CPU 竞争 -> 查询变慢 -> 复制滞后/写入拒绝
根因通常在上游写入模式或分区过细,而不是 Merge 线程“数量不够”。
批量参数如何选择
按行数、压缩后字节、最大等待时间三者之一触发 flush。例如每批 1 万至数十万行、或数 MB 至数十 MB、或等待不超过几秒,实际阈值取决于行宽、写入峰值、延迟 SLO 和内存。单批极大可能导致客户端 OOM、重试成本大、单次写入尾延迟高;单批过小又会产生 parts。
批处理器要有有界内存和背压:上游速度超过写入能力时,选择阻塞、限流、丢弃低价值数据、持久化队列还是降采样,必须由业务决定,不能让内存无限积压。
写入格式与排序
Native、RowBinary、Parquet、JSONEachRow 等格式在解析成本、调试友好性和生态兼容上不同。高吞吐内部链路选结构化二进制或批量格式,调试/外部接入可用 JSON,但要避免每行重复解析复杂字符串。预先按 ORDER BY 局部排序可改善压缩和后续 merge 效率,但不要为了全局排序在应用端制造巨大内存和延迟。
异步写入的可靠性
异步写入把小请求在服务端或客户端聚合,降低 parts,但确认时机可能从“已落本地 part”变成“已进入缓冲”。要明确缓冲崩溃、网络超时、重试时是否可能重复或丢失;关键数据应有上游可重放日志、唯一事件 ID 和落盘确认策略。
Kafka + 消费写入 ClickHouse 常见,但消费者应根据写入成功后提交位点、批次失败可重试、重复数据可识别。不要把消息队列当作自动去重器。
线上排查
发现 Too many parts、写入 P99 上升或 merge backlog 时,检查每表/分区 part 数、平均批大小、活跃分区数、磁盘吞吐、后台任务、TTL/mutation 和副本队列。应先降低小批写入和分区打散,再评估资源或配置;频繁手工 OPTIMIZE 往往只短暂缓解。
稳定性设计
客户端设置有界队列、退避重试和幂等标识,监控 parts 数、Merge 队列、写入延迟、拒绝与磁盘空间。Kafka 等缓冲层可吸收峰值,但必须处理消费重放和端到端去重。
容易踩坑的地方
只增加后台 Merge 线程可能争抢 CPU 与磁盘,拖慢查询和写入。频繁执行 OPTIMIZE 也不是治理小 part 的常规方案,根因通常在写入批次和分区设计。
常见问题
追问:异步写入会丢数据吗?
风险取决于等待确认和持久化配置;必须明确服务端在何时确认、客户端失败如何重试,并用故障演练验证所需可靠性。
追问:为什么不把批次设得越大越好?
大批次降低 part 数,却增加内存、超时、失败重试和长尾。最优点取决于吞吐、行宽和延迟目标,应压测寻找平衡。
追问:按天分区却每批包含很多天的数据有什么问题?
一次 INSERT 会在多个分区创建 part,放大 parts 和 merge 工作。可按分区先聚合或限制单批跨分区范围。
追问:复制表写入失败可以直接重试吗?
可以有限重试,但要考虑请求实际已成功却响应超时的重复风险。事件 ID、版本或最终去重策略是更可靠的保障。