背景
最近团队遇到一个具有挑战性的难题:实时数据处理系统在峰值流量下出现了显著的写入瓶颈,CPU 利用率飙升至 90% 以上,写入延迟从毫秒级直接恶化到秒级。作为一名不相信“玄学调优”的技术人,我决定深入剖析 ClickHouse 的写入机制,找出问题的根本原因。

问题分析
现象复述
- 峰值写入 QPS 达到 5 万时,ClickHouse 集群响应明显变慢
- 部分写入操作超时,数据丢失风险开始显现
- 节点 CPU 使用率持续高位运行,内存使用情况则保持正常
初步诊断
首先查询 ClickHouse 的系统表,重点关注 system.metrics 和 system.events:
SELECT * FROM system.metrics WHERE metric LIKE '%Write%' OR metric LIKE '%Insert%';SELECT * FROM system.events WHERE event LIKE '%Write%' OR event LIKE '%Insert%' ORDER BY value DESC LIMIT 20;
分析后发现几个关键指标存在异常:
WriteBufferFromFileDescriptorWriteBytes增长速度快得不正常InsertedRows与InsertedBytes的比例与预期不符MergeTreeDataWriter相关指标波动幅度较大
源码分析
“源码之下,没有秘密。”直接翻阅 ClickHouse 的写入相关源码,重点考察 MergeTreeDataWriter 和 WriteBufferFromFile 这两部分。
在 MergeTreeDataWriter.cpp 中,发现一个关键问题:当并发写入量增大时,内存中的写缓冲区(WriteBuffer)会频繁触发刷盘操作。每次刷盘都需要获取表级锁,导致其他写入操作被阻塞等待。
// 简化后的关键代码逻辑void MergeTreeDataWriter::writeTempPart(...) { // 获取表级锁 auto lock = table->lockForShare(); // 写入数据到临时分区 // ... // 刷盘操作 writer->flush(); // 释放锁}
优化方案
根据源码分析的结果,制定以下优化方案:
1. 调整写入缓冲区大小
1048576 10000 10485760
2. 启用并行写入
4 4
3. 优化分区策略
根据业务特点,将原有的按天分区改为按小时分区,减小单个分区的数据量:
CREATE TABLE events ( event_time DateTime, user_id UInt64, event_type String, data String) ENGINE = MergeTree()PARTITION BY toHour(event_time)ORDER BY (event_time, user_id);
压测验证
“用基准测试说话。”搭建一个压测环境,使用 clickhouse-client 进行并发写入测试:
# 压测命令for i in {1..100}; do clickhouse-client --query "INSERT INTO events VALUES (now(), $i, 'test', 'data')" &done
测试结果对比
| 指标 | 优化前 | 优化后 | 提升比例 |
|---|---|---|---|
| 峰值 QPS | 5 万 | 15 万 | 200% |
| 平均写入延迟 | 800ms | 120ms | 85% |
| CPU 使用率 | 90%+ | 60% | 33% |
| 内存使用 | 4GB | 4.2GB | -5% |
生产部署
测试环境验证通过后,生产环境采用灰度发布。部署策略也较为稳妥:
- 先在一个节点上应用配置
- 观察 24 小时,确认无异常
- 逐步推广到整个集群
经验总结
- 写入缓冲区调整:根据数据特征和硬件规格,找到最合适的缓冲区大小
- 并行度优化:合理设置并行写入线程数,充分利用多核 CPU 性能
- 分区策略:依据数据量和查询模式,选择正确的分区粒度
- 监控体系:建立完善的监控机制,才能第一时间发现性能瓶颈
