☰
ClickHouse备份恢复实战:从part机制到clickhouse-backup工具
2026/10/1 4:27:43 网站建设 项目流程

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)

就这么简单?但我们要理解它背后的执行流程,方便排查问题:

  1. 工具连接 ClickHouse,获取所有需要备份的库表列表。
  2. 对每张表执行ALTER TABLE ... FREEZE,生成硬链接快照。
  3. 读取 shadow 目录中新增的 part 列表。
  4. 将元数据和数据文件打包上传到 S3/MinIO。
  5. 上传完成后清理本地 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, s3

local表示本地也保留了一份,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、调整过索引,恢复时工具可能默认恢复最原始的表结构,然后你会发现原本应该有数据的列全没了。

解决方案:

  1. 在备份配置里开启restore_schema: true,让工具总是恢复备份时的表结构。
  2. 恢复前先规划好是否需要把表结构恢复到某个特定时间点,如果不需要,恢复后立即执行ALTER TABLE ... ADD COLUMN补齐缺失字段。
  3. 用--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: true

verify会在上传完成后对备份文件重新做一次校验,及时发现损坏并重传。另外,定期做一次“从远端 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入手是最平滑的路径。先用全量备份解决“有或无”的问题,再逐步引入增量、远程存储、自动化告警,最后做到可以随时拉起一个新集群并恢复最近半小时的数据。

希望这篇文章对你有用,也希望大家永远都用不上恢复按钮,但需要它的时候,它能稳稳接住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询