备份每天都在跑却恢复不了:3-2-1 原则、脚本自校验与每周恢复演练实战

从一个典型的"备份失败"说起

「我每天都备份,为什么还是丢了数据?」这句话是个人站长最容易踩的坑。实际情况通常是这样的:备份脚本跑了三个月,日志里一直写着 success,但某天真的需要恢复时,打开备份目录才发现——最近 90 个备份文件大小全部是 0 字节。

根因是 crontab 里的这条命令:

0 3 * * * mysqldump -uroot -p'密码' mydb > /backup/db_$(date +\%F).sql

问题出在 -p'密码' 和 crontab 的 % 转义上。crontab 会把命令里的 % 当作"换行符"处理,导致 date 的格式串被截断,命令实际执行时参数错乱,mysqldump 报错退出,但因为 > 重定向先创建了文件,于是留下一个空文件,而 cron 只报告"命令已执行",不会告诉你内容是否正常。

这个案例暴露了备份这件事的核心矛盾:备份的价值不在于"跑了",而在于"能恢复"。而绝大多数个人站的备份,从未被验证过。

第一原则:备份必须离线、异地、多版本

业界有个著名的"3-2-1 原则":至少 3 份数据、存储在 2 种不同介质上、其中 1 份在异地。对个人站长,可以简化成更可操作的版本:

  • 本机留一份:方便快速回滚,但服务器磁盘挂了就一起完蛋。
  • 异地存一份:备份到另一台服务器、对象存储(如 S3/MinIO/阿里云 OSS),或至少是不同机房的机器。
  • 版本要保留多个:不能只留最新一份。如果数据是被误删后慢慢发现的,最新备份可能早已被污染。建议保留最近 7 天日备 + 4 周周备。

一个必须警惕的陷阱:不要把备份存在同一块磁盘的另一个目录。很多人把 /backup 和 /www 放在同一块盘上,磁盘坏了两个目录一起没。理想情况下备份目录应是另一块物理盘、NAS,或者远端。

第二原则:备份脚本必须自带验证

只写备份不写校验,等于没写。三步让备份变得可信:

1. 写入前检查磁盘空间

磁盘写满时,mysqldump > file 会静默产生截断的文件。备份开始前先判断剩余空间:

#!/bin/bash
set -euo pipefail
BACKUP_DIR=/backup/db
mkdir -p "$BACKUP_DIR"
# 检查剩余空间,少于 2GB 就告警退出
AVAIL=$(df --output=avail -BG "$BACKUP_DIR" | tail -1 | tr -dc '0-9')
if [ "$AVAIL" -lt 2 ]; then
    echo "空间不足: ${AVAIL}G" >&2
    exit 1
fi

set -euo pipefail 是关键:-e 让任何命令失败立即退出,-u 防止引用未定义变量(避免 rm -rf $DIR/ 变成 rm -rf /),-o pipefail 让管道中任一环节失败都能被捕获。

2. 备份后立即校验文件

mysqldump 的产物是 SQL 文本,可以检查尾部是否有完整的结束标记:

DUMP_FILE="$BACKUP_DIR/db_$(date +%F).sql"
mysqldump --single-transaction --quick mydb > "$DUMP_FILE"
# 校验:文件非空 && 最后一行包含 "Dump completed"
if [ ! -s "$DUMP_FILE" ]; then
    echo "备份文件为空!" >&2; exit 1
fi
if ! tail -5 "$DUMP_FILE" | grep -q "Dump completed"; then
    echo "备份文件不完整(缺少完成标记)!" >&2; exit 1
fi
# 压缩,节省空间
gzip -f "$DUMP_FILE"

--single-transaction 用于 InnoDB 表,可以在不锁表的情况下获得一致性快照,这是线上库备份的必备参数。没有它,备份期间写入的数据会导致备份集不一致。

3. 备份完必须推送到异地

# 推送到另一台服务器(用 rsync 增量 + 断点续传)
rsync -avz --partial --progress -e "ssh -o ServerAliveInterval=30" \
  "$BACKUP_DIR/" backup@1.2.3.4:/data/backup/www/

-e "ssh -o ServerAliveInterval=30" 是为了防止长传过程中 SSH 连接空闲被防火墙断开。--partial 让中断的传输可以续传。

第三原则:恢复演练才是真正的验证

这一步 90% 的人不做,但它才是"备份是否有效"的唯一证明。建议每周自动演练一次:把最新的备份恢复到一个临时数据库,对比几个关键表的行数,然后删掉临时库。

#!/bin/bash
set -euo pipefail
LATEST=$(ls -t /backup/db/*.sql.gz | head -1)
TESTDB="restore_test_$(date +%s)"

echo "演练恢复: $LATEST"
mysql -e "CREATE DATABASE $TESTDB;"
zcat "$LATEST" | mysql "$TESTDB"

# 对比关键表行数
for tbl in posts users comments; do
    SRC=$(mysql -N -e "SELECT COUNT(*) FROM mydb.$tbl;")
    DST=$(mysql -N -e "SELECT COUNT(*) FROM $TESTDB.$tbl;")
    echo "$tbl: 线上=$SRC 恢复=$DST"
    if [ "$SRC" -ne "$DST" ]; then
        echo "!!! 行数不一致,备份可能损坏" >&2
    fi
done

mysql -e "DROP DATABASE $TESTDB;"
echo "演练完成"

把这个脚本挂到 cron 里每周跑一次,并把输出通过邮件或 Webhook 发给自己。当演练结果出现"行数不一致"时,你就知道该检查备份链路了——这比等到真出事才发现要早得多。

第四原则:一定要留一份"不可变"的备份

近年勒索软件和入侵事件的常见套路是:加密你的数据,然后连备份一起加密/删除。如果你的备份服务器用的是同一套 SSH 密钥、同一个可以互相访问的网络,那么攻击者拿到一台机器的控制权后,可以轻松删掉所有备份。

个人站长可以做的低成本加固:

  • 备份账号只写不删:在备份接收端创建专用用户,目录设为只写权限,或者用 chattr +a(仅追加)标志,让已有文件无法被修改或删除。
  • 用对象存储的版本控制:S3 兼容存储开启 versioning 和 object lock 后,即使误删也能找回历史版本。
  • 关键备份离线:定期把重要备份拷到移动硬盘,并断开连接。物理隔离是唯一无法被远程攻破的防线。
# 备份目录设为只可追加,防止被覆盖或删除
chattr +a /data/backup/www
lsattr -d /data/backup/www

注意 chattr +a 的副作用:该目录下的文件只能被追加,不能重命名。所以 rsync 的 --delete 和 --inplace 会失败。正确组合是:本地保留可写的轮转目录,异地归档目录用 +a 保护。

常见误区清单

  • 只备份数据库,忘了网站文件:上传的图片、附件、配置文件往往比数据库还重要。数据库和文件目录必须一起备份,且时间点要接近。
  • 备份到同一块盘:磁盘故障时全军覆没。
  • 没有恢复文档:出事后连"恢复到哪台机器、用什么命令"都不知道。建议在备份目录里放一个 RESTORE.md,写明恢复步骤和依赖版本。
  • 备份脚本没有告警:备份失败没人知道,等于没有备份。任何失败分支都要有 exit 1,并让 cron 通过邮件或 webhook 通知你。

小结

把这件事拆成四句话:离线异地多版本、脚本自带校验、每周演练恢复、留一份不可变。做到这四点,你的备份才算真正可用。

最后提醒一句:永远不要在源服务器上验证备份的唯一副本。演练时先复制到临时位置再操作,别让"验证备份"这个动作本身变成了数据丢失的原因。

补充:文件类数据的备份姿势

数据库有 mysqldump,网站文件则要靠文件级备份。但直接 tar 打包整个目录有几个坑:打包期间文件还在变,可能导致归档内容不一致;大目录打包耗时长,会占用大量 I/O;生成了巨大的单文件,恢复时无法只取其中一部分。

更合理的方案是用 rsync 做增量镜像 + 定期完整归档。增量同步日常跑,完整归档每周一次:

# 日常增量(保留删除,做精确镜像)
rsync -aHAX --delete --numeric-ids \
  -e "ssh -o ServerAliveInterval=30" \
  /www/uploads/ backup@1.2.3.4:/data/backup/uploads/

# 每周归档(硬链接快照,省空间)
rsync -aHAX --delete --link-dest=/data/backup/uploads_prev/ \
  /www/uploads/ /data/backup/uploads_$(date +%F)/

关键参数说明:-a 归档模式保留权限时间,-H 保留硬链接,-A 保留 ACL,-X 保留扩展属性。--link-dest 让未改变的文件在上一个快照里共享硬链接,所以 30 份"全量视图"实际只占一份的空间——这是性价比最高的备份方式。

上传目录的注意点:很多站的 uploads/ 目录里混着大量临时文件、缩略图缓存、甚至被上传的恶意脚本。备份前最好先做一次清理,把明显是垃圾的文件排除掉,可以用 --exclude 参数:

rsync -a --exclude='*.tmp' --exclude='cache/' --exclude='*.log' /www/uploads/ /backup/uploads/

补充:备份的加密与访问控制

异地备份意味着数据离开你的服务器,如果里面含有用户信息,就必须考虑加密。最简单的做法是在打包时就加密,而不是依赖传输层:

# 用 openssl 加密备份(对称加密,密码要单独保管)
tar czf - /www/uploads | \
  openssl enc -aes-256-cbc -salt -pbkdf2 -out /backup/uploads_$(date +%F).tar.gz.enc

# 恢复时解密
openssl enc -d -aes-256-cbc -pbkdf2 -in /backup/uploads_2026-01-01.tar.gz.enc | tar xzf - -C /www/

注意密码不能跟备份存在同一台机器上,也不能写进脚本里明文保存。可以放在密码管理器,或者用环境变量在运行时注入。-pbkdf2 参数很重要,它让密钥派生更抗暴力破解,不加的话老版本的 openssl 用的是弱派生。

访问控制方面,备份接收端应该用专用账号 + 限定目录,最好配合 SSH 的 command= 限制,让这个账号只能执行 rsync,不能开 shell:

# 在备份接收端的 ~/.ssh/authorized_keys 里
command="rsync --server --sender -logDtpre.iLsfxC . /data/backup/",no-pty,no-port-forwarding ssh-ed25519 AAAA... backup@www

这样即使备份账号的密钥泄露,攻击者也无法用它登录 shell 或删除其他数据。这是"最小权限"原则在备份场景的具体应用,配置成本极低,收益却很大。

Last modification:September 30th, 2026 at 08:24 pm

Leave a Comment