ClickHouse 的备份与恢复,一直都是踩坑重灾区。别看它查询快得离谱,真要到了数据误删、节点损坏、机房故障这种节骨眼,没提前把备份体系设计好,分分钟就是事故现场。这两年我在生产环境里从最土的cp -r一路做到基于 clickhouse-backup 的全量加增量体系,中间踩过的坑、趟过的雷,足够写一本小册子了。这篇文章就结合最新版 ClickHouse(23.x/24.x 时代)把备份恢复这件事彻底讲透,从底层原理到工具选型,从全量备份到自动恢复演练,尽量做到保姆级。
文章不会只贴命令,还会告诉你每个操作背后的动机,比如 ClickHouse 的 part 命名规则为什么和备份强相关,FREEZE为什么要配合硬链接使用,clickhouse-backup的增量备份到底靠什么判断变化,以及恢复的时候为什么不能只把文件 copy 回去。这些坑我刚入门时全部踩过,希望你能一步跨过去。
1. 备份前的三个关键认知
在做任何备份方案选型之前,我强烈建议你先花点时间确认自己真的理解 ClickHouse 的数据存储结构。这个理解不到位,后面不管是 FREEZE 还是 clickhouse-backup,你都只是“照着命令抄”,出了问题完全不知道从哪里排查。这章我们不讲废话,直接讲三个和备份恢复强相关的底层概念。
1.1 搞清楚 part 到底是什么
ClickHouse 是列式存储,数据表在物理层面由一个个目录组成。每次INSERT进来的数据,都会在分区目录下形成一个不可变的数据片段(data part),后台线程再在合适时机把这些小 part 合并成大 part。关键就在这:合并是异步的、不确定的,而且每个 part 都有自己的生命周期。
part 目录的命名规则非常有讲究,格式是:
{partition_id}_{minBlockNum}_{maxBlockNum}_{level}举例说明,某个表的分区是202401,那么可能的 part 目录名就是202401_1_3_1。含义如下:
202401:分区值,表示这坨数据属于 2024 年 1 月。1:最小块编号,表示这部分数据在全局写入块序列中的起始编号。3:最大块编号,一般自增。1:合并层级,每次合并这个数字就加一。
为什么说它和备份强相关?因为 ClickHouse 的备份最小单元基本就是 part。你在做增量备份时,工具要判断“哪些数据是新增的”,靠的就是检查自上次备份之后新出现了哪些 part 目录。另外恢复的时候,如果只覆盖文件但没处理detached目录,或者 part 目录编号冲突,都会导致恢复后的表数据异常。所以请务必记住:part 目录名既代表数据范围,也代表合并代数,是备份系统判断数据变化的核心依据。
1.2 副本和副本节点要分开看待有很多人问:我的 ClickHouse 已经用了ReplicatedMergeTree,还有三个副本节点,是不是就不需要备份了?这是个很经典的误区。
副本(replica)解决的是高可用问题,它通过 ZooKeeper 或 ClickHouse Keeper 来同步元数据和数据 part,保证某个节点挂了,查询流量可以自动切到别的副本。但副本同步有一个致命盲区:如果你执行了TRUNCATE TABLE、DROP TABLE,或者某个 SQL 把一批数据DELETE了,那么这些操作会通过分布式 DDL 同步到所有副本上。你拥有 3 个副本,等于把错误完整地复制了 3 份,最后还是要靠备份来兜底。
所以正确的认知是:
- 副本负责“高可用”,保证单节点故障时业务不中断。
- 备份负责“可恢复”,保证逻辑错误、误删、恶意操作后能把数据找回来。
- 两者是不能互相替代的,生产环境必须同时具备。
1.3 备份策略的核心是 RPO 和 RTO
聊备份策略之前,先统一两个概念:
- RPO(Recovery Point Objective):你能容忍丢失多长时间的数据。比如每小时备份一次,最坏情况可能丢 1 小时数据。
- RTO(Recovery Time Objective):你从故障发生到业务恢复,需要多长时间。
ClickHouse 不像 MySQL 那样可以靠 binlog 实现分钟级的精确回放,因为它的 merge tree 结构决定了常规备份做不到“任意时间点恢复”。我们需要在自己的场景里做一个权衡。以我负责的业务为例:日志分析类表,一天大概 20 亿行,用户能接受丢 30 分钟的数据,那我就设定每 30 分钟做一次增量备份、每天凌晨做一次全量备份。如果是金融类对账表,那就要考虑每次导入后立即触发快照,或者用官方备份语句做到更细粒度。
这个权衡不是技术问题,是产品问题。所以在设计备份频率之前,先和业务方对齐这两个参数。
2. 备份方案选型:从最暴力到最优雅
ClickHouse 的备份方案五花八门,但归根结底就是四种思路:文件系统快照、手工 FREEZE、备份工具、官方备份 SQL。我按推荐程度从低到高讲一遍,并说说每种方案的适用边界。
2.1 方案一:直接拷贝数据目录
最先要吐槽的就是直接cp -r数据目录。很多人第一次接触 ClickHouse,想当然觉得停掉服务,把整个/var/lib/clickhouse拷走,恢复时再放回去就行。这个方案在小数据量、单机、低写入场景能成功,但存在很大的隐患:
- ClickHouse 在写 part 时并不是原子性的,先写临时目录再 rename 到目标目录。如果拷贝时机没卡好,可能拷到一半状态的目录。
- 对于分布式表,
system.*表和元数据都分散在多个节点,手动拷贝很容易漏。 - 拷贝期间要停服或者保证数据静止,对在线服务不现实。
所以这个方案我只建议在虚拟机快照场景使用,比如测试环境临时搭一个仿生产库。
2.2 方案二:文件系统快照(LVM / 云盘快照)
云原生时代,很多团队选择直接给 ClickHouse 的数据盘做云厂商快照,或者用 LVM 做逻辑卷快照。这个方案的好处是备份对 ClickHouse 完全透明,不用停服,秒级生成快照,恢复的时候也能快速挂载。
但快照方案有它的代价:
- 它是整机/整盘级别的,没办法精细到某个表、某个分区。
- 快照是和基础设施绑定的,一旦更换云厂商,恢复流程可能完全失效。
- 快照文件的管理和保留策略往往不是 ClickHouse 团队自己控制的,出现问题不好排查。
所以如果你在用云盘快照,建议把它作为“最后一道防线”,而不是唯一方案。云盘快照适合兜底灾难恢复,不适合做日常逻辑错误的快速回滚。
2.3 方案三:ALTER TABLE ... FREEZE 手工快照
这是 ClickHouse 官方支持的备份方式。原理是给当前所有活跃 part 创建硬链接,放到一个独立的备份目录里。硬链接不复制数据块,所以备份速度极快,几乎不占额外空间(只是在文件系统层面多了一个目录项)。
基础操作三步走:
# 1. 触发全备份快照 ALTER TABLE db.table FREEZE; # 2. 备份文件生成在 shadow 目录 ls /var/lib/clickhouse/shadow/ # 3. 把 shadow 目录里的内容拷贝到远程存储恢复时,只需要把 shadow 目录里对应路径的 part 文件放回/var/lib/clickhouse/data/{database}/{table}/detached/,然后执行:
ALTER TABLE db.table ATTACH PART 'part_name';但这里有几个巨坑:
- FREEZE 的备份只是 part 的一个“快照引索”,它依赖原始 part 文件还存在。如果你在备份之后做了大量合并,原始 part 被清理,那 shadow 里的硬链接仍然指向数据块本身,所以数据还在;但如果你清理了 shadow 目录,备份就没了。
- 手工方式做增量很麻烦,因为没有内置“自上次备份后新增了哪些 part”的能力。
- 恢复时要一个一个 ATTACH,表多了不现实。
所以 FREEZE 适合“临时手动备份”和“单表快速救援”,但作为长期系统化备份方案还是差点意思。
2.4 方案四:clickhouse-backup 工具
这是目前社区里最主流的开源备份工具,项目地址在 GitHub 上搜AlexAkulov/clickhouse-backup就能找到。它做的事情本质上是“把 FREEZE 的高级操作封装成了可配置、可自动化、支持增量、支持远程存储的完整工具”。
我为什么推荐它?因为它在生产环境里被验证过很多年,特性足够实用:
- 支持全量备份、增量备份、单表/多表备份。
- 支持本地目录、S3、MinIO、GCS、COS 等多种存储。
- 备份内容不仅包含数据 part,还包含表结构、元数据、权限相关配置。
- 支持
cron定时调度,方便集成到监控体系里。
2.5 方案五:官方 BACKUP 语法
如果你用的是 ClickHouse 23.8 以上版本,可以关注一下官方自身的BACKUP语法。它的基本用法是:
BACKUP TABLE db.table TO Disk('backup_disk', 'backup_name'); RESTORE TABLE db.table FROM Disk('backup_disk', 'backup_name');官方备份支持全量、增量(SETTINGS base_backup = ...),也支持上传到 S3。它的优点是语法原生、不做额外进程,缺点是版本迭代快,不同小版本的稳定性和参数差异比较大。如果你们公司版本更新激进,我建议先用 clickhouse-backup 或者 FREEZE 作为主线,等官方备份语法在你们版本上验证充分后再切换。
3. 保姆级实操:用 clickhouse-backup 完成全量与增量备份
这一章是硬核内容,我尽量把每一步都写清楚,包括为什么要这么做。我们假设一套典型环境:ClickHouse 部署在/var/lib/clickhouse,远程备份存储用 MinIO(兼容 S3 的对象存储),业务表是分布式表,每天数据量约 1TB。环境可能不完全一样,但思路是通用的。
3.1 安装与配置
首先是下载工具。到 GitHub Releases 页面找到对应系统架构的二进制包,比如clickhouse-backup-linux-amd64.tar.gz。解压后把二进制放到/usr/local/bin,并赋予执行权限。
验证安装:
clickhouse-backup --version然后生成默认配置文件:
clickhouse-backup init默认配置文件会生成在/etc/clickhouse-backup/config.yml。你需要重点关注的配置块有:
general: remote_storage: s3 max_file_size: 107374182400 # 100GB,超过则自动分片 clickhouse: host: localhost port: 9000 username: backup_user password: "your_password" secure: false s3: endpoint: http://minio.example.com:9000 bucket: clickhouse-backup region: us-east-1 access_key: "minio_access_key" secret_key: "minio_secret_key" force_path_style: true这里有几个容易出错的地方:
- 如果你用的是阿里云 OSS 或腾讯云 COS,endpoint 可能要带
https://,且force_path_style可能要设成 true 或 false 根据各厂商规定来。 username建议单独建一个备份专用账号,不要直接用 default 超管。权限至少需要SELECT、ALTER TABLE FREEZE、SHOW TABLES等。max_file_size很关键,如果单个 part 超大,不上传时容易 timeout。
3.2 全量备份实战
全量备份命令:
clickhouse-backup create full_backup_$(date +%Y%m%d%H%M)就这么简单?但我们要理解它背后的执行流程,方便排查问题:
- 工具连接 ClickHouse,获取所有需要备份的库表列表。
- 对每张表执行
ALTER TABLE ... FREEZE,生成硬链接快照。 - 读取 shadow 目录中新增的 part 列表。
- 将元数据和数据文件打包上传到 S3/MinIO。
- 上传完成后清理本地 shadow 临时文件。
如果你只想备份单张表或某些表,可以用:
clickhouse-backup create --tables "db1.table1,db2.table2" my_backup_name全量备份完成后,查看备份列表:
clickhouse-backup list输出类似:
my_backup_20240101_0001 2024-01-01 00:01:10 full local, s3local表示本地也保留了一份,s3表示已经上传到对象存储。具体保留策略由配置文件里的retention控制。
3.3 增量备份与定时任务
增量备份命令更简单:
clickhouse-backup create --base backup_name incremental_name注意,--base参数必须指定一个全量备份作为基准。工具会去比较当前数据目录中的 part 列表和 base 备份中记录的 part 列表,只备份新增的 part 或发生变化的 part。这个机制看起来很聪明,但实际使用中要注意一点:如果某张表的 part 因为合并被重写了,旧 part 虽然被清理,但增量备份识别这种替换的依据是“part 名变了”。所以只要系统的合并还在发生,增量备份就会不断重复上传那些被合并过的大 part,这很正常,也正是 ClickHouse 这类 LSMTree 架构无法做到像 MySQL binlog 那样轻量级增量的原因。
因此,我建议的备份节奏是:
- 每天凌晨 1 点做一次全量备份。
- 每 30 分钟做一次增量备份,基准是当天的全量备份。
- 所有备份保留 7 天,超过 7 天的定期清理。
在 crontab 里加两条:
0 1 * * * /usr/local/bin/clickhouse-backup create --tables "db.*" daily_full_$(date +\%Y\%m\%d) >> /var/log/clickhouse-backup.log 2>&1 */30 * * * * /usr/local/bin/clickhouse-backup create --base daily_full_$(date +\%Y\%m\%d) incremental_$(date +\%Y\%m\%d_\%H\%M) >> /var/log/clickhouse-backup.log 2>&1注意 crontab 里%必须转义成\%,这是个老坑,别问我怎么知道的。
3.4 恢复演练:如何验证备份真的可用
备份做得再漂亮,不演练等于没有备份。这是所有做数据平台的人都要牢记的一句话。我见过太多团队,备份脚本跑了半年,结果真到恢复那一刻,发现 S3 的 policy 配错了,或者某个元数据文件损坏,根本恢复不出来。
clickhouse-backup 的恢复分两步:
第一步,从远程存储拉取备份到本地:
clickhouse-backup restore --rm --schema-only my_backup_name--rm是删除本地已有的同名表,--schema-only是只恢复表结构不恢复数据。这一步通常先做,确保建表语句和元数据没问题。
第二步,恢复数据:
clickhouse-backup restore --data-only my_backup_name如果你希望一次恢复到位,也可以直接:
clickhouse-backup restore my_backup_name恢复完成后,我用下面这套 SQL 做校验:
-- 1. 对比表数量 SELECT count() FROM system.tables WHERE database = 'dbname'; -- 2. 对比分区数量 SELECT partition, count() FROM system.parts WHERE table='mytable' GROUP BY partition; -- 3. 抽查表行数 SELECT count() FROM dbname.mytable;如果之前的备份是来自生产,恢复后最好再抽样跑几条汇总 SQL 对比一下数值。这样才算一次完整的恢复演练。
4. 常见问题与排查技巧实录
再回到实操里,我把自己在生产环境中真实遇到过的坑整理成备忘录,每一条都是用失联/告警/加班换来的。
4.1 part 丢失和校验失败
有一次增量备份报错:
ERROR: part 202401_10_20_1 not found in shadow dir排查过程:先看system.parts里的 part 状态,发现这个 part 已经被后台合并掉了,所以不在 shadow 里。但 clickhouse-backup 是在寻找“当前活跃的 part”,理论上不应该出现这种错误。后来发现问题出在备份命令启动时,FREEZE 和读取列表之间存在一点点时间差,在这个间隙里,某个 part 被后台合并,FREEZE 创建的硬链接指向的旧 part 已经在本地被清理了。
解决方法是调整 ClickHouse 的 merge 参数,降低合并频率,或者在备份期间临时设置:
SYSTEM STOP MERGES; -- 执行备份 SYSTEM START MERGES;生产环境如果要保证备份一致性,这是我强烈推荐的操作。当然,备份前STOP MERGES也要谨慎,如果备份时间太长,会影响合并进度,进而影响查询性能。折中方案是只对需要备份的表STOP MERGES,而不用全局停。
4.2 FREEZE 太占磁盘空间
FREEZE 本质是硬链接,理论上零 COPY 成本。但有些文件系统(或者某些 Docker 存储驱动)并不支持真正的硬链接,导致 FREEZE 变成了全量复制。另外,如果 part 文件被合并后 old part 被物理删除,硬链接会阻止空间释放,等于把原本要清理的空间“钉住”了。
我的实践心得:
- 数据目录不要放在 Docker 的 overlay 文件系统上。否则硬链接行为不可控。
- 使用 ext4 或 xfs,并提前规划好 shadow 目录和主数据目录在同一文件系统,因为硬链接不能跨文件系统。
- 备份完成后尽快清理 shadow 目录,使用工具自带的清理能力,不要手动
rm -rf部分文件,否则可能破坏后续增量的对比逻辑。
4.3 恢复后表结构对不上
恢复时最容易翻车的是表结构。备份工具会保存表结构,但如果你在备份之后改过表结构,例如加了字段、改过 TTL、调整过索引,恢复时工具可能默认恢复最原始的表结构,然后你会发现原本应该有数据的列全没了。
解决方案:
- 在备份配置里开启
restore_schema: true,让工具总是恢复备份时的表结构。 - 恢复前先规划好是否需要把表结构恢复到某个特定时间点,如果不需要,恢复后立即执行
ALTER TABLE ... ADD COLUMN补齐缺失字段。 - 用
--schema-only先恢复结构,然后人工对比前后 DDL 差异,再恢复数据。这样能规避很多让人头秃的隐性问题。
4.4 跨版本恢复的兼容性问题
ClickHouse 版本迭代快,元数据和数据格式会变。有一次我在测试环境从 22.8 备份,恢复到 23.3 的集群,结果部分表能正常查询,部分表报Cannot parse错误。查了半天发现是ALTER TABLE ... MODIFY COLUMN产生的类型变更没有完整备份进去,导致列类型不匹配。
从此以后我的铁律是:
- 备份恢复尽量保持大版本一致。不要在 22.x 和 24.x 之间直接跨大版本恢复数据。
- 如果确实要跨版本,先用
clickhouse-backup create --tables "db.*" --schema-only把表结构恢复出来,再人工对比系统表的类型定义,确认无误后再恢复数据。 - 不放心的话,可以先恢复到中间版本,再用
INSERT INTO SELECT导数据到目标集群。
4.5 备份文件损坏怎么预防
备份文件放在对象存储上也不是 100% 安全。我见过某云厂商的对象存储偶发返回403或数据损坏错误。为了预防,建议在备份配置中开启校验:
general: verify: trueverify会在上传完成后对备份文件重新做一次校验,及时发现损坏并重传。另外,定期做一次“从远端 S3 拉取备份并恢复到一个新表”的演练,比任何理论分析都有效。
| 常见问题 | 可能原因 | 快速排查方法 |
|---|---|---|
| Backup list 看不到备份 | S3 配置/bucket 权限问题 | clickhouse-backup list前先mc ls验证连通性 |
| Incremental 备份总是全量大小 | 表格参与合并频繁,part 名不断变化 | 调整 merge 参数,降低合并频率 |
| Restore 很慢 | part 文件多、网络带宽受限 | 调整并发上传/下载参数 |
| 恢复后 count 对不上 | 有dettach的孤儿分区 | 检查detached目录,手动清理或 ATTACH |
| 备份进程卡死 | ClickHouse 的system.mutations长时间运行 | 查看system.mutations,等待完成或手动 KILL |
5. 生产环境自动化备份体系参考模板
备份不只是跑个 cron 脚本就完了,一个可落地的体系还应该包含监控、告警、日志留存和定期演练。我把自己正在用的模板简化后贴出来,希望能帮大家减少设计的成本。
5.1 备份脚本参考
#!/bin/bash # clickhouse_backup_helper.sh set -euo pipefail BACKUP_BASE_DIR="/data/clickhouse_backup" S3_BUCKET="s3://clickhouse-backup-prod" USER="backup_user" PASS="xxx" LOG_FILE="/var/log/clickhouse-backup-wrapper.log" export CLICKHOUSE_BACKUP_LOG_LEVEL=info export CLICKHOUSE_BACKUP_CONFIG="/etc/clickhouse-backup/config.yml" ts() { date '+%Y-%m-%d %H:%M:%S'; } log() { echo "$(ts) $*" >> "$LOG_FILE"; } # 1. 全量备份 FULL_NAME="daily_full_$(date +%Y%m%d)" log "start full backup: $FULL_NAME" if clickhouse-backup create --tables "db.*" "$FULL_NAME"; then log "full backup ok" else log "full backup failed" # 这里把消息推送到钉钉/企微/webhook curl -s -X POST "$ALERT_WEBHOOK" -d '{"msg":"full backup failed"}' || true exit 1 fi # 2. 按保留策略清理本地和远端备份 clickhouse-backup delete local --older-than 7d || true clickhouse-backup delete remote --older-than 7d || true # 3. 增量备份由单独 cron 调用,逻辑类似,此处省略5.2 监控与告警的结合
光有脚本还不够,你需要确保“备份失败”这件事能在 5 分钟内被人看见。比较实用的方案是:利用clickhouse-backup list的 JSON 输出,接入 Prometheus + Alertmanager。
简单的方式是把备份日志中的ok和failed统计成指标:
clickhouse-backup list --json | jq 'map(select(.name | contains("daily_full"))) | length'如果当天的全量备份不存在,就触发告警。虽然粗暴,但非常好用。
另外要强调演练机制。我建议每季度做一次完整恢复演练,并且把演练结果报告化,发给相关的负责人看。为什么要这么做?因为恢复流程里的很多坑,只有在你真的执行恢复时才会暴露出来。演练不是给审计看的,是给自己救命的。
6. 一些有心得的收尾内容
ClickHouse 的备份恢复之所以让很多人头疼,本质原因是它底层的数据组织方式和我们熟悉的 MySQL/PostgreSQL 不一样。它没有传统意义上的 WAL 回放能力,任何备份方案都必须围绕“part 不可变 + 后台合并 + 硬链接快照”这套机制来设计。理解了这套机制,你再看任何工具都会觉得非常通透。
个人经验里最想强调的还是这几条:备份账号别用超级管理员的明文密码;备份目录单独挂盘,别和数据目录混在一起;每次恢复演练后把输出结果保存下来,下次对比差异。这些细节看着不起眼,但关键时刻都能救命。
如果你刚开始接触 ClickHouse 备份,从clickhouse-backup入手是最平滑的路径。先用全量备份解决“有或无”的问题,再逐步引入增量、远程存储、自动化告警,最后做到可以随时拉起一个新集群并恢复最近半小时的数据。
希望这篇文章对你有用,也希望大家永远都用不上恢复按钮,但需要它的时候,它能稳稳接住。