【Clickhouse从入门到精通】第56篇:ClickHouse运维常见问题与故障排查指南
上一篇【第55篇】ClickHouse性能调优实战:从SQL到系统层
下一篇【第57篇】ClickHouse与Kafka实时数据管道实战
摘要
本文是《ClickHouse原理解析与应用实践》系列博客的第56篇文章。ClickHouse在生产环境中运行时,会遇到各种各样的运维问题,包括ZooKeeper连接异常、Too many parts错误、内存溢出、Merge故障、副本同步延迟以及磁盘空间告警等。本文从故障排查方法论出发,系统梳理各类常见问题的现象、根因和解决方案,提供常见错误码速查表,并通过3个真实生产故障案例展示完整的排查与修复过程,帮助运维工程师快速定位并解决ClickHouse集群中的疑难问题。
关键词:故障排查、ZooKeeper异常、Too many parts、OOM内存溢出、副本同步、Broken Part
1. 故障排查方法论
在面对ClickHouse集群告警时,很多工程师的第一反应是直接去翻日志,或者凭经验猜测原因。这种方式往往效率低下,甚至误判。一套系统化的排查方法论能够显著提升故障定位效率。
1.1 三层排查模型
ClickHouse故障排查遵循"日志 → 系统表 → 监控指标"的由内到外的排查顺序:
第一层:日志(clickhouse-server.log / error.log)
↓ 确认错误类型和时间点
第二层:系统表(system.query_log / system.replicas / system.parts 等)
↓ 关联查询历史、副本状态、分区状态
第三层:监控指标(Prometheus + Grafana / system.metrics / system.events)
↓ 分析资源使用趋势、性能瓶颈
1.2 日志文件位置与级别
ClickHouse默认日志路径为 /var/log/clickhouse-server/,主要包含:
| 日志文件 | 说明 |
|---|---|
clickhouse-server.log |
主日志,记录INFO及以上级别事件 |
clickhouse-server.err.log |
仅记录ERROR级别及以上的错误 |
clickhouse-server-text.log |
文本格式的详细日志(部分版本) |
调整日志级别(临时):
-- 临时调整日志级别为 trace(慎用,日志量极大)
SET send_logs_level = 'trace';
-- 查看当前服务器日志级别
SELECT value FROM system.server_settings WHERE name = 'logger.level';
1.3 常用系统表速查
| 系统表 | 用途 |
|---|---|
system.query_log |
查询历史、执行时间、内存使用 |
system.processes |
当前正在执行的查询 |
system.replicas |
副本状态、同步队列大小 |
system.parts |
数据分片信息、大小、行数 |
system.merges |
当前正在进行的Merge任务 |
system.replication_queue |
副本复制队列详情 |
system.errors |
历史错误计数 |
system.metrics |
实时指标(连接数、并发查询等) |
system.events |
累计事件计数 |
system.disks |
磁盘使用情况 |
-- 查看当前正在执行的查询
SELECT query_id, user, elapsed, memory_usage, query
FROM system.processes
ORDER BY elapsed DESC;
-- 查看最近1小时内执行最慢的Top10查询
SELECT
query_id,
query_duration_ms,
memory_usage,
read_rows,
query
FROM system.query_log
WHERE event_time >= now() - INTERVAL 1 HOUR
AND type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 10;
2. ZooKeeper相关故障
ClickHouse的ReplicatedMergeTree依赖ZooKeeper进行元数据协调,ZooKeeper的任何异常都会直接影响副本表的可用性。
2.1 ZooKeeper连接超时
现象:错误日志中出现类似以下信息:
DB::Exception: Cannot get children of /clickhouse/tables/01/events/replicas:
ZOPERATIONTIMEOUT (org.apache.zookeeper.KeeperException$OperationTimeoutException)
根因分析:
- ZooKeeper集群负载过高(session过多、watch数量过大)
- 网络抖动导致TCP连接超时
- ZooKeeper JVM GC停顿(Full GC时间过长)
- ZooKeeper节点数据量过大(超过推荐的100万节点)
处理步骤:
# 1. 检查ZooKeeper服务状态
echo "stat" | nc zookeeper-host 2181
# 2. 查看ZooKeeper连接数
echo "mntr" | nc zookeeper-host 2181 | grep connections
# 3. 查看ClickHouse中ZooKeeper连接状态
# 在ClickHouse中执行:
SELECT * FROM system.zookeeper WHERE path = '/';
-- 查看ZooKeeper会话信息
SELECT * FROM system.zookeeper_connection;
-- 检查ZooKeeper路径下节点数量
SELECT count() FROM system.zookeeper
WHERE path = '/clickhouse/tables/01/events/replicas';
解决方案:
- 在
config.xml中调整ZooKeeper超时参数:
<zookeeper>
<node>
<host>zk1.example.com</host>
<port>2181</port>
</node>
<session_timeout_ms>30000</session_timeout_ms>
<operation_timeout_ms>10000</operation_timeout_ms>
<!-- 增加连接重试次数 -->
<connection_timeout_ms>10000</connection_timeout_ms>
</zookeeper>
- ZooKeeper集群层面优化:
# 增大ZooKeeper JVM堆内存(在zkEnv.sh中)
export ZK_SERVER_HEAP=4096 # 4GB
# 调整tickTime和initLimit
# zoo.cfg
tickTime=2000
initLimit=20
syncLimit=10
maxClientCnxns=300
2.2 ZooKeeper数据不一致(replica diverged)
现象:查询副本状态时出现 is_readonly=1 或 Replica数据不一致:
SELECT
database, table, replica_name,
is_leader, is_readonly,
absolute_delay, queue_size
FROM system.replicas
WHERE is_readonly = 1 OR absolute_delay > 300;
处理步骤(DETACH + 重新ATTACH修复):
-- 步骤1:确认哪个副本出现问题
SELECT database, table, replica_name, last_exception
FROM system.replicas
WHERE last_exception != '';
-- 步骤2:在问题节点上,先DETACH该表
DETACH TABLE db_name.table_name;
-- 步骤3:删除ZooKeeper中的副本节点(谨慎操作!)
-- 在问题节点上执行,会清理本地副本在ZK中的注册信息
SYSTEM DROP REPLICA 'replica_name' FROM TABLE db_name.table_name;
-- 步骤4:重新ATTACH表,ClickHouse会自动从其他副本同步数据
ATTACH TABLE db_name.table_name;
-- 步骤5:手动触发同步
SYSTEM SYNC REPLICA db_name.table_name;
-- 步骤6:验证同步状态
SELECT
replica_name,
is_leader,
absolute_delay,
queue_size,
last_exception
FROM system.replicas
WHERE database = 'db_name' AND table = 'table_name';
2.3 ZooKeeper节点数据过多导致GC
现象:ZooKeeper频繁Full GC,ClickHouse日志中出现大量超时。
根因:ClickHouse会在ZooKeeper中为每次INSERT、每个Part的操作记录日志,长期运行后积累大量过期的ZooKeeper节点(特别是/clickhouse/tables/.../log/路径下的日志节点)。
清理过期数据:
-- 查看每张表在ZooKeeper中的日志节点数量
SELECT
database,
table,
zookeeper_path
FROM system.replicas;
-- 手动触发ZooKeeper日志清理(ClickHouse会自动清理,也可手动触发)
SYSTEM CLEANUP db_name.table_name;
# 使用zkcli查看并清理过期节点
# 列出日志节点数量
zkCli.sh -server zk1:2181 ls /clickhouse/tables/shard1/events/log | wc -w
# ClickHouse config.xml 中设置自动清理
# <zookeeper_log_max_size>4096</zookeeper_log_max_size>
3. Too many parts问题
3.1 现象与错误
Code: 252. DB::Exception: Too many parts (600). Merges are processing
significantly slower than inserts (merges: 1, inserts: 30).
这是ClickHouse生产环境中最常见的问题之一,当分区内的Part数量超过阈值(默认300)时写入被阻塞,超过临界值(默认500)时抛出异常。
3.2 根因分析
MergeTree的写入流程:每次INSERT都会在对应分区下创建一个新的Part(数据目录),后台Merge线程异步将多个小Part合并成大Part。
INSERT → 创建新Part → 后台Merge
↑ ↓
└──── 若INSERT频率 >> Merge速度 ──→ Part数量积累 → Too many parts
监控当前分区Part数量:
-- 查看Part数量最多的分区Top20
SELECT
database,
table,
partition,
count() AS parts_count,
sum(rows) AS total_rows,
formatReadableSize(sum(bytes_on_disk)) AS disk_size
FROM system.parts
WHERE active = 1
GROUP BY database, table, partition
ORDER BY parts_count DESC
LIMIT 20;
-- 查看当前Merge进度
SELECT
database, table,
elapsed,
progress,
num_parts,
result_part_name
FROM system.merges
ORDER BY elapsed DESC;
3.3 解决方案
方案一:增大后台Merge线程数
<!-- config.xml 或 users.xml -->
<merge_tree>
<!-- 增大后台Merge并发数,默认16 -->
<background_pool_size>32</background_pool_size>
<!-- 后台Merge线程数 -->
<background_merges_mutations_concurrency_ratio>2</background_merges_mutations_concurrency_ratio>
</merge_tree>
动态调整(无需重启):
-- 动态增大后台线程池
SYSTEM START MERGES;
-- 查看当前后台任务状态
SELECT * FROM system.metrics
WHERE metric LIKE '%BackgroundPool%' OR metric LIKE '%Merge%';
方案二:降低INSERT频率(批量写入)
-- 反模式:每条记录单独INSERT(会迅速产生大量Part)
-- INSERT INTO events VALUES (1, now(), 'click');
-- INSERT INTO events VALUES (2, now(), 'view');
-- 正确模式:批量INSERT,建议每批1万~100万行
INSERT INTO events VALUES
(1, now(), 'click'),
(2, now(), 'view'),
(3, now(), 'purchase'),
-- ... 批量数据
;
-- 推荐的最小批量间隔:至少1秒1次,建议5~10秒1次
方案三:使用Buffer引擎缓冲小批量写入
-- 创建Buffer表,在内存中积累数据后批量写入目标表
CREATE TABLE events_buffer AS events
ENGINE = Buffer(
currentDatabase(), -- 目标数据库
'events', -- 目标表
16, -- 分片数
10, -- 最小刷新间隔(秒)
100, -- 最大刷新间隔(秒)
10000, -- 最小刷新行数
1000000, -- 最大刷新行数
10000000, -- 最小刷新字节数
100000000 -- 最大刷新字节数
);
-- 所有写入都指向Buffer表
INSERT INTO events_buffer VALUES (...);
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 增大Merge线程 | 临时缓解、资源充足 | 立即见效 | 消耗更多CPU/IO |
| 批量写入 | 新业务设计 | 根本解决 | 需修改写入逻辑 |
| Buffer引擎 | 无法控制写入端 | 透明缓冲 | 内存消耗、宕机风险 |
| 降低写入频率 | 可控的写入场景 | 成本低 | 实时性降低 |
4. OOM(内存溢出)问题
4.1 定位OOM查询
-- 查找内存使用最高的历史查询
SELECT
query_id,
user,
event_time,
query_duration_ms,
formatReadableSize(memory_usage) AS memory,
read_rows,
substr(query, 1, 200) AS query_preview
FROM system.query_log
WHERE event_time >= now() - INTERVAL 1 DAY
AND type IN ('QueryFinish', 'ExceptionWhileProcessing')
ORDER BY memory_usage DESC
LIMIT 20;
-- 查看当前正在执行的查询内存占用
SELECT
query_id,
user,
elapsed,
formatReadableSize(memory_usage) AS memory,
query
FROM system.processes
ORDER BY memory_usage DESC;
4.2 设置内存限制
-- 用户级别:在users.xml中为用户设置内存上限
-- <max_memory_usage>10000000000</max_memory_usage> -- 10GB
-- 查询级别:单次查询最大内存限制
SET max_memory_usage = 5368709120; -- 5GB
-- 服务器全局级别:在config.xml中设置
-- <max_server_memory_usage_to_ram_ratio>0.8</max_server_memory_usage_to_ram_ratio>
4.3 常见OOM场景与优化
场景一:大GROUP BY导致OOM
-- 问题查询:对高基数列进行GROUP BY
-- SELECT user_id, count() FROM events GROUP BY user_id;
-- 优化方案1:使用近似聚合
SELECT
uniqApprox(user_id) AS approx_users -- 近似去重,内存友好
FROM events;
-- 优化方案2:启用GROUP BY溢出到磁盘
SET max_bytes_before_external_group_by = 10000000000; -- 10GB
SET group_by_two_level_threshold = 100000;
-- 优化方案3:分批查询
SELECT user_id, count()
FROM events
WHERE toYYYYMMDD(event_time) = '20260519' -- 按日期分批
GROUP BY user_id;
场景二:大JOIN导致OOM
-- 优化:将大表放在左边,小表放在右边(右表会构建哈希表)
SELECT e.user_id, u.username, count()
FROM events e -- 大表在左
INNER JOIN (
SELECT user_id, username FROM users WHERE is_active = 1
) u ON e.user_id = u.user_id -- 小表构建哈希表
GROUP BY e.user_id, u.username;
-- 或者使用IN替代JOIN(对小表更高效)
SELECT user_id, count()
FROM events
WHERE user_id IN (SELECT user_id FROM users WHERE is_active = 1)
GROUP BY user_id;
场景三:ORDER BY大结果集导致OOM
-- 问题:对全表排序
-- SELECT * FROM events ORDER BY event_time DESC;
-- 优化:使用LIMIT限制结果集
SELECT * FROM events ORDER BY event_time DESC LIMIT 1000;
-- 或者启用外部排序
SET max_bytes_before_external_sort = 10000000000; -- 允许溢出到磁盘排序
5. Merge异常处理
5.1 Merge过慢
监控Merge状态:
-- 查看当前Merge任务及进度
SELECT
database,
table,
partition_id,
elapsed,
progress,
num_parts,
formatReadableSize(total_size_bytes_compressed) AS total_size,
result_part_name
FROM system.merges
ORDER BY elapsed DESC;
-- 查看历史Merge速度(通过system.events)
SELECT
event,
value
FROM system.events
WHERE event LIKE '%Merge%';
加速Merge:
-- 手动触发强制Merge(将指定分区合并为1个Part)
OPTIMIZE TABLE db_name.events PARTITION '202605' FINAL;
-- 注意:FINAL会强制合并,即使只有1个Part也会执行,代价较高
-- 生产环境谨慎使用,建议在业务低峰期执行
5.2 Broken Part处理
现象:日志中出现 Checksum doesn't match 或查询时报错 Part is broken。
-- 查找损坏的Part
SELECT
database,
table,
name,
is_broken,
remove_time,
refcount
FROM system.parts
WHERE is_broken = 1;
-- 检查Part完整性(会触发checksum验证)
CHECK TABLE db_name.events;
修复步骤:
-- 方案1:DETACH损坏的分区(移出活跃数据目录,但不删除)
ALTER TABLE db_name.events DETACH PARTITION '202605';
-- 验证其他副本数据完整性
SELECT count() FROM db_name.events WHERE toYYYYMM(event_time) = 202605;
-- 方案2:如果确认数据已在其他副本,可以直接DROP损坏的分区
ALTER TABLE db_name.events DROP PARTITION '202605';
-- ClickHouse副本机制会自动从其他副本重新同步该分区
-- 方案3:对单个损坏Part进行DROP(更精细的操作)
-- 先找到损坏Part所在目录
-- 然后在文件系统层将其移动到 detached/ 目录
-- ClickHouse会自动感知并排除该Part
5.3 Large Merges阻塞Small Merges
现象:后台Merge线程被几个超大的Merge任务占满,大量小Part积压。
优化配置:
<!-- config.xml -->
<merge_tree>
<!-- 控制单次Merge的最大Part数量,避免单个Merge过大 -->
<max_parts_in_total>100000</max_parts_in_total>
<!-- 最大单个Part大小(字节),超过此大小不会被合并 -->
<max_bytes_to_merge_at_max_space_in_pool>161061273600</max_bytes_to_merge_at_max_space_in_pool>
<!-- 小Part优先Merge(低于此大小的Part会提高优先级) -->
<write_ahead_log_interval_ms>1000</write_ahead_log_interval_ms>
</merge_tree>
6. 副本同步延迟处理
6.1 监控副本状态
-- 全面查看副本健康状态
SELECT
database,
table,
replica_name,
is_leader,
can_become_leader,
is_readonly,
is_session_expired,
future_parts,
parts_to_check,
queue_size,
inserts_in_queue,
merges_in_queue,
absolute_delay,
total_replicas,
active_replicas,
last_exception
FROM system.replicas
ORDER BY absolute_delay DESC, queue_size DESC;
-- 查看副本队列详情(queue_size过大时使用)
SELECT
type,
source_replica,
new_part_name,
create_time,
required_quorum,
is_currently_executing
FROM system.replication_queue
WHERE database = 'db_name' AND table = 'events'
ORDER BY create_time ASC
LIMIT 50;
6.2 手动触发同步
-- 手动触发副本同步(等待直到队列清空)
SYSTEM SYNC REPLICA db_name.events;
-- 设置同步超时时间(秒)
SYSTEM SYNC REPLICA db_name.events TIMEOUT 300;
-- 如果某个副本长期落后,可以重启其同步进程
SYSTEM RESTART REPLICA db_name.events;
-- 重启所有表的副本进程
SYSTEM RESTART REPLICAS;
6.3 副本数据对齐
-- 当副本数据完全不一致时,通过ATTACH重新对齐
-- 步骤1:在问题副本上DETACH表
DETACH TABLE db_name.events;
-- 步骤2:删除本地数据目录中的Part(可选,让ClickHouse完全重新同步)
-- 在OS层面:rm -rf /var/lib/clickhouse/data/db_name/events/
-- 步骤3:重新ATTACH,ClickHouse会从Leader副本重新同步所有数据
ATTACH TABLE db_name.events;
-- 步骤4:监控同步进度
SELECT queue_size, absolute_delay
FROM system.replicas
WHERE database = 'db_name' AND table = 'events';
7. 磁盘空间问题
7.1 找出大表与大分区
-- 各表磁盘占用排行
SELECT
database,
table,
formatReadableSize(sum(bytes_on_disk)) AS disk_size,
formatReadableSize(sum(data_compressed_bytes)) AS compressed,
formatReadableSize(sum(data_uncompressed_bytes)) AS uncompressed,
round(sum(data_uncompressed_bytes) / sum(data_compressed_bytes), 2) AS ratio,
count() AS parts_count,
sum(rows) AS total_rows
FROM system.parts
WHERE active = 1
GROUP BY database, table
ORDER BY sum(bytes_on_disk) DESC
LIMIT 20;
-- 某张表中各分区磁盘占用
SELECT
partition,
formatReadableSize(sum(bytes_on_disk)) AS disk_size,
sum(rows) AS rows,
count() AS parts
FROM system.parts
WHERE active = 1 AND database = 'db_name' AND table = 'events'
GROUP BY partition
ORDER BY sum(bytes_on_disk) DESC;
-- 查看磁盘整体使用情况
SELECT
name,
path,
formatReadableSize(free_space) AS free,
formatReadableSize(total_space) AS total,
round((total_space - free_space) / total_space * 100, 1) AS used_pct
FROM system.disks;
7.2 清理过期分区
-- 删除指定分区(不可逆!确认备份后操作)
ALTER TABLE db_name.events DROP PARTITION '202501';
-- 批量删除多个旧分区(通过查询生成语句)
SELECT
concat('ALTER TABLE db_name.events DROP PARTITION ''', partition, ''';') AS drop_sql
FROM (
SELECT DISTINCT partition
FROM system.parts
WHERE database = 'db_name'
AND table = 'events'
AND active = 1
AND toDate(partition) < toDate('2025-01-01')
ORDER BY partition
);
-- 使用TTL自动清理过期数据(推荐,防止磁盘告警)
ALTER TABLE db_name.events MODIFY TTL event_time + INTERVAL 90 DAY;
7.3 清理shadow目录
FREEZE操作会在 /var/lib/clickhouse/shadow/ 目录下创建数据快照,长期积累会占用大量磁盘空间。
-- 查看当前freeze快照
SELECT * FROM system.backups;
-- 清理旧的freeze快照
-- 首先确认哪些备份不再需要,然后在OS层面删除
-- rm -rf /var/lib/clickhouse/shadow/*
-- 使用SQL方式清理(ClickHouse 22.4+)
-- 通过查询确认shadow目录大小
# 查看shadow目录大小
du -sh /var/lib/clickhouse/shadow/
# 按备份时间列出
ls -la /var/lib/clickhouse/shadow/
# 删除指定编号的备份
rm -rf /var/lib/clickhouse/shadow/1
8. 常见错误码速查表
| Error Code | 错误信息 | 常见原因 | 解决方法 |
|---|---|---|---|
| 252 | Too many parts | INSERT频率过高,Merge跟不上 | 降低INSERT频率、增大background_pool_size |
| 241 | Memory limit exceeded | 查询内存超限 | 设置max_memory_usage、优化查询 |
| 243 | Cannot allocate memory | 系统内存不足 | 扩容内存、限制并发查询数 |
| 60 | Table doesn’t exist | 表不存在或未ATTACH | 检查表名、执行ATTACH TABLE |
| 81 | Database doesn’t exist | 数据库不存在 | 检查数据库名 |
| 285 | TOO_FEW_LIVE_REPLICAS | 副本数不足,无法满足quorum | 修复宕机副本 |
| 319 | UNKNOWN_STATUS_OF_INSERT | INSERT状态未知(网络异常) | 检查数据是否写入成功再重试 |
| 395 | QUERY_WAS_CANCELLED | 查询被取消 | 检查是否超时或手动取消 |
| 159 | TIMEOUT_EXCEEDED | 查询超时 | 优化查询或增大max_execution_time |
| 76 | CANNOT_READ_ALL_DATA | 数据读取失败(Part损坏) | CHECK TABLE + DETACH损坏分区 |
| 1000 | ZooKeeper session expired | ZK会话过期 | 检查ZooKeeper连通性 |
| 999 | Keeper exception | ZooKeeper通用异常 | 查看ZooKeeper日志详细信息 |
9. 实战案例
案例一:生产集群Too many parts导致写入阻塞
背景:某电商平台实时订单系统,直接将Flink结果以每条记录一次INSERT的方式写入ClickHouse,上线后约4小时开始出现写入超时,最终写入完全阻塞。
排查过程:
-- 第一步:查看系统日志,发现大量 "Too many parts" 错误
-- grep "Too many parts" /var/log/clickhouse-server/clickhouse-server.err.log
-- 第二步:查询Part数量分布
SELECT partition, count() AS parts
FROM system.parts
WHERE active = 1 AND table = 'orders'
GROUP BY partition
ORDER BY parts DESC;
-- 结果:当日分区 '20260519' 已有 587 个Parts(阈值300触发警告,500触发阻塞)
-- 第三步:查看Merge状态
SELECT * FROM system.merges WHERE table = 'orders';
-- 发现只有2个Merge任务在运行,远跟不上INSERT速度
-- 第四步:查看写入QPS
SELECT count(), toStartOfMinute(event_time) AS ts
FROM system.query_log
WHERE type = 'QueryStart' AND query LIKE 'INSERT%' AND table = 'orders'
AND event_time >= now() - INTERVAL 10 MINUTE
GROUP BY ts ORDER BY ts;
-- 发现每分钟约3000次INSERT,平均50次/秒
修复方案:
-- 紧急处理:手动触发Merge
OPTIMIZE TABLE orders PARTITION '20260519';
-- 临时增大后台线程(动态生效)
SYSTEM STOP MERGES orders;
-- 修改config.xml后重载
SYSTEM START MERGES orders;
# 修改Flink写入逻辑:改为批量写入
# 原来:每条记录执行一次INSERT
# 修改后:每5000条记录或每5秒执行一次批量INSERT
buffer = []
for record in stream:
buffer.append(record)
if len(buffer) >= 5000:
clickhouse_client.execute('INSERT INTO orders VALUES', buffer)
buffer = []
结果:批量写入改造后,INSERT次数从50次/秒降至2次/秒,Part数量在1小时内恢复正常。
案例二:ZooKeeper宕机导致副本表只读
背景:某日志分析平台,ZooKeeper节点因机器故障宕机1台(共3节点ZK集群,仍满足多数派),但ClickHouse某些副本表进入只读状态无法写入。
排查过程:
-- 查看副本状态
SELECT database, table, replica_name, is_readonly, last_exception
FROM system.replicas
WHERE is_readonly = 1;
-- 发现只读表的last_exception:
-- "ZooKeeper session has been expired"
-- 查看ZooKeeper连接状态
SELECT * FROM system.zookeeper_connection;
-- 发现部分连接已断开
修复步骤:
-- ZooKeeper集群恢复多数派后,重启ClickHouse副本进程
SYSTEM RESTART REPLICAS;
-- 逐表确认副本恢复
SELECT database, table, is_readonly, queue_size
FROM system.replicas
ORDER BY is_readonly DESC, queue_size DESC;
-- 对仍然只读的表手动触发修复
SYSTEM SYNC REPLICA db_name.nginx_logs;
结果:ZooKeeper集群自愈后,通过 SYSTEM RESTART REPLICAS 命令使所有副本恢复正常写入,总恢复时间约3分钟。
案例三:磁盘空间告警,数据目录占满
背景:某数据仓库ClickHouse节点磁盘使用率达95%,触发告警,需要快速释放空间。
排查过程:
# 第一步:查看整体磁盘使用
df -h /var/lib/clickhouse/
# 第二步:找出最大的目录
du -sh /var/lib/clickhouse/data/*/* | sort -rh | head -20
# 第三步:确认shadow目录大小(FREEZE备份残留)
du -sh /var/lib/clickhouse/shadow/
# 发现shadow目录占用380GB!
-- 第四步:通过SQL确认各表占用
SELECT
database, table,
formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.parts
WHERE active = 1
GROUP BY database, table
ORDER BY sum(bytes_on_disk) DESC
LIMIT 10;
-- 第五步:找到可清理的历史分区
SELECT partition, formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.parts
WHERE database = 'dw' AND table = 'user_events' AND active = 1
GROUP BY partition
ORDER BY partition ASC;
-- 发现2024年以前的分区共计150GB,可按照数据保留策略清理
修复步骤:
# 清理shadow目录(确认没有正在进行的备份后)
rm -rf /var/lib/clickhouse/shadow/*
# 释放380GB
# 删除过期分区(通过事先确认的保留策略)
-- 删除2024年之前的历史分区
ALTER TABLE dw.user_events DROP PARTITION '202312';
ALTER TABLE dw.user_events DROP PARTITION '202311';
-- ... 共删除14个月的历史分区
-- 添加TTL防止未来再次出现磁盘告警
ALTER TABLE dw.user_events MODIFY TTL event_date + INTERVAL 18 MONTH;
结果:清理shadow目录释放380GB,删除历史分区释放150GB,磁盘使用率从95%降至48%。同时添加TTL策略,实现数据自动清理。
10. 总结与最佳实践
通过本文的系统梳理,我们可以总结出ClickHouse运维的核心最佳实践:
预防优于治疗:
- 写入设计:生产环境禁止单条INSERT,务必使用批量写入(建议每批≥1万行,间隔≥1秒)。
- ZooKeeper容量规划:定期监控ZooKeeper节点数量,单个ZooKeeper集群服务的ClickHouse分片数不超过50个,总节点数控制在100万以内。
- 磁盘预警阈值:将告警阈值设置在80%,而非90%,为应急操作预留足够空间。
- TTL策略:所有生产表应配置TTL策略,避免数据无限增长导致磁盘耗尽。
- 监控体系:建立Prometheus + Grafana监控体系,重点监控:Part数量、Merge速度、副本延迟、内存使用、磁盘使用率。
快速响应手册:
| 症状 | 第一步处理 | 根治方案 |
|---|---|---|
| Too many parts | SYSTEM START MERGES |
改批量写入 |
| 副本只读 | SYSTEM RESTART REPLICAS |
修复ZooKeeper |
| 查询OOM | KILL QUERY WHERE query_id=... |
设置内存限制 |
| 磁盘告警 | 清理shadow目录 | 添加TTL策略 |
| Broken part | DETACH PARTITION |
从副本重新同步 |
ClickHouse的运维复杂度主要来自其独特的存储模型和分布式副本机制。深入理解MergeTree的Part合并原理、ZooKeeper在副本协调中的作用,是解决大多数生产问题的基础。在此基础上,配合完善的监控体系和预防性运维规范,ClickHouse集群完全可以做到高可用、高稳定性运行。
上一篇【第55篇】ClickHouse性能调优实战:从SQL到系统层
下一篇【第57篇】ClickHouse与Kafka实时数据管道实战
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)