主题
第 17 章 运维与可观测
学习目标:学完本章你能独立完成一台 ClickHouse 实例的「日常运维四件套」 —— 改配置、做备份、升级版本、接监控。能讲清楚
config.xml/users.xml/config.d/的层叠加载规则;能熟练使用SYSTEM命令族操控副本、合并、缓存;能实施BACKUP / RESTORE与clickhouse-backup两套备份方案;能配置 Prometheus + Grafana 监控并写出实用告警;最后能独立排查一次「副本停止同步」的真实事故。
0. 开场白:DBA 的一天
07:00 Grafana 告警 → 副本延迟 5 分钟 → 17.6 节会教你查
09:00 开发提需求 → 临时调高某用户 max_memory → 17.2 / 17.3 会教你 users.d/
11:00 做备份 → 17.4 节
14:00 升级 23.8 → 24.3 → 17.5 节
16:00 磁盘告警 → 老分区搬到 HDD → 17.2 + 17.3 节
22:00 发布窗口 → SYSTEM RELOAD CONFIG → 17.3 节如果上述任一项你看了一眼就能说出大致命令,可以快速跳过;否则建议从头读一遍 —— 它们在生产里全都会发生,而且都有「错按一下就翻车」的细节。
17.1 关键配置文件全景
ClickHouse 的配置不只一个文件,而是一整套分层 XML 体系。理解层叠规则是改配置不翻车的前提。
17.1.1 文件结构
/etc/clickhouse-server/
├── config.xml ← 实例级主配置(端口、存储、副本协调器)
├── users.xml ← 用户/Profile/Quota 配置
├── config.d/ ← 主配置覆盖目录(按字母序加载)
│ ├── disable_logs.xml
│ ├── prometheus.xml
│ └── storage_s3.xml
├── users.d/ ← 用户配置覆盖目录
│ ├── readonly.xml
│ └── tenant_a.xml
└── conf.d/ ← 旧名字,等价于 config.d/层叠加载顺序:
1. 读 config.xml (基础)
2. 顺序合并 config.d/*.xml
3. 应用环境变量 CLICKHOUSE_* (新版本支持)
4. 应用启动命令行 --<key>=<value>
↓
5. users.xml + users.d/*.xml 同样规则合并
最终在内存里形成「合成配置树」合并规则(重要,面试常考):
| 操作 | XML 写法 | 行为 |
|---|---|---|
| 覆盖 | 默认 | 同路径节点直接替换 |
| 追加 | <remote_servers replace="replace"> | 整段替换 |
| 删除 | <some_node remove="remove" /> | 移除该节点 |
| 引用 | <openSSL incl="openSSL_default" /> | 引用 /etc/metrika.xml 中的同名片段 |
📌 首次术语解释 ·
config.d/users.d:约定俗成的「Drop-in 目录」,灵感来自 systemd。永远不要直接改config.xml/users.xml,所有自定义改动都丢到config.d/users.d,升级时不会被包管理器覆盖。
17.1.2 必看的配置块清单
xml
<!-- config.xml 必看片段 -->
<clickhouse>
<!-- ① 监听地址 -->
<listen_host>0.0.0.0</listen_host> <!-- 监听所有网卡;生产建议改成 :: 或具体 IP -->
<!-- ② 端口 -->
<tcp_port>9000</tcp_port> <!-- TCP 协议(clickhouse-client / driver) -->
<http_port>8123</http_port> <!-- HTTP 协议(curl / clickhouse-connect) -->
<interserver_http_port>9009</interserver_http_port> <!-- 副本之间互拷数据 -->
<mysql_port>9004</mysql_port> <!-- MySQL 兼容协议 -->
<postgresql_port>9005</postgresql_port><!-- PG 兼容协议 -->
<!-- ③ 存储路径与多盘 -->
<path>/var/lib/clickhouse/</path>
<storage_configuration>
<disks>
<hot> <type>local</type><path>/data1/ssd/</path></hot>
<cold> <type>local</type><path>/data2/hdd/</path></cold>
<s3> <type>s3</type><endpoint>https://...</endpoint></s3>
</disks>
<policies>
<hot_to_cold>
<volumes>
<ssd><disk>hot</disk></ssd>
<hdd><disk>cold</disk></hdd>
</volumes>
<move_factor>0.2</move_factor>
</hot_to_cold>
</policies>
</storage_configuration>
<!-- ④ 集群拓扑:定义多分片 / 多副本 -->
<remote_servers>
<prod_cluster>
<shard>
<internal_replication>true</internal_replication>
<replica><host>ck-1</host><port>9000</port></replica>
<replica><host>ck-2</host><port>9000</port></replica>
</shard>
<shard>
<internal_replication>true</internal_replication>
<replica><host>ck-3</host><port>9000</port></replica>
<replica><host>ck-4</host><port>9000</port></replica>
</shard>
</prod_cluster>
</remote_servers>
<!-- ⑤ 协调器:ZK 或 ClickHouse Keeper -->
<zookeeper>
<node><host>kp-1</host><port>9181</port></node>
<node><host>kp-2</host><port>9181</port></node>
<node><host>kp-3</host><port>9181</port></node>
</zookeeper>
<!-- ⑥ MergeTree 全局默认 -->
<merge_tree>
<parts_to_throw_insert>3000</parts_to_throw_insert>
<parts_to_delay_insert>1000</parts_to_delay_insert>
<max_bytes_to_merge_at_max_space_in_pool>161061273600</max_bytes_to_merge_at_max_space_in_pool>
</merge_tree>
<!-- ⑦ 内置监控 -->
<prometheus>
<endpoint>/metrics</endpoint>
<port>9363</port>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>true</asynchronous_metrics>
</prometheus>
</clickhouse>📌 改配置后:
SYSTEM RELOAD CONFIG;大多数项可以热加载;但端口、存储路径、Keeper 地址这类启动期才读的项必须重启实例。
17.1.3 用 SQL 直接看活配置
不想去文件里翻?直接查表:
sql
-- 当前所有 server 设置
SELECT name, value, changed
FROM system.server_settings
ORDER BY changed DESC, name;
-- 所有 query 级 settings
SELECT name, value, description
FROM system.settings
WHERE changed = 1; -- 只看相对默认值改了的
-- 看用户配置
SELECT name, host_ip, default_database
FROM system.users;
-- 看 profile / quota
SELECT * FROM system.profiles;
SELECT * FROM system.quotas;17.2 SYSTEM 命令族大全
SYSTEM 是运维 ClickHouse 的「遥控器」,按功能分为五个家族。
17.2.1 配置 / 字典 / 模型重载
sql
SYSTEM RELOAD CONFIG; -- 重新读取 config.xml + config.d/*
SYSTEM RELOAD USERS; -- 重新读取 users.xml + users.d/*
SYSTEM RELOAD DICTIONARIES; -- 全部字典刷新
SYSTEM RELOAD DICTIONARY <name>; -- 单个字典刷新
SYSTEM RELOAD MODEL <name>; -- CatBoost / ONNX 模型
SYSTEM RELOAD FUNCTION <name>; -- Executable UDF 刷新17.2.2 后台任务控制
sql
SYSTEM STOP MERGES [db.table]; -- 暂停所有 / 某表的 merge
SYSTEM START MERGES [db.table];
SYSTEM STOP REPLICATION_QUEUES; -- 暂停副本队列
SYSTEM START REPLICATION_QUEUES;
SYSTEM STOP DISTRIBUTED SENDS [db.table];
SYSTEM START DISTRIBUTED SENDS [db.table];
SYSTEM STOP TTL MERGES;
SYSTEM START TTL MERGES;典型场景:导入 1 TB 历史数据时先 STOP MERGES,避免边写边合并把 IO 打满;导完再 START MERGES,让后台慢慢追。
17.2.3 副本协调
sql
SYSTEM SYNC REPLICA <db>.<table>; -- 等本副本追平 leader
SYSTEM RESTART REPLICA <db>.<table>; -- 重新初始化某副本(不删数据)
SYSTEM RESTORE REPLICA <db>.<table>; -- 重建本副本元数据(数据完好但 ZK 状态丢了时用)
SYSTEM DROP REPLICA '<replica_name>'; -- 在协调器中清理已下线副本的注册
SYSTEM RESTART REPLICAS; -- 重启所有副本📌 常见踩坑:
SYSTEM SYNC REPLICA会阻塞直到追平;如果队列卡了 1 小时,命令也会等 1 小时。生产建议加SETTINGS receive_timeout = 600。
17.2.4 缓存与日志
sql
SYSTEM DROP MARK CACHE; -- 清 mark 索引缓存
SYSTEM DROP UNCOMPRESSED CACHE; -- 清未压缩块缓存
SYSTEM DROP COMPILED EXPRESSION CACHE;-- 清 LLVM 编译表达式缓存
SYSTEM DROP DNS CACHE; -- 清 DNS 缓存(修了 hosts 才用)
SYSTEM DROP QUERY CACHE; -- 23.x 引入的 query result cache
SYSTEM DROP FILESYSTEM CACHE; -- 清远端存储(S3)的本地文件缓存
SYSTEM FLUSH LOGS; -- 把 query_log/text_log/trace_log 强制刷盘
SYSTEM FLUSH DISTRIBUTED <table>; -- 强制把 Distributed 表的写入缓冲冲下去17.2.5 维护与修复
sql
SYSTEM SHUTDOWN; -- 优雅关停(等所有 query 跑完)
SYSTEM KILL; -- 强杀(不等)
SYSTEM RELOAD ASYNCHRONOUS_METRICS;
SYSTEM ENABLE FAILPOINT '<name>'; -- 测试用,注入故障
SYSTEM JEMALLOC PURGE; -- 触发 jemalloc 内存归还 OS17.2.6 与 PG / MySQL 对比
📌 横向对比
操作 ClickHouse PostgreSQL MySQL 重载配置 SYSTEM RELOAD CONFIGpg_reload_conf()/kill -HUPFLUSH PRIVILEGES仅权限杀死会话 KILL QUERY WHERE query_id=...pg_cancel_backend(pid)/pg_terminate_backend(pid)KILL <thread_id>清缓存 SYSTEM DROP MARK CACHEDISCARD ALL仅会话级RESET QUERY CACHE强制刷日志 SYSTEM FLUSH LOGSpg_switch_wal()半相关FLUSH BINARY LOGS
17.3 备份与恢复
OLAP 数据动辄 TB ~ PB 级,备份策略不能套用 OLTP 那一套「每天一份全量」。ClickHouse 提供四种主流方案,按场景挑。
17.3.1 方案一:内置 BACKUP / RESTORE(21.x+,推荐入门)
sql
-- 1) 配置一个备份盘(config.d/backup_disk.xml)
<storage_configuration>
<disks>
<backups>
<type>local</type>
<path>/var/backups/clickhouse/</path>
</backups>
</disks>
</storage_configuration>
<backups>
<allowed_disk>backups</allowed_disk>
<allowed_path>/var/backups/clickhouse/</allowed_path>
</backups>sql
-- 2) 备份单表
BACKUP TABLE learn_ck.events_v2
TO Disk('backups', 'events_v2_2025_04_17.zip');
-- 备份整库
BACKUP DATABASE learn_ck
TO Disk('backups', 'learn_ck_2025_04_17.zip');
-- 增量备份(基于上一次的 base 快照)
BACKUP TABLE learn_ck.events_v2
TO Disk('backups', 'events_v2_2025_04_17_inc.zip')
SETTINGS base_backup = Disk('backups', 'events_v2_2025_04_17.zip');
-- 备份到 S3
BACKUP DATABASE learn_ck
TO S3('https://my-bkt.s3.amazonaws.com/ck/2025-04-17', 'AKID', 'SK');
-- 恢复
RESTORE TABLE learn_ck.events_v2 AS learn_ck.events_v2_restored
FROM Disk('backups', 'events_v2_2025_04_17.zip');
-- 看历史与状态
SELECT id, name, status, error, num_files, total_size
FROM system.backups
ORDER BY start_time DESC;特点:
- 优点:原生命令,支持全量 / 增量 / 选择性恢复;可备份到 Local / S3 / Azure;一致性快照(基于 part 硬链接)。
- 缺点:自身不做调度、不做生命周期;备份元数据存在
system.backups,重启后只剩历史,不能续传。
17.3.2 方案二:clickhouse-backup(社区生产标配)
Altinity/clickhouse-backup 是社区最广泛使用的运维工具,对 BACKUP / RESTORE 做了一层「工程化封装」:调度、压缩、保留策略、远端推送、增量管理、UI。
bash
# 安装
curl -L https://github.com/Altinity/clickhouse-backup/releases/latest/download/clickhouse-backup_linux_amd64.tar.gz | tar xz
sudo mv clickhouse-backup /usr/local/bin/
# 配置 /etc/clickhouse-backup/config.yml
general:
remote_storage: s3
backups_to_keep_local: 7
backups_to_keep_remote: 30
clickhouse:
username: default
s3:
access_key: ...
secret_key: ...
bucket: my-ck-backups
region: ap-east-1
path: prod/
# 一条命令完成:本地快照 → 推 S3
clickhouse-backup create_remote daily_$(date +%F)
# 列出
clickhouse-backup list
# 还原
clickhouse-backup restore_remote daily_2025-04-17clickhouse-backup 推荐配 systemd timer 跑,比写 cron 更稳。
17.3.3 方案三:FREEZE PARTITION(最古老,最低开销)
sql
ALTER TABLE learn_ck.events_v2 FREEZE PARTITION 202504;
-- → 在 /var/lib/clickhouse/shadow/<N>/ 下生成对硬链接
-- → 几乎瞬间完成(不复制数据,只 hardlink)
-- → 之后 rsync / tar 这个目录到外部存储即可恢复流程是手动的:把 shadow 目录 rsync 回 detached/,再 ALTER TABLE ... ATTACH PARTITION 挂载。
适用场景:
- 想跨 ClickHouse 集群手工搬数据;
- 需要在备份时完全不影响业务(hardlink 不占新空间,秒级完成);
- 自己写脚本管理增量。
17.3.4 方案四:物理备份(rsync 整个数据目录)
最朴素:实例停机或 SYSTEM STOP MERGES; + FLUSH ASYNC INSERT QUEUE; 后整目录 rsync 走。不推荐:
- 不能保证一致性快照;
- 与 ZK / Keeper 中的元数据不同步;
- 恢复时副本注册要手工修。
仅在「机器要报废、紧急抢救」时一次性用。
17.3.5 物理 vs 逻辑、全量 vs 增量
| 维度 | 物理(FREEZE / rsync) | 逻辑(BACKUP / clickhouse-backup) |
|---|---|---|
| 速度 | 快(hardlink) | 略慢(要序列化 part metadata) |
| 跨版本 | 不保证 | 推荐做大版本升级前的安全备份 |
| 选择性恢复 | 难 | 易(按表/分区/库) |
| 远端存储 | 自己 rsync | 内置 S3 / Azure / GCS |
| 一致性 | 看你怎么搞 | 自带快照点 |
生产建议:日常 clickhouse-backup + 大变更前一次 BACKUP DATABASE,本地保留 7 天,远端保留 30 天。
17.4 升级与版本兼容
ClickHouse 发版极快(基本一月一版),生产升级要讲方法。
17.4.1 选 LTS 版
ClickHouse 官方有「LTS(Long-Term Support)」标签,例如 22.8、23.3、23.8、24.3、24.8。生产环境不要追最新,优先选最近的 LTS:
bash
# 看当前装的版本
clickhouse-server --version
# ClickHouse server version 23.8.7.24 (official build).
# Altinity 的 LTS 列表
# https://docs.altinity.com/altinitystablebuilds/17.4.2 升级前的检查清单
- 看 release notes 中的 Backward Incompatible Changes(每次都要看,绝对不能跳过)。
BACKUP DATABASE system; BACKUP DATABASE <biz>;备一次。SELECT * FROM system.settings WHERE changed=1导出当前自定义 settings。- 下载新版本,先在 staging 实例上跑业务样本 SQL 验证(重点测:物化视图、复杂聚合、UDF)。
- 确认 driver / clickhouse-connect / JDBC 等客户端兼容性。
17.4.3 灰度升级流程(多副本集群)
前提:至少 2 副本 / 分片,业务通过 LB / 域名 + Keeper 访问
第 1 步:下线副本 R1(从 LB 摘掉)
↓
第 2 步:在 R1 上:
systemctl stop clickhouse-server
apt install clickhouse-server=24.3.x.x
systemctl start clickhouse-server
↓
第 3 步:观察 system.replicas / replication_queue
确认 R1 与 R2 同步追平
↓
第 4 步:把 R1 加回 LB,观察 query_log 错误率
↓
第 5 步:对其余副本逐台重复 1~4回滚:只要 part 兼容(同一年内的版本通常兼容),把 deb 包降级即可。如果 part 格式有破坏性变更(罕见,发布说明会强提示),需要从备份还原。
17.4.4 版本兼容速记
| 范围 | 通常兼容性 |
|---|---|
| Patch 升级(24.3.1 → 24.3.5) | 100% 兼容,随便升 |
| Minor 升级(24.3 → 24.5) | 高度兼容,看 release notes 即可 |
| 跨大版本(22.8 → 24.3) | 必须读完所有中间版本的不兼容变更,并准备灰度 |
| 协议 / wire 兼容 | 服务端版本通常向下兼容客户端 1~2 个大版本 |
17.5 监控:Prometheus + Grafana
「不可观测的系统等于不可运维」。ClickHouse 内置 Prometheus 端点,10 分钟搭起整套监控。
17.5.1 启用内置 Prometheus 端点
config.d/prometheus.xml:
xml
<clickhouse>
<prometheus>
<endpoint>/metrics</endpoint>
<port>9363</port>
<metrics>true</metrics>
<events>true</events>
<asynchronous_metrics>true</asynchronous_metrics>
<status_info>true</status_info>
<errors>true</errors>
</prometheus>
</clickhouse>SYSTEM RELOAD CONFIG; 后 curl http://127.0.0.1:9363/metrics 就能看到几百个指标。
Prometheus 抓取配置:
yaml
scrape_configs:
- job_name: 'clickhouse'
static_configs:
- targets: ['ck-1:9363', 'ck-2:9363', 'ck-3:9363', 'ck-4:9363']
scrape_interval: 15s17.5.2 必盯的关键指标
| 指标 | 类型 | 说明 | 告警阈值参考 |
|---|---|---|---|
ClickHouseProfileEvents_Query | counter | 总查询数 | rate 突变看是否压测 / 流量异常 |
ClickHouseProfileEvents_SelectQuery / _InsertQuery | counter | 读写分别计数 | 比例突变定位是否插入风暴 |
ClickHouseAsyncMetrics_ReplicasMaxAbsoluteDelay | gauge | 副本最大绝对延迟(秒) | > 60 报警 |
ClickHouseAsyncMetrics_ReplicasSumQueueSize | gauge | 复制队列总长 | > 100 报警 |
ClickHouseMetrics_BackgroundMergesAndMutationsPoolTask | gauge | 后台 merge 在跑数 | 长期 = 池上限 → 调大或排查写入风暴 |
ClickHouseAsyncMetrics_MaxPartCountForPartition | gauge | 单分区最大 part 数 | > 150 警告,> 300 紧急 |
ClickHouseMetrics_MemoryTracking | gauge | 实例总内存(追踪值) | > 物理内存 80% 报警 |
ClickHouseProfileEvents_MarkCacheHits / _Misses | counter | mark cache 命中 | hit_ratio < 90% 警告 |
ClickHouseAsyncMetrics_DiskUsed_default | gauge | 默认盘占用 | > 80% 报警 |
ClickHouseProfileEvents_FailedQuery | counter | 失败查询数 | rate 突增 |
ClickHouseProfileEvents_OSCPUVirtualTimeMicroseconds | counter | 用户态 CPU | 推 utilization 公式 |
17.5.3 Grafana 面板模板
ClickHouse 官方 + Altinity 维护的常用面板 ID:
- Grafana ID
14192:Altinity ClickHouse Server Dashboard,覆盖 Query / Replication / Merge / Disk / Memory 五大版块。 - Grafana ID
13500:ClickHouse Internal Metrics,更细粒度的 ProfileEvents。 - Grafana ID
2515:经典 ClickHouse Overview。
直接 Import → Paste ID → 选 Prometheus 数据源,几秒钟就能用。
17.5.4 Prometheus 告警规则示例
yaml
groups:
- name: clickhouse
rules:
- alert: CHReplicaDelay
expr: ClickHouseAsyncMetrics_ReplicasMaxAbsoluteDelay > 60
for: 5m
labels: { severity: critical }
annotations:
summary: "ClickHouse 副本延迟 > 60s"
description: "{{ $labels.instance }} 副本延迟 {{ $value }} 秒"
- alert: CHTooManyParts
expr: ClickHouseAsyncMetrics_MaxPartCountForPartition > 200
for: 10m
labels: { severity: warning }
annotations:
summary: "单分区 Part 数过多 ({{ $value }})"
- alert: CHMemoryHigh
expr: ClickHouseMetrics_MemoryTracking
/ on(instance) node_memory_MemTotal_bytes > 0.85
for: 5m
labels: { severity: critical }
annotations: { summary: "ClickHouse 内存占比 > 85%" }
- alert: CHDiskUsage
expr: (1 - ClickHouseAsyncMetrics_DiskAvailable_default
/ ClickHouseAsyncMetrics_DiskTotal_default) > 0.85
for: 10m
labels: { severity: critical }
annotations: { summary: "ClickHouse 数据盘 > 85%" }
- alert: CHFailedQueryBurst
expr: rate(ClickHouseProfileEvents_FailedQuery[5m]) > 1
for: 5m
labels: { severity: warning }
annotations: { summary: "失败查询飙升 (>1/s)" }17.6 真实案例:副本停止同步的完整排查流程
下面这个场景在生产里几乎每个 DBA 都遇到过,借此把 17.1 ~ 17.5 学到的东西串起来。
现象
监控告警:CHReplicaDelay = 7200s(2 小时),同时业务反馈某些表查到的数据陈旧。
Step 1:确认范围 —— 是哪张表 / 哪个副本
sql
-- 在卡住的副本上执行
SELECT
database, table,
is_leader, is_readonly, is_session_expired,
absolute_delay,
queue_size, inserts_in_queue, merges_in_queue,
log_max_index, log_pointer
FROM system.replicas
WHERE absolute_delay > 60
ORDER BY absolute_delay DESC;输出长这样:
database │ table │ is_readonly │ absolute_delay │ queue_size │ inserts_in_queue
learn_ck │ events_dist │ 0 │ 7203 │ 412 │ 398→ 锁定到 learn_ck.events_dist(其实是它对应的本地表 events_local)卡住了 412 条队列。
Step 2:看队列里到底卡在哪条
sql
SELECT
type, -- GET_PART / MERGE_PARTS / MUTATE_PART / ...
new_part_name,
create_time,
last_attempt_time,
last_exception,
num_postponed,
postpone_reason
FROM system.replication_queue
WHERE database = 'learn_ck' AND table = 'events_local'
ORDER BY create_time
LIMIT 20;通常 last_exception 一栏就有线索。常见三种:
Cannot fetch part xxx, exception: Connection refused→ 源副本宕了 / 网络断了。Cannot allocate disk space→ 磁盘满了。Code: 999, e.what() = Coordination→ Keeper 出问题了。
Step 3:分情况处理
3.1 源副本宕
sql
-- 在还活的副本看其它副本的活跃情况
SELECT host_name, host_address, port, is_active
FROM system.clusters WHERE cluster = 'prod_cluster';
-- 在协调器中看注册副本
SELECT * FROM system.zookeeper
WHERE path = '/clickhouse/tables/<shard>/events_local/replicas';确认源副本机器实际状态;启不来就 SYSTEM DROP REPLICA '<replica_name>' FROM TABLE events_local; 把它从协调器中踢掉,再添新副本。
3.2 磁盘满
清出空间,或把老分区 MOVE PARTITION ... TO DISK 'cold' 搬走。然后队列会自动恢复。
3.3 Keeper / ZK 异常
sql
-- 看 keeper 是不是健康
SELECT name, value FROM system.zookeeper WHERE path = '/';
-- 4 字命令(Keeper / ZK)
$ echo mntr | nc kp-1 9181
$ echo stat | nc kp-1 9181如果发现 znode count 异常大或者 leader 漂移频繁,问题在协调器侧,要重启 Keeper 实例。
Step 4:恢复后验证
sql
-- 推一下追平
SYSTEM SYNC REPLICA learn_ck.events_local;
-- 等命令返回后再验证
SELECT absolute_delay, queue_size FROM system.replicas
WHERE database = 'learn_ck' AND table = 'events_local';
-- absolute_delay → 0
-- queue_size → 0最后写一条事故复盘到 wiki。真实事故 90% 都来自这三类:源副本宕、磁盘满、Keeper 抖。
17.7 与 PG / MySQL 运维工具对比
📌 横向对比小框
维度 ClickHouse PostgreSQL MySQL 配置层叠 config.xml+config.d/*postgresql.conf+conf.d/*+ALTER SYSTEMmy.cnf+!includedir重载 SYSTEM RELOAD CONFIGSELECT pg_reload_conf()FLUSH PRIVILEGES(仅权限)备份方案 BACKUP/RESTORE+ clickhouse-backup + FREEZEpg_dump+pg_basebackup+pgBackRest/barmanmysqldump+xtrabackup监控生态 内置 Prometheus 端点 pg_exporter(外置)mysqld_exporter(外置)副本管理 system.replicas+SYSTEM SYNCpg_stat_replication+pg_promote()SHOW SLAVE STATUS+START/STOP SLAVE升级方式 包升级 + 灰度 pg_upgrade(大版本要做)in-place 或 dump-restore
一句话比较:ClickHouse 的运维像「写 SQL 操作底层 —— SYSTEM 是 SQL,监控是 SQL,备份也是 SQL」,对熟悉 SQL 的人极其友好;PG / MySQL 则更依赖外部工具链。
17.8 本章小结
┌──────────────────────────────────────────────────────────┐
│ 本章核心要点 │
├──────────────────────────────────────────────────────────┤
│ ① 配置体系: │
│ config.xml + config.d/ + users.xml + users.d/ │
│ 永远改 .d/,不改主文件 │
│ │
│ ② SYSTEM 命令五家族: │
│ RELOAD(配置/字典)/ STOP-START(merge/repl) │
│ SYNC-RESTART(副本)/ DROP CACHE/ FLUSH LOGS │
│ │
│ ③ 备份四方案: │
│ BACKUP/RESTORE 入门首选 │
│ clickhouse-backup 生产标配 │
│ FREEZE PARTITION 零成本快照 │
│ rsync 应急用 │
│ │
│ ④ 升级原则:选 LTS / 灰度副本 / 备份兜底 │
│ │
│ ⑤ 监控:内置 Prometheus 端点 │
│ 必盯指标:副本延迟 / Part 数 / 内存 / 磁盘 / 失败查询 │
│ Grafana ID 14192 / 13500 即用 │
│ │
│ ⑥ 事故 SOP: │
│ system.replicas → replication_queue → ZK status │
│ 90% 事故来自源副本宕 / 磁盘满 / Keeper 抖 │
└──────────────────────────────────────────────────────────┘17.9 面试高频题
Q1:config.xml 与 users.xml 中的配置加载优先级是怎样的?为什么不要直接改主配置文件?
考察点:是否理解 ClickHouse 的配置层叠机制和最佳实践。
标准答案:
- ClickHouse 启动时按顺序加载:①
config.xml主文件 → ② 顺序合并config.d/*.xml→ ③ 合并环境变量CLICKHOUSE_*→ ④ 合并启动命令行--key=value,形成「合成配置树」;users.xml和users.d/同理。 - 同名节点默认是「覆盖」语义;可以用
replace="replace"/remove="remove"/incl="..."控制合并行为。 - 不要改主文件的原因:升级 deb / rpm 包时主文件可能被覆盖;自定义改动放
config.d/、users.d/既不会被升级覆盖,也方便用 ConfigMap / Ansible 管理。 - 改完用
SYSTEM RELOAD CONFIG热加载;端口、存储路径、Keeper 地址等启动期项需要重启。
加分项:能提「SELECT * FROM system.server_settings WHERE changed=1 直接看活配置」;能讲 incl="..." 引用外部文件(早年的 metrika.xml 习惯)。
易错点:以为 config.d/ 中的文件会整体替换 config.xml —— 实际是节点级合并。
Q2:SYSTEM SYNC REPLICA 和 SYSTEM RESTART REPLICA 有什么区别?
考察点:副本协调机制。
标准答案:
SYSTEM SYNC REPLICA <table>:阻塞等本副本把当前replication_queue里的所有任务跑完(追平 leader),不影响数据本身,只是「等」。常用于「我刚改完拓扑,先等同步再放业务流量」。SYSTEM RESTART REPLICA <table>:把这个副本的元数据从 ZK / Keeper 重新加载一次,相当于「卸载 + 重新挂载这张表」,数据不会丢,但会清空内存里的副本状态。常用于副本卡死、状态错乱时的「软重启」。- 还有
SYSTEM RESTORE REPLICA(更重):当 ZK 元数据丢了但数据 part 还在时,重建协调器中的注册项。
加分项:能补一句「SYSTEM DROP REPLICA '<name>' FROM TABLE t」用于清理已下线副本在协调器中的「僵尸注册」。
易错点:把 RESTART 当成「重启进程」 —— 它只重启这张表的副本对象,不影响其他表,也不影响进程。
Q3:你怎么给一个上线 1 年的 ClickHouse 集群做升级?
考察点:实战流程。
标准答案(按步骤回答):
- 预检:读新版本 release notes 中的 Backward Incompatible Changes;导出当前
system.settings中改过默认值的项;记录使用的物化视图、UDF、Kafka 引擎等敏感对象。 - 备份:
BACKUP DATABASE+clickhouse-backup双保险,远端 S3 至少一份。 - Staging 验证:在低流量副本(或独立 staging 集群)升级,跑业务样本 SQL 半天到一天,重点测物化视图触发、复杂聚合、JOIN、客户端兼容。
- 灰度上线:摘 1 个副本 → 升级 → 观察
system.replicas追平 → 加回 LB → 观察system.query_log错误率与 P99 → 重复对其余副本。 - 回滚预案:包降级即可;若 part 格式有变化(罕见),从备份还原。
- 优先选 LTS 版(22.8 / 23.3 / 23.8 / 24.3 / 24.8 这些标 LTS 的)。
加分项:能提到「客户端 driver 也要测兼容性」和「升级期间停止 mutation 队列,等队列清空再开始」。
易错点:直接 apt upgrade 全节点滚一遍 —— 出错就是全军覆没。
Q4:备份方案怎么选?BACKUP/RESTORE、clickhouse-backup、FREEZE PARTITION 有什么区别?
考察点:场景选型。
标准答案:
| 方案 | 一句话 | 适合 |
|---|---|---|
BACKUP / RESTORE | 内置 SQL 命令,支持本地/S3/Azure,全量+增量 | 中小规模,作为入门方案 |
clickhouse-backup | 社区工具对 BACKUP 的工程化封装:调度+保留策略+远端推送 | 生产标配 |
FREEZE PARTITION | 硬链接快照,零成本,秒级完成,再自己 rsync | 跨集群手工搬数据;零业务影响 |
rsync 整目录 | 朴素物理备份 | 只在「机器报废、紧急抢救」用 |
加分项:能提「BACKUP 的本质也是基于 part 硬链接打快照 + 序列化 metadata」;能说「逻辑备份(INSERT INTO ... SELECT 写到另一张 S3 表)跨大版本最稳」。
易错点:用 mysqldump 思维 SELECT * FROM ... INTO OUTFILE 当备份 —— 没有元数据 / 索引 / 物化视图 / DICTIONARY 信息,只能算「数据导出」。
Q5:ClickHouse 监控应该重点看哪几个指标?为什么?
考察点:实战经验。
标准答案:5 个核心指标 + 它们对应的「事故症状」:
ReplicasMaxAbsoluteDelay:副本最大延迟。> 60 秒说明同步滞后,分布式查询会读到陈旧数据。MaxPartCountForPartition:单分区最大 part 数。> 150 警告,> 300 INSERT 会被拒绝。MemoryTracking:实例总内存追踪。> 物理内存 80% 即将 OOM。BackgroundMergesAndMutationsPoolTask:后台 merge 在跑数。长期顶池上限 = 写入风暴或 mutation 失控。MarkCacheHits / Misses命中率:< 90% 说明工作集塞不进 mark cache,要么调大 cache、要么改 SQL 减少 mark 扫描。
加分项:还能提 FailedQuery 速率突变、DiskUsed_default 容量、OSCPUVirtualTimeMicroseconds 折算 CPU 利用率;能说出 Grafana 模板 ID(14192 / 13500)。
易错点:只盯 QPS / 平均查询耗时,没看副本延迟 → 业务读到旧数据投诉时才发现。
Q6:副本停止同步了,你怎么排查?
考察点:综合事故处理。
标准答案:4 步 SOP:
- 范围确定:
SELECT database, table, absolute_delay, queue_size FROM system.replicas WHERE absolute_delay > 60锁定卡住的表 / 副本。 - 看队列:
SELECT type, new_part_name, last_exception, num_postponed, postpone_reason FROM system.replication_queue WHERE database=... AND table=... ORDER BY create_time LIMIT 20,重点看last_exception。 - 分类处理:
- 异常含「Connection refused」/「Cannot fetch part」→ 源副本宕,去看那台机器;启不来就
SYSTEM DROP REPLICA+ 加新副本。 - 异常含「Cannot allocate disk space」→ 磁盘满,清空间或
MOVE PARTITION到冷盘。 - 异常含「Coordination」/「Session expired」→ Keeper / ZK 异常,
echo mntr | nc keeper 9181检查协调器,必要时重启 Keeper 实例。
- 异常含「Connection refused」/「Cannot fetch part」→ 源副本宕,去看那台机器;启不来就
- 验证恢复:
SYSTEM SYNC REPLICA <table>推一下追平,再确认absolute_delay = 0且queue_size = 0,事后写复盘。
加分项:能提「事故记录到 system.text_log 里搜 level='Error' 看历史触发时间线」;能提「Keeper 集群的 leader 漂移监控也很关键」。
易错点:直接 RESTART REPLICA 而不查原因,结果队列还是卡住,徒劳。
📌 下一章预告:第 18 章我们看 ClickHouse 的「外延」 ——
clickhouse-local、chdb、UDF、与 BI / Spark / Flink 集成、生态周边工具。配合这一章,你就能把 ClickHouse 从「一个数据库」用成「一套数据基础设施」。
🎬 可视化演示
演示加载缓慢或样式异常?点此在新标签页打开 ↗
💻 示例代码
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
第 17 章 · ClickHouse 健康巡检脚本
====================================
干什么:
一次性查 5 张关键 system 视图, 把「副本延迟 / Part 数 / Merge 队列 /
Mutation 队列 / 磁盘水位 / 内存 / 失败查询」的现状汇总成一张 Markdown 风格
的健康报告. 适合放到 cron / systemd timer 里每 5 分钟跑一次.
前置:
python -m pip install clickhouse-connect
用法:
python ops_check.py
python ops_check.py --host 127.0.0.1 --port 8123
python ops_check.py --json # 输出 JSON, 给 Alertmanager / 飞书机器人吃
退出码:
0 = 全部 OK
1 = WARNING (有指标超阈值, 但不致命)
2 = CRITICAL (必须立刻处理)
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
from typing import Any, Dict, List
import clickhouse_connect
# ============== 阈值表(可改) ==============
THRESH = {
"replica_delay_warn": 30, # 秒
"replica_delay_crit": 120,
"queue_size_warn": 50,
"queue_size_crit": 300,
"parts_per_partition_warn": 150,
"parts_per_partition_crit": 300,
"mutation_pending_warn": 10,
"mutation_pending_crit": 50,
"disk_pct_warn": 0.80,
"disk_pct_crit": 0.90,
"mem_pct_warn": 0.75,
"mem_pct_crit": 0.90,
"mark_cache_hit_warn": 0.90,
}
LEVEL_OK = 0
LEVEL_WARN = 1
LEVEL_CRIT = 2
LEVEL_NAME = {0: "OK", 1: "WARN", 2: "CRIT"}
@dataclass
class Section:
name: str
level: int = LEVEL_OK
rows: List[Dict[str, Any]] = field(default_factory=list)
notes: List[str] = field(default_factory=list)
def get_client(host: str, port: int, user: str, password: str):
return clickhouse_connect.get_client(
host=host, port=port, username=user, password=password,
)
# ---------------------------------------------------------------------------
# 1) 副本延迟 + 队列
# ---------------------------------------------------------------------------
def check_replicas(client) -> Section:
s = Section("1) 副本同步状态 (system.replicas)")
rows = client.query("""
SELECT
database, table,
is_readonly, is_session_expired,
absolute_delay,
queue_size, inserts_in_queue, merges_in_queue
FROM system.replicas
ORDER BY absolute_delay DESC
LIMIT 20
""").result_rows
cols = ["db", "table", "ro", "expired", "delay_s",
"queue", "ins_q", "mrg_q"]
for r in rows:
d = dict(zip(cols, r))
if d["delay_s"] >= THRESH["replica_delay_crit"] \
or d["queue"] >= THRESH["queue_size_crit"] \
or d["expired"] == 1:
d["__level"] = LEVEL_CRIT
s.level = max(s.level, LEVEL_CRIT)
elif d["delay_s"] >= THRESH["replica_delay_warn"] \
or d["queue"] >= THRESH["queue_size_warn"] \
or d["ro"] == 1:
d["__level"] = LEVEL_WARN
s.level = max(s.level, LEVEL_WARN)
else:
d["__level"] = LEVEL_OK
s.rows.append(d)
if not rows:
s.notes.append("(无 Replicated 表, 跳过)")
return s
# ---------------------------------------------------------------------------
# 2) Part 数量 (找出单分区 Part 数过多)
# ---------------------------------------------------------------------------
def check_parts(client) -> Section:
s = Section("2) 单分区 Part 数 (system.parts active=1)")
rows = client.query("""
SELECT
database, table, partition,
count() AS parts,
sum(rows) AS rows,
formatReadableSize(sum(bytes_on_disk)) AS size
FROM system.parts
WHERE active AND database NOT IN ('system','INFORMATION_SCHEMA')
GROUP BY database, table, partition
HAVING parts >= 50
ORDER BY parts DESC
LIMIT 20
""").result_rows
cols = ["db", "table", "partition", "parts", "rows", "size"]
for r in rows:
d = dict(zip(cols, r))
if d["parts"] >= THRESH["parts_per_partition_crit"]:
d["__level"] = LEVEL_CRIT
s.level = max(s.level, LEVEL_CRIT)
elif d["parts"] >= THRESH["parts_per_partition_warn"]:
d["__level"] = LEVEL_WARN
s.level = max(s.level, LEVEL_WARN)
else:
d["__level"] = LEVEL_OK
s.rows.append(d)
if not rows:
s.notes.append("所有分区 part 数 < 50, 健康.")
return s
# ---------------------------------------------------------------------------
# 3) 后台 Merge / Mutation 队列
# ---------------------------------------------------------------------------
def check_merges(client) -> Section:
s = Section("3) 后台任务 (system.merges + system.mutations)")
merges = client.query("""
SELECT database, table, elapsed, progress, num_parts,
formatReadableSize(total_size_bytes_compressed) AS size,
is_mutation
FROM system.merges
ORDER BY elapsed DESC LIMIT 10
""").result_rows
cols = ["db", "table", "elapsed_s", "progress",
"num_parts", "size", "is_mut"]
for r in merges:
d = dict(zip(cols, r))
d["__level"] = LEVEL_OK
s.rows.append(d)
pending = client.query("""
SELECT database, table, mutation_id,
command, parts_to_do, latest_fail_reason
FROM system.mutations
WHERE is_done = 0
ORDER BY create_time
""").result_rows
cols2 = ["db", "table", "mut_id", "cmd", "parts_left", "fail_reason"]
for r in pending:
d = dict(zip(cols2, r))
d["__level"] = LEVEL_WARN
s.level = max(s.level, LEVEL_WARN)
s.rows.append(d)
if len(pending) >= THRESH["mutation_pending_crit"]:
s.level = LEVEL_CRIT
elif len(pending) >= THRESH["mutation_pending_warn"]:
s.level = max(s.level, LEVEL_WARN)
if not merges and not pending:
s.notes.append("无活跃 merge, 无待处理 mutation, 健康.")
return s
# ---------------------------------------------------------------------------
# 4) 磁盘水位
# ---------------------------------------------------------------------------
def check_disks(client) -> Section:
s = Section("4) 磁盘水位 (system.disks)")
rows = client.query("""
SELECT name, path,
formatReadableSize(total_space) AS total,
formatReadableSize(free_space) AS free,
total_space, free_space, type
FROM system.disks
""").result_rows
cols = ["name", "path", "total", "free", "total_b", "free_b", "type"]
for r in rows:
d = dict(zip(cols, r))
used_pct = 1.0 - (d["free_b"] / max(d["total_b"], 1))
d["used_pct"] = round(used_pct * 100, 1)
if used_pct >= THRESH["disk_pct_crit"]:
d["__level"] = LEVEL_CRIT
s.level = max(s.level, LEVEL_CRIT)
elif used_pct >= THRESH["disk_pct_warn"]:
d["__level"] = LEVEL_WARN
s.level = max(s.level, LEVEL_WARN)
else:
d["__level"] = LEVEL_OK
d.pop("total_b"); d.pop("free_b")
s.rows.append(d)
return s
# ---------------------------------------------------------------------------
# 5) 内存 + Mark Cache 命中
# ---------------------------------------------------------------------------
def check_memory(client) -> Section:
s = Section("5) 内存与缓存 (system.metrics + system.events)")
mem_track = client.query("""
SELECT value FROM system.metrics WHERE metric = 'MemoryTracking'
""").result_rows[0][0]
try:
mem_total_row = client.query("""
SELECT value FROM system.asynchronous_metrics
WHERE metric = 'OSMemoryTotal'
""").result_rows
mem_total = mem_total_row[0][0] if mem_total_row else None
except Exception:
mem_total = None
mem_pct = None
if mem_total and mem_total > 0:
mem_pct = mem_track / mem_total
s.rows.append({
"item": "MemoryTracking",
"value": f"{mem_track / (1024 ** 3):.2f} GiB",
"of_total_pct": (
f"{mem_pct * 100:.1f}%" if mem_pct is not None else "n/a"
),
"__level": (
LEVEL_CRIT if mem_pct and mem_pct >= THRESH["mem_pct_crit"]
else LEVEL_WARN if mem_pct and mem_pct >= THRESH["mem_pct_warn"]
else LEVEL_OK
),
})
if mem_pct and mem_pct >= THRESH["mem_pct_crit"]:
s.level = max(s.level, LEVEL_CRIT)
elif mem_pct and mem_pct >= THRESH["mem_pct_warn"]:
s.level = max(s.level, LEVEL_WARN)
cache = client.query("""
SELECT
sumIf(value, event = 'MarkCacheHits') AS hits,
sumIf(value, event = 'MarkCacheMisses') AS misses
FROM system.events
WHERE event LIKE 'MarkCache%'
""").result_rows[0]
hits, misses = cache
total = hits + misses
hit_ratio = hits / total if total else 1.0
lvl = LEVEL_OK if hit_ratio >= THRESH["mark_cache_hit_warn"] else LEVEL_WARN
s.level = max(s.level, lvl)
s.rows.append({
"item": "MarkCacheHitRatio",
"value": f"{hit_ratio * 100:.2f}%",
"of_total_pct": f"hits={hits:,} misses={misses:,}",
"__level": lvl,
})
return s
# ---------------------------------------------------------------------------
# 6) 最近 5 分钟失败查询 Top
# ---------------------------------------------------------------------------
def check_failed(client) -> Section:
s = Section("6) 最近 5 分钟失败查询 (system.query_log)")
rows = client.query("""
SELECT
count() AS cnt,
substring(any(exception), 1, 100) AS exception_sample,
substring(normalizeQuery(any(query)), 1, 100) AS query_sample
FROM system.query_log
WHERE event_time > now() - INTERVAL 5 MINUTE
AND type IN ('ExceptionBeforeStart', 'ExceptionWhileProcessing')
GROUP BY normalizeQueryHash(query)
ORDER BY cnt DESC
LIMIT 10
""").result_rows
if not rows:
s.notes.append("最近 5 分钟无失败查询, 健康.")
cols = ["cnt", "exception", "query_template"]
for r in rows:
d = dict(zip(cols, r))
d["__level"] = LEVEL_WARN if d["cnt"] >= 5 else LEVEL_OK
s.level = max(s.level, d["__level"])
s.rows.append(d)
return s
# ---------------------------------------------------------------------------
# 报告渲染
# ---------------------------------------------------------------------------
SYMBOL = {LEVEL_OK: "[OK]", LEVEL_WARN: "[WARN]", LEVEL_CRIT: "[CRIT]"}
def render_text(sections: List[Section]) -> str:
out = []
overall = max((s.level for s in sections), default=LEVEL_OK)
out.append("=" * 64)
out.append(f"ClickHouse 健康巡检报告 总体状态: {SYMBOL[overall]}")
out.append("=" * 64)
for s in sections:
out.append("")
out.append(f"## {SYMBOL[s.level]} {s.name}")
for n in s.notes:
out.append(f" - {n}")
if not s.rows:
continue
keys = [k for k in s.rows[0].keys() if not k.startswith("__")]
widths = {k: max(len(k), max(len(str(r.get(k, ""))) for r in s.rows))
for k in keys}
header = " " + " | ".join(k.ljust(widths[k]) for k in keys)
sep = " " + "-+-".join("-" * widths[k] for k in keys)
out.append(header)
out.append(sep)
for r in s.rows:
tag = SYMBOL[r.get("__level", LEVEL_OK)]
line = " " + " | ".join(
str(r.get(k, "")).ljust(widths[k]) for k in keys
)
out.append(f"{line} {tag}")
out.append("")
out.append("=" * 64)
return "\n".join(out)
def render_json(sections: List[Section]) -> str:
overall = max((s.level for s in sections), default=LEVEL_OK)
body = {
"overall": LEVEL_NAME[overall],
"sections": [
{
"name": s.name,
"level": LEVEL_NAME[s.level],
"notes": s.notes,
"rows": [
{**{k: v for k, v in r.items() if not k.startswith("__")},
"level": LEVEL_NAME[r.get("__level", LEVEL_OK)]}
for r in s.rows
],
} for s in sections
],
}
return json.dumps(body, ensure_ascii=False, indent=2)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="127.0.0.1")
ap.add_argument("--port", type=int, default=8123)
ap.add_argument("--user", default="default")
ap.add_argument("--password", default="")
ap.add_argument("--json", action="store_true",
help="以 JSON 格式输出, 适合喂给告警系统")
args = ap.parse_args()
client = get_client(args.host, args.port, args.user, args.password)
sections = [
check_replicas(client),
check_parts(client),
check_merges(client),
check_disks(client),
check_memory(client),
check_failed(client),
]
if args.json:
print(render_json(sections))
else:
print(render_text(sections))
overall = max((s.level for s in sections), default=LEVEL_OK)
sys.exit(overall)
if __name__ == "__main__":
main()