ClickHouse 的备份恢复,大概是很多团队从“能跑”到“敢挂”之间最容易被跳过的一道坎。我印象很深刻的一次事故:某个凌晨节点磁盘报错,同事第一反应是去执行备份脚本,结果发现上次成功全量备份已经是一个月前的存量,期间所有增量因为脚本里的路径写错,一直在静默失败。那种在故障现场硬着头皮恢复数据的感觉,经历过一次就再也不想经历。所以这篇东西我宁可写啰嗦一点,也要把 ClickHouse 备份与恢复当中那些容易踩、踩了又很难查的细节摊开讲清楚,尤其是版本更新后命令和行为的差异,足够新,也足够“保姆级”。
这篇文章适合谁?刚把 ClickHouse 从单机玩到集群、准备接生产的人;被领导要求“做个备份机制”但不知道从哪下手的运维开发;以及已经用过clickhouse-backup但遇到过恢复失败、对不上数据的人。我会尽量先从底层逻辑讲起,再进入完整实操,最后还附上我实际踩过的坑和排查思路,照着做基本能覆盖日常 90% 的场景。
1. 先搞懂数据落盘的底层逻辑,备份才不会抓瞎
1.1 MergeTree 家族在磁盘上到底长什么样
ClickHouse 最常用的表引擎是 MergeTree 系列,它的数据组织方式和 MySQL、PostgreSQL 那种“一个库一个目录、里面一堆文件”完全不同。默认数据目录是/var/lib/clickhouse/,往下依次是data/、{database}/、{table}/、{partition_id}/、{part_name}/。
列表展开大概是这个结构:
/var/lib/clickhouse/ ├── data/ │ └── default/ │ └── my_table/ │ ├── 20240101_20240101_1_1_0/ │ │ ├── id.bin │ │ ├── id.mrk2 │ │ ├── ts.bin │ │ ├── ts.mrk2 │ │ └── primary.idx │ └── 20240102_20240102_2_2_0/ └── shadow/每个 part 目录里放的是这一小块数据对应的列文件(.bin是压缩后的列数据)、标记文件(.mrk2)和主键索引文件(primary.idx)。ClickHouse 写数据是“先写内存缓冲区,再按 block 落盘生成一个不可变的 part”,后台再由后台 merge 线程把多个小 part 合并成大 part。
这个机制直接决定了备份策略的走向:如果你在业务正在写入的时候直接cp -r整个数据目录,拷贝过程中 ClickHouse 可能正在 merge、正在把内存中的数据刷成新 part,甚至正在把某些 part 标记为删除。你拿到的文件集合可能处于“某个 part 只拷了一半”的状态。MySQL 的物理备份还得先FLUSH TABLES WITH READ LOCK,ClickHouse 的可变 part 机制让简单拷目录更不可靠。
所以,理解 ClickHouse 备份的第一步不是背命令,而是理解它本质上是“一堆不可变的小文件 plus 后台的合并动作”。真正安全的备份必须建立在“文件系统级别的一致性”上,或者干脆绕开文件系统、走官方快照机制。
1.2 part 命名里藏着的全量/增量线索
如果你进到某个 MergeTree 表目录里,会看到类似这样的目录名:
ls /var/lib/clickhouse/data/default/my_table/ # 20240101_20240101_1_1_0 # 20240101_20240102_5_8_1这里20240101是 partition id,它来自表的分区键。比如按月分区,2024 年 1 月的分区 id 可能长这样;如果按toYYYYMMDD分区,那这个 id 就是日期。
后面四个数字是_minBlockNum_maxBlockNum_level:
minBlockNum/maxBlockNum:每次插入操作会从全局的 block 序号里取一段,这两个数字表示这个 part 涵盖的数据块范围。如果同一个分区下有多个 part,它们的块号区间一般不重叠。level:这个 part 经历了多少次 merge。新写入的 part level 是 0,两个 level 为 0 的 part 被 merge 后,新 part 的 level 就变成 1。
这些信息对备份恢复很有用:比如你在恢复时看到某个分区下有好几个 part,而不是一个完整的目录,这是正常的,说明这些 part 还没来得及被 merge。用ALTER TABLE ... ATTACH PARTITION恢复时,ClickHouse 自己会判断哪些 part 需要合并,你不用手工合并,但如果shadow快照里的 part 和表里已有 part 的maxBlockNum范围发生重叠,就可能报 “Part ... intersects previous part” 之类的错误。这个后面恢复篇会详细说。
1.3 副本表、分布式表和单机表的备份差异
表引擎不同,备份方式也要区分:
MergeTree:纯本地存储,备份它的数据目录或走 FREEZE / clickhouse-backup 都可以。ReplicatedMergeTree:本地也有数据,但元数据里的复制队列、zk 路径信息很关键。只拷贝数据文件、不带replicas元数据的话,新节点可能起不来,或者需要手工处理ReplicatedMergeTree的 zookeeper 路径冲突问题。Distributed:本身不存数据,只是一个逻辑层。备份它主要是备份表结构以及每个分片的本地表,恢复时重建所有分片上的本地表即可。
很多人以为 ClickHouse 有副本就可以不做备份,这是误解。副本解决的是“节点挂掉”的问题,不解决“有人执行了DROP TABLE,或误更新了全表数据”的问题。副本会把你的误操作同步复制到所有副本。ClickHouse 的备份,本质上还是要在“所有副本之外”留存一份可独立恢复的数据。
2. 备份方案选型:三种主流思路的取舍边界
2.1 为什么直接复制数据目录几乎必挂
理论上,如果把 ClickHouse 优雅停掉,让所有 part 都落盘并处于一致状态,再cp -r整个目录,然后再启动,这操作在单机场景偶尔是可行的。但在生产环境,特别是 7x24 写入的场景下,你不可能为了备份停库。
即便你不停库,在拷贝过程中也存在多个风险点:
- 拷贝到一半,后台 merge 把旧的 part 删了、把新 part 写出来了,目录列表在不同时间点看到的内容不一致。
- 正在写入的
.bin文件可能只写了一半,拷过去就是损坏的文件。 detached目录里可能有正在排障时丢进去的孤儿 part,拷过去之后 attach 时容易出问题。
所以我在团队里定了一条规矩:任何没有经过官方快照或专用工具的备份,都不算备份。cp -r /var/lib/clickhouse这种看似朴素的方案,在灾难恢复时会给你开一个大大的盲盒。
2.2ALTER TABLE ... FREEZE:官方自带快照机制的底层逻辑
ClickHouse 官方提供了一种内置的“备份姿势”,执行:
ALTER TABLE my_table FREEZE;这条 SQL 会把当前表里所有 part 在/var/lib/clickhouse/shadow/目录下建立硬链接。注意是硬链接,不是复制。因为 part 文件不可变,新写入的 part 不会改老 part,所以用硬链接做快照非常高效:快照瞬间完成,不占额外磁盘空间,只有当原始文件被清理、而快照里还引用着它时,磁盘空间才会真正被“释放”。
这个机制背后的原理是 inode 引用计数。shadow/N/目录下会生成和data/目录结构类似的层级,每个目录里就是指向原文件的硬链接。FREEZE 之后,你可以把shadow/里的某个表目录拷贝到其他机器上恢复。把 shadow 目录里的 part 移到目标表的detached目录,再执行:
ALTER TABLE my_table ATTACH PART '20240101_20240101_1_1_0';就能把数据挂回去。FREEZE 是理解后面所有备份工具的基础——很多工具本质上是帮你把“FREEZE + 复制 + 恢复”这套流程自动化。
2.3 clickhouse-backup 为什么成了社区事实标准
虽然官方有 FREEZE,但生产环境需要的显然不止是“把快照留在本地”。还需要远程存储、定时调度、增量备份、表级恢复、跨集群迁移,这些官方原生 SQL 一时半会做不利索,于是社区里出了clickhouse-backup这个工具。
它做的事情可以用一句话概括:备份时,先通过 ClickHouse 的系统表读取表结构、分区信息、part 列表,再把每个 part 目录上传到本地或远程存储(S3、GCS、SFTP 等),同时生成一份元数据 JSON;恢复时,根据元数据重建表结构,再把 part 目录拉回来放到对应位置。
和直接 FREEZE 相比,它解决了几个很实际的问题:
- 备份是“目录级一致性”的,工具内部会对一张表先做一次短暂的一致性快照,再开始上传,不会像手动 cp 那样拷到一半看到不一致状态。
- 原生支持增量备份,依赖 part 的不可变特性,只需要上传自上次备份以来新出现的 part。
- 支持表级恢复,可以只恢复某几个表,而不是全库。
- 有远程存储能力,备份文件可以传到 S3,机器烧了都不怕。
我用它做主力备份工具用了很长时间,整体稳定性是够的。不过要注意它的命令行参数在不同大版本之间有过不兼容的调整,尤其是 v1.x 和 v2.x 的差异,所以照着老教程执行clickhouse-backup create之前,最好先确认你装的版本对应的参数。
2.4 云盘快照 / 文件系统快照适合什么场景
除了 FREEZE 和 clickhouse-backup,还有一种思路是依赖底层基础设施做磁盘快照,比如:
- 云服务商提供的云盘快照(EBS snapshot / 云盘自动快照)
- 本地的 LVM 快照、ZFS 快照
- 虚拟机层面打快照
这些方案本质上是对整个数据目录做一次性文件系统一致性快照,通常要求你在快照前让 ClickHouse 处于一致性状态。ClickHouse 的文档也提到,如果在FREEZE之后再做文件系统快照,会是很稳妥的做法;但如果你直接对正在运行的实例打 LVM 快照,需要先执行SYSTEM FREEZE或者确保写入能够容忍。
我的判断标准是:
| 方案 | 适合场景 | 不适合场景 |
|---|---|---|
ALTER TABLE ... FREEZE | 临时备份、手动操作、部分表恢复 | 没有远程存储、全自动化需求 |
| clickhouse-backup | 日常全量/增量、S3 远程、表级恢复 | 一次性应急、想完全不引入外部工具 |
| 云盘快照 | 多表全量、整机恢复、不想装任何工具 | 细粒度表级恢复、误删数据精准找回 |
说实话,这三者不是互斥关系。我现在的生产环境是:每天凌晨clickhouse-backup做全量+增量推到 S3,同时对数据目录所在云盘开启每日快照作为第二重保险。两者可以同时存在,容灾时优先用 clickhouse-backup 做表级恢复,万一工具本身有问题,还有云盘快照兜底。
3. 保姆级实操:clickhouse-backup 的安装、配置与全量备份
3.1 下载二进制并确认版本匹配
clickhouse-backup是开源工具,最简单的方式是从它的 GitHub Releases 页面下载和你系统架构匹配的二进制,放到/usr/local/bin/下并加执行权限:
curl -L https://github.com/Altinity/clickhouse-backup/releases/download/v2.6.0/clickhouse-backup-linux-amd64.tar.gz -o ch-backup.tar.gz tar -xzf ch-backup.tar.gz mv clickhouse-backup /usr/local/bin/ chmod +x /usr/local/bin/clickhouse-backup clickhouse-backup --version这里有两件事必须确认:
- ClickHouse 版本不能太新或太旧到完全不兼容。clickhouse-backup 的 Release 页面一般会标明支持的 ClickHouse 版本范围,比如某些新版依赖系统表字段
system.parts的active状态,老版本 ClickHouse 可能缺字段,备份时就会报错。生产环境升级 ClickHouse 前,最好先看一眼当前使用版本的备份工具是否兼容。 - 配置文件路径。首次执行任意命令时,工具会提示没有配置文件,可以用
clickhouse-backup default-config把默认配置导出来,再慢慢改。不要手工凭记忆写配置。
3.2 细读 config.yml:这些字段最容易配错
运行下面的命令生成默认配置:
clickhouse-backup default-config > /etc/clickhouse-backup/config.yml打开之后,核心节点就几个,我直接给你一份注释过的最小可用配置:
general: remote_storage: s3 # 远程存储类型:s3 / gcs / local / none max_file_size: 1099511627776 # 单文件超过这个大小会尝试分片,默认 1TB allow_to_create_new_table_restore: true # 恢复时允许自动建表 clickhouse: host: 127.0.0.1 port: 9000 username: default # 建议用专用账号,给 SELECT、CREATE、ALTER 等权限 password: "your_password" secure: false # 如果开了 TLS,改成 true timeout: 30s backups: path: /var/lib/clickhouse-backup/backups # 备份在本地暂存的目录 restore_path: /var/lib/clickhouse-backup/restore s3: access_key: "AKIA..." secret_key: "xxxx" bucket: "my-clickhouse-backup" region: "ap-northeast-1" endpoint: "" # 如果用的不是 AWS S3 兼容对象存储,填对应的 endpoint配置里最容易踩的坑有三个:
remote_storage: none会让备份只保留在本地暂存目录,很多新手配完以为传到 S3 了,实际没有。要传远程必须显式指定 s3 或 gcs。username和password的权限不够时,备份过程会在读取系统表或执行FREEZE阶段就报权限错误,日志还比较隐晦。建议给备份账号SELECT、CREATE TEMPORARY TABLE、ALTER ... FREEZE权限,恢复时还需要建库建表权限。secure如果你连接的是clickhouse-client --secure对应的 9440 端口,这里不改成 true 会一直超时。
3.3 执行第一次全量备份并验证
配置完成后,先不要急着接定时任务,手动跑一次:
clickhouse-backup create my_first_backup如果省略备份名,工具会生成一个带时间戳的名字。执行完以后,用下面的命令确认备份状态:
clickhouse-backup list输出大致是:
my_first_backup 2024-01-01 03:00:00 2024-01-01 03:05:00 local 0 B如果你想直接看备份内容是否包含某张表,加一个参数查看明细:
clickhouse-backup list --all另外,去本地暂存目录看一眼:
ls -lh /var/lib/clickhouse-backup/backups/my_first_backup/ # metadata/ # shadow/metadata目录里是建表语句,shadow里是各表的数据 part。看到这个结构,基本上第一次全量备份就成了。
我建议你备份完成后再做一个动作,把备份文件大小和源库数据量对比一下,确认量级合理。别等到要恢复才发现备份脚本根本没在跑或只备份了系统库。
3.4 增量备份、表级备份与保留策略
clickhouse-backup 增量备份的实现思路是:新 part 具有不可变性,所以增量备份只需要找出“上次备份之后新增的 part”上传即可。命令行参数一般是:
clickhouse-backup create --diff-from my_first_backup incremental_after_first它会把my_first_backup之后新增的 part 作为增量内容保存。对于每天都跑定时任务的场景,我习惯这样设计:
# 周一 03:00 全量 0 3 * * 1 /usr/local/bin/clickhouse-backup create --tables "default.*" full_$(date +\%F) >> /var/log/clickhouse-backup.log 2>&1 # 其余每天 03:00 增量 0 3 * * 2-7 /usr/local/bin/clickhouse-backup create --diff-from latest --tables "default.*" inc_$(date +\%F) >> /var/log/clickhouse-backup.log 2>&1注意,--tables "default.*"可以限定只备份某个库或某几张表,如果库很多,排除掉不重要的日志表能显著降低备份耗时。但引号里的通配符规则要提前在测试环境验证,否则很容易出现“备份成功但内容是空的”。
保留策略上,我一般是保留最近 7 个全量、30 个增量,更老的就删除。删除备份文件直接用:
clickhouse-backup delete local old_backup_name如果远程存储也传了,加--remote参数删除远端文件。这里有个经验:不要手工去删 S3 桶里的对象目录,否则元数据列表里会出现“备份不存在但占着名额”的状态,后患无穷。
4. 恢复链路实操:单表恢复、分区恢复与跨集群迁移
4.1 恢复前必须明确的三件事
很多人跑到恢复环节心态是“赶紧执行 restore 把数据弄回来”,但我在生产环境处理过太多次恢复事故,必须严肃地提三个问题:
- 要恢复的是整机、整库、单表,还是只是某个分区?需求不同,命令和参数完全不同。
- 恢复到原集群还是新集群?如果原集群的节点还活着只是数据被误删,可以直接 restore;如果是重建环境,表结构里如果带
ReplicatedMergeTree,zookeeper 路径、副本标识都要处理。 - 目标表当前是否已有数据?如果表里已经有数据,盲恢复可能导致 part 冲突。clickhouse-backup 的处理策略是:恢复时对已存在的表默认不会覆盖,要么在 restore 参数里加
--rm先删掉目标表,要么手工清理旧 part。
4.2 用 clickhouse-backup 恢复单表的完整过程
假设我们误删了default.orders这张表,备份名叫full_2024-01-01,目标是把这张表恢复到当前集群。先确认备份内容里包括这张表:
clickhouse-backup list --all # full_2024-01-01 ... default.orders default.users ...然后执行表级恢复:
clickhouse-backup restore full_2024-01-01 --tables "default.orders"如果业务涌入、原表还在,并且你确定要用备份数据覆盖它,可以在恢复前这样操作:
clickhouse-backup restore full_2024-01-01 --tables "default.orders" --rm--rm会在恢复前删除目标表并重建,这非常危险,但确实有效。我建议在命令执行前,再确认一次环境变量或者备份名,避免把“测试集群”恢复成“生产集群”。
恢复过程中会依次做:建表(如果需要)→ 下载 part 到restore_path→ 把 part 移动到detached→ 执行ALTER TABLE ... ATTACH PART。完成后,立刻验证数据量和最新分区时间:
SELECT count(), max(ts) FROM default.orders;如果数据量和误删前对得上,恢复链路就闭环了。
4.3 FREEZE 快照 + ATTACH PARTITION 手动恢复法
有些场景你手上没有 clickhouse-backup,只有shadow快照目录,比如你自己执行过ALTER TABLE ... FREEZE,或者从另一台机器拷贝过来一份 shadow 目录。这时候可以手动恢复。
假设shadow/1/data/default/orders下有多个 part 目录,目标表已经建好。把 part 目录放到目标表的detached目录:
cp -r /path/to/shadow/1/data/default/orders/20240101_20240101_1_1_0 /var/lib/clickhouse/data/default/orders/detached/ chown -R clickhouse:clickhouse /var/lib/clickhouse/data/default/orders/detached/然后在 ClickHouse 客户端执行:
ALTER TABLE default.orders ATTACH PART '20240101_20240101_1_1_0';如果detached目录下有很多 part,你不想一个个写 SQL,可以执行:
ALTER TABLE default.orders ATTACH PARTITION '20240101';这会把该分区下所有 detached 的 part 一次性挂载回去。注意partition id要写对,它不一定是toYYYYMMDD的结果,建议用SELECT partition_id, name FROM system.parts WHERE table='orders'查一下。
为什么要把 part 先丢到detached而不是直接放回data目录?因为detached目录是 ClickHouse 专门为“手工导入/排除”设计的中转区,服务启动时不会自动加载它,只有你显式ATTACH之后才会识别。直接放回 data 目录很可能导致服务启动时加载到不完整 part,或者 part 校验失败,触发异常。
4.4 跨集群迁移:macros、集群名和 ZooKeeper 路径的处理
跨集群迁移是恢复场景里最容易“卡壳”的领域。比如从旧集群的备份恢复到新集群,如果表是ReplicatedMergeTree,那么建表语句里通常包含这样的参数:
ENGINE = ReplicatedMergeTree('/clickhouse/tables/{shard}/orders', '{replica}'){shard}和{replica}是从每个节点的macros配置里读取的。新集群的宏可能和老集群不一致,直接恢复会导致两个副本写了同一个 zookeeper 路径,或者因为 replica 名称冲突而同步混乱。
我在跨集群恢复时的处理顺序:
- 备份和元数据拷贝到新集群。
- 在配置文件的
<macros>里,保证新集群各节点的shard、replica定义与旧集群一致,或者在恢复前手动改掉建表语句里的 zookeeper 路径。 - 用
clickhouse-backup restore时如果报 zookeeper 相关错误,优先检查system.replicas里表的zookeeper_path和replica_name。 - 恢复完成后,执行
SYSTEM RESTORE REPLICA或者SYSTEM SYNC REPLICA,确保副本队列能正常追上。
简单说,跨集群恢复不只是在“拉数据”,表引擎的分布式元数据也要跟着匹配。否则你看到数据在,但副本之间互相不认,问题比丢数据还难排。
5. 自动化、监控与备份验证:别让故障日变成“开盲盒”
5.1 定时任务与日志如何设计
定时备份我前面给过 crontab 的例子,但真正生产级要考虑的不只是“命令跑没跑”,还有:
- 日志要有时间戳、备份名、耗时、备份大小。
- 如果备份失败,要能告警,而不是等第二天人工看 log。
- 备份产生的本地临时文件要定期清理,否则磁盘会被
/var/lib/clickhouse-backup/backups占满。
我自己用的脚本结构大概是这样:
#!/usr/bin/env bash set -euo pipefail LOG_FILE="/var/log/clickhouse-backup/backup_$(date +\%F).log" BK_NAME="full_$(date +\%F)" /usr/local/bin/clickhouse-backup create "$BK_NAME" >> "$LOG_FILE" 2>&1 || { echo "backup failed at $(date)" >> "$LOG_FILE" curl -fsS -m 10 "https://your-monitor.example.com/api/v1/alert?msg=clickhouse_backup_failed" || true exit 1 } # 上传到远程存储 /usr/local/bin/clickhouse-backup upload "$BK_NAME" --remote >> "$LOG_FILE" 2>&1 # 清理 7 天前的本地备份 /usr/local/bin/clickhouse-backup delete local "$BK_NAME_OLD" --dry-run >> "$LOG_FILE" 2>&1告警这步很重要,它把“备份任务失败”从静默问题变成主动暴露问题。不要等故障当天才发现脚本已经坏了二十天。
5.2 备份可还原性的日常巡检
备份不等于可恢复。我见过太多环境,备份目录存在,但从来没恢复过,等真要恢复时才发现某个表被跳过了、某个分区没有数据。
日常巡检我建议做三件事:
- 至少每周挑一个备份,在临时环境执行一次
restore,不需要恢复全量,挑一个数据量中等的表验证。 - 用
clickhouse-backup list --all检查备份里表和实际表数量是否一致。 - 如果配置了远程存储,每两周做一次
clickhouse-backup upload后的文件抽查,到 S3 桶里看对象数量和大小是否和本地备份一致。
有些团队会用“恢复演练”代替“备份监控”,效果最好但成本也高。折中下来,至少做到“每周真实 restore 一张表”。这个动作在关键时刻救命的概率极大。
5.3 定期演练的建议
说个真实案例。某次我们需要从 S3 恢复一个 30 节点的 ClickHouse 集群到新机房,演练之前以为备份很完整,结果发现老集群部分表早就被重命名过,备份的元数据里还是旧表名,恢复后业务头上顶着“表不存在”的报错。这个问题的根因相当简单:几个月前一次建表变更没有同步更新备份元数据,或者说备份任务对“重命名表”的感知太弱。
后来我把恢复演练固定成季度一次,并且明确演练内容不只是“数据能查”,还包括:
- 新机房能不能靠备份独立拉起一个可用集群。
- 业务查询所需的
Distributed表和Dictionary是否也恢复好了。 - 权限账号、物化视图、
Kafka/RabbitMQ引擎表是否要单独处理。
演练发现的问题越多,生产事故越少,这个账值得算。
6. 高频踩坑实录:问题现象、排查链路与根因
6.1 备份目录比源库小很多,正常吗
如果你发现备份目录明显小于你预估的数据量,先不要慌,看看是不是以下原因:
- ClickHouse 的
system.parts里有大量active=0的旧 part,它们也占磁盘,但备份工具默认只备份 active part,所以备份会小于du -sh /var/lib/clickhouse/data的统计值。 - 备份工具默认会跳过
detached目录,如果大量数据被人为移到 detached,备份大小会“异常偏小”。
遇到这种情况,我的排查顺序是:先看备份日志里统计的表和 part 数,再看备份目录里各表的大小,最后去源库执行:
SELECT table, count() AS part_count, sum(bytes_on_disk) AS total_bytes FROM system.parts WHERE active GROUP BY table;如果这两边对得上,就没问题。问题是有些新手看到备份目录小,就以为备份失败,然后误删了备份目录,反而丢了数据。所以“先判断大小差异合不合理”比盲目采取措施更重要。
6.2 restore 提示 Schema not found 之类错误
clickhouse-backup恢复时报schema not found或表不存在,常见原因有三个:
- 备份时该表还没有建,或者备份元数据里没有该表。
--tables参数过滤了表名,恢复时没包含进去。- 备份里表名和当前环境里表名数据库名不匹配。
排查方法是先解压或查看备份元数据中的metadata/default/orders.sql,确认建表语句存在;然后用clickhouse-backup restore 备份名 --tables "default.orders" -v输出详细日志,看卡在哪一步。这类问题大多是备份和恢复命令参数的匹配问题,一般不涉及数据损坏。
6.3 备份长时间卡在某个表
备份卡住的现象往往是日志停在那张表,表现为 part 数量多、单个 part 文件特别大、或者备份账号权限不足导致查询system.parts一直等待。还有一个容易被忽略的原因:CLickHouse 本身有大量ReplicatedMergeTree表待同步任务,执行FREEZE时因为 zookeeper 连接不稳定或复制队列积压,导致操作无法快速完成。
我的处理方式:先找到卡住的具体表,用SELECT * FROM system.processes;看当前是否有长时间运行的 DDL 或 SELECT,必要时杀掉这个查询进程,再重新执行备份。如果多次卡住,检查 zookeeper 的延迟和节点间网络,这往往是根因所在。
6.4 恢复后数据有偏差,先查这几个位置
恢复完成后数据量和源库对不上,先别怀疑工具,按顺序检查:
system.parts里active状态。如果恢复过程有 part 被标记为outdated,它可能不会出现在查询里,但数据其实还在磁盘上。- 分区键是否一致。例如备份时表用的
toYYYYMMDD(ts)分区,恢复后建表语句如果被改动过,分区划分可能不同。 - 有没有遗漏
Distributed表。只恢复了本地表,没恢复分布式表,业务查到的数据自然“变少”了。 max_ts或者最新分区是否缺失,先把数据和备份列表里的时间范围对齐,再谈其他问题。
最后再分享一个细节:每次恢复完成,我都会在目标表上执行一遍OPTIMIZE TABLE ... FINAL,虽然这会触发一次大 merge、耗时长,但它能把分区内的 part 整理干净,也会顺带清理掉一些异常状态。这个操作在列式存储里并不便宜,但相比数据完整性来说,这笔时间开销是值得的。