备份脚本每天凌晨跑,退出码 0、日志写"backup completed",监控一片绿。真到数据被误删那天,你打开备份文件,才发现
tar解不开、gzip报 CRC 错、或者库里少了一张关键表。
备份的价值不在"跑成功",而在"能恢复"。这篇文章讲四个步骤,把备份从"绿灯"验到"真能还原"。
一、先认清四种"备份成功"的假象
大多数备份事故不是没备份,而是备份看起来正常但实际不可用。先分清是哪一种:
- 文件大小不对 :备份文件是 0 字节,或者比昨天小了一半。常见原因是磁盘写满、管道被上游截断、
set -e之外的中途失败。 - 文件存在但不可读 :
gzip压缩流不完整(CRC 校验失败)、tar归档被截断。这类文件ls -lh看着完全正常。 - 文件完整但内容缺 :漏库漏表、
--exclude把关键目录排掉了、mysqldump中途被 kill 没有结束标记。 - 能恢复但太慢:数据都在,可恢复要 6 小时,而你的 RTO 是 1 小时------那照样算失败。
二、第 1 步:校验备份文件本身能不能读
这一步最便宜,也最容易被跳过。先看大小趋势,再做完整性与内容校验:
bash
# 看最近 7 个备份的大小,异常小的立刻发现
ls -lh /data/backup/*.gz | tail -7
# gzip 完整性校验(不落盘,只跑 CRC)
gzip -t /data/backup/app-2026-10-14.sql.gz && echo "gz OK"
# tar 归档可读性校验
tar -tzf /data/backup/www-2026-10-14.tar.gz > /dev/null && echo "tar OK"
# 关键:mysqldump 正常结束一定有这一行
zcat /data/backup/app-2026-10-14.sql.gz | tail -3 | grep -q "Dump completed" \
&& echo "dump 完整" || echo "dump 被中断!"
mysqldump 在正常结束时会在文件末尾写入 -- Dump completed on ...。没有这一行,说明 dump 是中途死掉的,文件再大也没用------这是最隐蔽的一类假成功。
校验和留档,恢复前再比一次:
bash
sha256sum /data/backup/app-2026-10-14.sql.gz > app-2026-10-14.sql.gz.sha256
sha256sum -c /data/backup/app-2026-10-14.sql.gz.sha256 # 恢复前复核
三、第 2 步:恢复到一个临时实例,别在生产上试
只校验文件不够,必须真导入一次。关键是恢复环境要和目标环境一致(版本、字符集、sql_mode),否则演练通过、真恢复照样报错。
MySQL 恢复演练的标准做法是起一个临时实例:
bash
# 起一个隔离的临时实例(端口 3307,数据目录放 /tmp,不碰生产)
mysqld --initialize-insecure --datadir=/tmp/rst --user=mysql
mysqld --datadir=/tmp/rst --port=3307 --socket=/tmp/rst.sock \
--skip-networking=0 --user=mysql &
# 导入备份
zcat /data/backup/app-2026-10-14.sql.gz \
| mysql --socket=/tmp/rst.sock -uroot --default-character-set=utf8mb4
# 表结构完整性与行数核对
mysql --socket=/tmp/rst.sock -uroot -e "
SELECT COUNT(*) AS tables FROM information_schema.tables
WHERE table_schema='app';
SELECT COUNT(*) AS orders, MAX(id) AS max_id, MAX(created_at) AS latest
FROM app.orders;"
三个核对口径最有用:表数量 (比对上生产)、行数与最大 ID (确认没截断)、最新一条数据的时间戳(确认 RPO 达标)。时间戳特别关键------它直接告诉你丢了多少数据。
如果只想知道某个文件在不在备份里,不用全解开:
bash
tar -tzf /data/backup/www-2026-10-14.tar.gz | grep 'uploads/2026/10'
tar -xzOf /data/backup/www-2026-10-14.tar.gz var/www/app/.env | head
-O 把单个文件吐到标准输出,比全量解压快得多,也安全。
四、第 3 步:核对"备份里到底有什么"
恢复成功不等于备份完整。这一步拿实际内容和基线做差集:
bash
# 库里应有哪些库,和备份里实际有哪些库,做差集
mysql -N -e "SHOW DATABASES" | grep -v -E '^(information_schema|performance_schema|mysql|sys)$' \
| sort > /tmp/expect.txt
zcat app-2026-10-14.sql.gz | grep -oP '^-- Current Database: `\K[^`]+' | sort > /tmp/actual.txt
comm -23 /tmp/expect.txt /tmp/actual.txt # 只出现在"应该有"里的,就是漏掉的库
归档文件同理,用 tar -tzf | wc -l 记录条目数,与基线对比;数量骤降往往意味着某个目录被排除规则吃掉了:
bash
rsync -avn --delete --exclude-from=/etc/backup.exclude /data/ /backup/ # -n 空跑,看文件清单
还要确认两件常被忘掉的东西:binlog (没它就没有 PITR,只能回到备份那一刻)和备份自己的"说明书" ------数据库账号权限、存储过程与触发器(--routines --triggers --events)、证书私钥、配置文件。只备份数据不备份这些,恢复出来的系统照样跑不起来。
五、第 4 步:把恢复变成周期性演练
一次性的验证不算数,要变成制度:
- 定 RPO / RTO 并写清:RPO 决定备份频率与 binlog 保留时长,RTO 决定恢复方式(物理备份 vs 逻辑备份)。演练时记录真实耗时,和 RTO 对表。
- 每月至少一次真恢复演练,写进值班手册,演练结果留档(恢复耗时、数据最新时间点、遇到的问题)。
- 给备份加独立监控 :不只监控"脚本是否退出 0",还要监控备份文件大小与耗时的基线偏离 (连续两天偏小 20% 就告警)、备份文件的新鲜度(超过 26 小时未更新即告警)。
- 遵循 3-2-1:3 份副本、2 种介质、1 份异地(或另一个可用区)。备份和生产放同一块盘、同一个可用区,等于没有备份。
六、避坑清单
- 只看脚本退出码,不看文件 ------
mysqldump失败但被管道里的gzip掩盖,退出码仍是 0(要用set -o pipefail或${PIPESTATUS[0]})。 - 备份时锁表 :InnoDB 用
--single-transaction --master-data=2拿一致性快照,别用--lock-all-tables。 - 备份文件被清理脚本误删 :通配符
rm /backup/*.gz一旦写错目录就全没了,改用按天保留(find -mtime +30 -delete)并先-print空跑。 - 备份与生产同盘同可用区:磁盘故障或误删一起丢,必须异地。
gzip单线程太慢 :大库换pigz -p 8,恢复用pigz -dc,耗时能砍到零头。- 恢复时字符集 /
sql_mode不一致:版本升级后老备份可能因默认值变更导入报错,演练环境要与生产同版本同参数。 - 从未验证过的备份,等于没有备份------这句话值得贴在机房墙上。
小结
验证顺序记成一句话:先校验文件能不能读(gzip -t / tar -tzf / "Dump completed")→ 恢复到临时实例真导一遍,核对表数、行数、最新时间戳 → 与基线做差集,确认漏库漏目录并补齐 binlog 与账号配置 → 把恢复变成每月演练,用 RPO/RTO 和文件新鲜度监控兜底。备份不是运维的例行公事,它是你出事那天唯一能拿出来的东西。