备份、容灾与恢复演练
备份这件事只有一句话是真理:没恢复过的备份等于没有备份。很多团队每天跑 mysqldump、文件静静躺在磁盘上,真出事时才发现压缩包是 0 字节、路径写错、或者恢复出来的数据少了三天的订单。本章讲备份三要素、3-2-1 原则、具体命令,以及怎么用一次恢复演练把「备份」变成「可恢复」。
要解决的问题
- 什么该备份、多久备一次、保留多久?
- 数据库怎么备才既一致又不锁表?
- 代码、配置、证书、上传目录分别怎么备?
- 怎么证明备份真的能恢复?
- 整机挂掉时,怎么在 30 分钟内重建服务?
备份三要素与 3-2-1
备份方案只要回答三个问题就完整了:备什么(对象)、多久备一次(频率)、留多久(保留策略)。再加一条业界通行的 3-2-1 原则:至少 3 份副本、存放在 2 种不同介质、其中 1 份在异地。
| 备份对象 | 频率 | 保留策略 | 存放位置 |
|---|---|---|---|
| 数据库全量 | 每日一次(业务低谷) | 本地 7 天 + 异地 30 天 | 对象存储(与服务器不同账号更佳) |
| 数据库增量/binlog | 持续 | 7 天 | 异地 |
| 上传目录 / 用户文件 | 每日增量 | 30 天 | 对象存储 |
| 配置与证书脚本 | 每次变更 | 全历史 | Git 仓库(敏感值走加密或环境变量) |
| 整机状态 | 每周 / 重大变更前 | 2~3 个 | 云平台快照 |
最常见的偷懒做法是「备份写到本机 /backup 目录」——机器磁盘坏了、被勒索加密了、机房故障了,备份和数据一起消失。备份必须离开这台机器,这是最低要求。
数据库备份
# 1) 一致性快照:--single-transaction 让 InnoDB 在事务里取一致快照,不锁表
mysqldump --single-transaction --quick --routines --triggers --events \
--default-character-set=utf8mb4 \
-u backup_user -p"$MYSQL_PWD" mydb | gzip -9 > /backup/mysql/mydb-$(date +%F).sql.gz
# 2) 立即做一次可读性校验(损坏的备份比没有备份更危险,因为它让你以为有备份)
zgrep -c 'INSERT INTO' /backup/mysql/mydb-$(date +%F).sql.gz
# 3) 异地存放:同步到对象存储或另一台机器
rsync -avz --delete /backup/mysql/ backup@backup-host:/data/mysql/
| 参数 | 作用 | 注意 |
|---|---|---|
--single-transaction | InnoDB 一致性快照,不锁表 | 只对 InnoDB 有效;MyISAM 表仍需 --lock-tables |
--quick | 逐行读取而不是全量载入内存 | 大表必备 |
--routines --triggers --events | 带上存储过程、触发器、事件 | 默认不导出,漏了恢复后业务会静默出错 |
--default-character-set=utf8mb4 | 明确字符集 | 避免恢复后中文乱码 |
-p"$MYSQL_PWD" | 用环境变量传密码 | 命令行明文会被 ps 看到 |
--single-transaction 不是万能保险:它保证的是 InnoDB 数据的一致性,如果同时有 DDL 在执行,快照可能失败或不准,所以备份时间要避开结构变更窗口。
文件与配置备份
# 上传目录:增量同步,--delete 让备份与源保持一致(源端被误删的文件在备份里也会删)
rsync -avz --delete /srv/www/uploads/ backup@backup-host:/data/uploads/
# 配置目录:同样同步一份,同时确保配置已进 Git
rsync -avz --delete /etc/nginx/ backup@backup-host:/data/etc-nginx/
# 配置仓库化:改动可追溯、可回滚、可重建
cd /srv/ops && git add -A && git commit -m "nginx: $(date +%F)" && git push
配置备份的关键不是「复制文件」,而是让配置可重建:把 Nginx 配置、systemd unit、部署脚本、证书申请命令都放进一个私有仓库,新机器上 git clone 就能恢复八成环境。注意仓库里绝不能提交私钥与数据库密码,用 .gitignore 加环境变量管理。
增量加密备份:restic / borg
全量 mysqldump + rsync 够用但重;增量、加密、去重的工具能让备份更快、更安全。两者思路接近,选一个即可。
# restic:仓库可以直接放在对象存储上,适合云环境
export RESTIC_REPOSITORY=s3:https://oss-cn-hangzhou.aliyuncs.com/my-backup
export RESTIC_PASSWORD_FILE=/root/.restic-pass # 仓库密码文件权限 600
restic init
restic backup /srv/www /etc/nginx --exclude-file=/etc/restic-exclude.txt
restic snapshots --last
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune # 保留策略 + 清理
restic check --read-data-subset=5% # 抽样校验仓库完整性
restic restore latest --target /tmp/restore-test --include /srv/www/uploads
# borg:本地或挂载盘上做去重加密仓库
borg init --encryption=repokey /backup/borg-repo
borg create --stats --compression zstd /backup/borg-repo::'{now:%F}' /srv/www /etc/nginx
borg prune --keep-daily 7 --keep-weekly 4 --keep-monthly 12 /backup/borg-repo
两者都比 tar 多出的三个能力值得为它换工具:增量(只存变化块)、加密(备份泄露不等于数据泄露)、校验(restic check 能确认仓库没坏)。仓库密码文件丢了等于备份全废,所以密码要单独存在密码管理器或另一台机器上。
备份校验:能恢复才算备份
# 在测试库上恢复一份,与源库比对行数——这是唯一可信的校验方式
gunzip -c /backup/mysql/mydb-2025-01-08.sql.gz | mysql -u root -p restore_test
mysql -N -e "SELECT COUNT(*) FROM restore_test.orders" > /tmp/rows_backup.txt
mysql -N -e "SELECT COUNT(*) FROM mydb.orders" > /tmp/rows_live.txt
diff /tmp/rows_backup.txt /tmp/rows_live.txt && echo '备份校验通过'
校验频率建议:每月一次完整恢复演练,每周一次抽样校验。演练要落到具体动作上——恢复到测试库、跑一遍核心查询、确认行数与关键字段正确,并记录耗时(这个耗时就是灾难恢复的真实耗时基线)。
定时任务与失败告警
# /etc/cron.d/backup —— 定时执行并落日志
0 3 * * * root /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
#!/usr/bin/env bash
# /opt/scripts/backup.sh —— 关键点:任何一步失败都要告警,不能静默
set -euo pipefail
WEBHOOK_URL="https://your-webhook"
trap 'curl -s -X POST -H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"备份失败:第 $LINENO 行\"}}" "$WEBHOOK_URL"' ERR
mysqldump --single-transaction --routines -u backup_user -p"$MYSQL_PWD" mydb \
| gzip -9 > /backup/mysql/mydb-$(date +%F).sql.gz
test -s /backup/mysql/mydb-$(date +%F).sql.gz # 文件不能是 0 字节
rsync -avz /backup/mysql/ backup@backup-host:/data/mysql/
find /backup/mysql -name '*.sql.gz' -mtime +7 -delete
set -euo pipefail 加上 trap ... ERR 是这份脚本的核心:备份脚本最危险的状态是「失败了但没人知道」。
灾难恢复:整机挂掉的 30 分钟清单
| 步骤 | 具体动作 | 目标耗时 |
|---|---|---|
| 1 拉起机器 | 云控制台按同配置新建,或从快照创建 | 5 分钟 |
| 2 恢复数据 | 从对象存储下载最近备份并导入 | 10 分钟 |
| 3 部署代码 | 拉取上一个可用 tag,用 CI 产物或重新构建 | 5 分钟 |
| 4 恢复配置 | 从 Git 拉配置、放证书(或重新签发)、启服务 | 3 分钟 |
| 5 切换解析 | 把域名 A 记录指向新 IP(TTL 要提前调小到 60s) | 2 分钟 |
| 6 验证 | 拨测、登录、核心流程走一遍 | 5 分钟 |
要让这 30 分钟成立,有三件事必须提前做:DNS 的 TTL 平时就保持在 300 秒以内、备份恢复的耗时被演练验证过、配置与部署脚本能在新机器上一条命令重建。真正意义上的「容灾」还需要多可用区或异地机房,那是成本更高的下一阶段;对多数项目来说,「备份能恢复 + 30 分钟能重建」就已经覆盖了 95% 的灾难场景。
常见坑
| 坑 | 现象 | 做法 |
|---|---|---|
| 备份只存本机 | 机器故障或勒索加密时数据一起丢 | 异地存放,3-2-1 原则 |
| 从没恢复过 | 出事才发现备份是空的 | 每月恢复演练,每周抽样校验 |
| 备份脚本静默失败 | 连续几周没有新备份,没人知道 | set -euo pipefail + trap + 告警 |
| 漏了存储过程与触发器 | 恢复后业务逻辑静默失效 | 加 --routines --triggers --events |
| 命令行明文写密码 | 密码出现在 ps 输出与历史记录里 | 用环境变量或 --defaults-extra-file |
| 只备份数据不备份配置 | 机器重建后要凭记忆重配 Nginx | 配置进 Git,脚本化重建 |
小结:备份方案要能被一句话说清「备什么、多久一次、留多久、放在哪」,并满足 3-2-1 原则;数据库用 mysqldump --single-transaction --routines 加压缩和异地同步,文件与配置用 rsync 加 Git 仓库化;想要增量与加密就上 restic 或 borg;最关键的一步是定期在测试库恢复一次并比对行数,同时给备份脚本加失败告警;最后把「整机挂掉怎么在 30 分钟内重建」写成清单并演练一遍——备份的价值不在于存了多少份,而在于恢复用了几分钟。