
ClickHouse 的备份与恢复一直都是踩坑重灾区。别看它查询快得离谱真要到了数据误删、节点损坏、机房故障这种节骨眼没提前把备份体系设计好分分钟就是事故现场。这两年我在生产环境里从最土的cp -r一路做到基于 clickhouse-backup 的全量加增量体系中间踩过的坑、趟过的雷足够写一本小册子了。这篇文章就结合最新版 ClickHouse23.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聊备份策略之前先统一两个概念RPORecovery Point Objective你能容忍丢失多长时间的数据。比如每小时备份一次最坏情况可能丢 1 小时数据。RTORecovery 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 或腾讯云 COSendpoint 可能要带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 21 */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 21注意 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 tablemytable 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等待完成或手动 KILL5. 生产环境自动化备份体系参考模板备份不只是跑个 cron 脚本就完了一个可落地的体系还应该包含监控、告警、日志留存和定期演练。我把自己正在用的模板简化后贴出来希望能帮大家减少设计的成本。5.1 备份脚本参考#!/bin/bash # clickhouse_backup_helper.sh set -euo pipefail BACKUP_BASE_DIR/data/clickhouse_backup S3_BUCKETs3://clickhouse-backup-prod USERbackup_user PASSxxx LOG_FILE/var/log/clickhouse-backup-wrapper.log export CLICKHOUSE_BACKUP_LOG_LEVELinfo export CLICKHOUSE_BACKUP_CONFIG/etc/clickhouse-backup/config.yml ts() { date %Y-%m-%d %H:%M:%S; } log() { echo $(ts) $* $LOG_FILE; } # 1. 全量备份 FULL_NAMEdaily_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入手是最平滑的路径。先用全量备份解决“有或无”的问题再逐步引入增量、远程存储、自动化告警最后做到可以随时拉起一个新集群并恢复最近半小时的数据。希望这篇文章对你有用也希望大家永远都用不上恢复按钮但需要它的时候它能稳稳接住。