MySQL 备份恢复演练实战:mysqldump 关键参数、完整性校验与每周自动演练脚本

备份不是拷一份就完事:mysql 备份恢复演练的完整流程

个人站长最常见的自我安慰是一句话:"我每天都备份的。"但当你真的需要恢复时,问题才刚开始:备份文件在哪里?能不能解开?解开之后能不能导入?导入之后站点功能是否完整?这四个问题里,任何一个答"不确定",那份备份就等于不存在。

这篇文章讲的是把备份从"心理安慰"变成"可验证资产"的完整做法,核心不是教你写 mysqldump 命令,而是教你定期做恢复演练,因为只有恢复过的备份才叫备份。

先定义清楚:你要防的是哪几种灾难

不同灾难对备份的要求完全不同,先分类再定策略:

误删数据:比如手滑执行了 DELETE FROM typecho_contents,或者被 sql_safe_updates 保护了却仍误改了内容。这类需要的是"最近一次全量 + 能够精确到某个表"。恢复粒度要求高,时间窗口短(通常是几小时内的备份)。

整机故障 / 云主机跑路:系统盘都没了。这类需要的是"异地、离线、可完整重建"的备份,包含数据库、网站文件、Nginx 配置、SSL 证书、crontab。

数据被勒索加密或遭入侵篡改:这类最危险,因为攻击者常常会先删掉你的备份。防御办法是备份采用只写不可删的存储(对象存储开启版本控制 + 生命周期锁定),并且备份服务器不要保存可以访问备份存储的长期凭证。

逻辑错误导致的缓慢损坏:比如某次升级把某字段写坏了,三天后才发现。这类需要较长的保留窗口,通常 7 到 30 天的每日备份轮转。

mysqldump 的关键参数,别用默认值

很多教程给的命令是 mysqldump -u root -p db > bak.sql,这个命令在生产环境里问题很大。看看它的默认行为:会加锁、会写大量的 /*!...*/ 兼容注释、不带事务一致性保证(对 InnoDB 其实有 --single-transaction 才安全)、字符集可能不对。

我实际使用的命令:

mysqldump \
  --single-transaction \
  --quick \
  --default-character-set=utf8mb4 \
  --set-gtid-purged=OFF \
  --routines --triggers --events \
  --hex-blob \
  --no-tablespaces \
  -u root -p 数据库名 \
  | gzip -9 > /backup/db_$(date +%F_%H%M).sql.gz

逐个解释为什么:

--single-transaction 对 InnoDB 是必须的。它开启一个一致性快照事务,备份期间不锁表,站点照常读写,导出的是开始那一刻的逻辑一致状态。没有这个参数,你的备份在写入高峰期可能导出自相矛盾的数据(比如文章表导了新版本,评论表导了旧状态)。

--quick 让 mysqldump 逐行取回数据而不是一次性载入内存,对大表是保命的。个人站的评论表、日志表动辄几十万行,不加这个参数可能直接把 MySQL 的内存吃满然后 OOM。

--set-gtid-purged=OFF 是给有主从复制的环境用的。不加的话导出的 SQL 里会带着 GTID 信息,往从库导入时可能报错或破坏复制拓扑。

--routines --triggers --events:默认 mysqldump 不导出存储过程、触发器和事件调度器。如果你用了事件来做定时清理,恢复后它会消失,这种"静默丢失"极难发现。

--hex-blob:二进制字段以十六进制导出,避免字符集转换破坏 blob 内容(比如你存了图片的缩略图二进制)。

--no-tablespaces:避免要求 PROCESS 权限,让低权限备份账号也能工作。

用低权限专用账号,不要用 root

备份脚本里明文写 root 密码是个隐患——脚本会被放进 crontab、放进 Git、被其他运维人员看到。更糟的是,如果脚本被入侵者读到,等于把整个数据库的最高权限送出去了。

正确做法是建一个只有读权限的备份账号:

CREATE USER 'backup'@'localhost' IDENTIFIED BY '强密码';
GRANT SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER ON 数据库名.* TO 'backup'@'localhost';
FLUSH PRIVILEGES;

然后把密码放在 ~/.my.cnf 里,权限设成 600

[client]
user=backup
password=强密码
host=127.0.0.1

这样脚本里就不用出现密码了,直接 mysqldump --defaults-file=/root/.my.cnf ...,而且 chmod 600 保证了其他系统用户读不到。

备份文件的完整性校验

最容易被忽略的一步。备份跑完不报错,不代表文件是完好的——磁盘满、管道中断、网络抖动都可能产生一个看起来正常但被截断的 gzip 文件,等到恢复时才发现解不开。

所以备份脚本最后一定要加校验:

#!/bin/bash
set -euo pipefail
F="/backup/db_$(date +%F_%H%M).sql.gz"
mysqldump --defaults-file=/root/.my.cnf --single-transaction --quick \
  --default-character-set=utf8mb4 --routines --triggers --events \
  --hex-blob --no-tablespaces 数据库名 | gzip -9 > "$F"

# 1. 文件非空
[ -s "$F" ] || { echo "备份为空"; exit 1; }
# 2. gzip 完整性
gzip -t "$F" || { echo "gzip 损坏"; exit 1; }
# 3. SQL 尾部完整(mysqldump 正常结束会写 Dump completed)
zcat "$F" | tail -c 200 | grep -q "Dump completed" || { echo "备份被截断"; exit 1; }
# 4. 记录校验和
sha256sum "$F" >> /backup/checksums.txt
echo "OK: $F ($(du -h "$F" | cut -f1))"

第三条检查特别实用。"Dump completed" 是 mysqldump 在正常结束时写的末尾注释,如果它不在,说明进程被 kill 或磁盘写满了。我见过一次磁盘满导致备份文件有 3GB,但最后 200MB 是空的,靠这个检查才发现。

真正的恢复演练:每周做一次,用脚本自动做

这是本文的核心。备份的价值只在恢复那一刻体现,所以必须定期拿备份去恢复,而且要恢复到一个隔离环境,不影响生产库。

演练脚本的逻辑是:建一个临时库 → 导入最新备份 → 跑一批校验查询 → 比对结果 → 删除临时库。

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

mysql --defaults-file=/root/.my.cnf -e "CREATE DATABASE $TESTDB DEFAULT CHARACTER SET utf8mb4;"
zcat "$LATEST" | mysql --defaults-file=/root/.my.cnf "$TESTDB"

# 关键校验:核心表是否有数据
POSTS=$(mysql --defaults-file=/root/.my.cnf -N -e "SELECT COUNT(*) FROM $TESTDB.typecho_contents WHERE type='post';")
COMMENTS=$(mysql --defaults-file=/root/.my.cnf -N -e "SELECT COUNT(*) FROM $TESTDB.typecho_comments;")

echo "备份文件: $LATEST"
echo "文章数: $POSTS  评论数: $COMMENTS"

if [ "$POSTS" -lt 50 ]; then
  echo "!!! 警告:恢复后文章数异常偏低,备份可能有问题"; exit 2
fi

mysql --defaults-file=/root/.my.cnf -e "DROP DATABASE $TESTDB;"
echo "恢复演练通过"

把这个脚本挂到 cron 里每周跑一次,并且要求它把结果发到你的手机或邮箱。没收到报告的警报才是真正的警报——如果演练脚本静静失败了你也不知道,那就白做了。

更进一步,可以引入"行数基线":每天记录核心表的 COUNT,如果某天恢复演练的结果比基线少了 20% 以上,立刻告警。这能抓住"备份跑的是旧数据源"这类隐蔽错误。

异地与离线:3-2-1 原则落到个人站

3-2-1 原则是:至少 3 份副本,2 种不同介质,1 份放在异地。个人站不需要那么复杂,但至少要满足两点:

备份不要只放在同一台服务器。服务器被删、被格式化、被勒索,备份跟数据一起没了。最简单的做法是用 rclone 同步到对象存储,或者用一台便宜的 VPS 通过 rsync 拉取。异地那一份建议开启版本控制,防止被同步逻辑覆盖(比如误删了本地备份,同步一跑,远端也被删了)。

还有一点,加密。数据库备份里包含用户邮箱、密码哈希、评论内容,直接明文扔到对象存储是隐私风险。可以用 gpg 对称加密,密钥保存在你自己的密码管理器里:

gpg --symmetric --cipher-algo AES256 -o "$F.gpg" "$F"
rm -f "$F"   # 明文不留

恢复时 gpg -d "$F.gpg" | mysql ... 直接管道导入,中途不落明文到磁盘。

文件层面的备份同样要演练

数据库只是一半。完整恢复一个站点还需要:网站目录(主题、插件、上传的附件)、Nginx 配置、SSL 证书、crontab、以及关键的 PHP 版本与扩展信息。

证书这项目前经常被漏。Let's Encrypt 证书可以重新签发,但如果你的站点用了付费证书,恢复时就要从备份里取。建议把 /etc/letsencrypt 整个目录纳入备份,同时记录一份 crontab -lnginx -T 的输出到一个文本文件——后者是所有生效配置的合并结果,包含 include 进来的内容,比手动收集配置文件可靠得多。

nginx -T > /backup/config/nginx-full-$(date +%F).conf 2>&1
crontab -l > /backup/config/crontab-$(date +%F).txt 2>&1
php -v > /backup/config/php-version-$(date +%F).txt
mysql --version >> /backup/config/php-version-$(date +%F).txt

这些文本文件只有几 KB,但当你需要在一台全新机器上重建站点时,它们价值连城。

一句话总结

备份的验收标准只有一条:你能不能在一次深呼吸的时间内,从备份把站点完整恢复出来。如果你的答案是"应该可以吧",那就在这个周末花两小时写个演练脚本。恢复演练跑通一次带来的安心感,远胜过每天看着备份目录里堆满文件时的错觉。真正的事故从不提前预约,它只会在你最不希望的时候,检验你有没有认真对待过这件事。

Last modification:September 22nd, 2026 at 01:24 pm

Leave a Comment