备份文件躺在那里,不等于你有一份可用的备份。真正决定成败的是三件事:多久备一次(RPO)、多久能恢复回来(RTO)、上一次成功演练是哪天。这篇把这三件事落成可执行的脚本和检查清单。
一、先把两个数字定下来
- RPO(能丢多少数据):出事后你最多能接受丢失多长时间的内容。RPO=24h 就是每天至少备一次数据库。
- RTO(多久能恢复):从发现故障到站点重新对外服务,你能接受多长时间。RTO 直接决定恢复演练要跑多快、备份要不要放在能快速拉取的地方。
定完这两个数字,频率就是算出来的,不是拍脑袋。
二、备份频率对照表
| 站点类型 | 数据库 | 文件(wp-content) | 保留 | 恢复演练 |
|---|---|---|---|---|
| 企业展示站,偶尔改内容 | 每日 1 次 | 每周 1 次 + 每次上线前 | 30 天 | 每季度 1 次 |
| 有询盘表单的站 | 每 6 小时 | 每日 | 60 天 | 每月 1 次 |
| 有在线交易的站 | 每小时 | 每日 | 90 天 | 每月 + 大促前 |
| 内容更新频繁的站 | 每日 | 每日 | 30 天 | 每季度 1 次 |
再加一条硬规则:任何一次改主题、改插件、改数据库、换主机之前,手动做一次全量备份并立刻验证。变更前的那份备份是唯一一份「确定是好的」状态。
保留策略建议按「日备留 14 份 + 周备留 8 份 + 月备留 12 份」,比单纯按天数滚动更省空间也更好找。
三、备什么:清单
必须备:
- 数据库全量(含自定义表,很多插件会自己建表)
wp-content/uploads(媒体库,最容易漏)wp-content/themes、wp-content/plugins、wp-content/mu-pluginswp-config.php(含数据库凭据和盐值)- Web 服务器配置:Nginx/Apache 的站点配置文件
- 系统 crontab 里跟 WordPress 相关的条目(
crontab -l) - PHP 版本、MySQL 版本、已装扩展的清单(
php -v、php -m、mysql --version)
不用备(占了体积还会拖慢恢复):
wp-content/cache、各类缓存目录、日志文件- 备份插件自己生成的备份目录(会自我嵌套)
node_modules、.git(除非你依赖它)
备份之外还要单独记一笔:
- DNS 托管在哪、域名注册商账号
- CDN 配置、SSL 证书申请方式
- 表单/邮件/SMTP/支付这些第三方服务的凭据和回调地址
这些不随网站文件走,恢复时却必须一起就位。
四、备份脚本
用 WP-CLI 做数据库导出(底层是 mysqldump),tar 做文件归档。放到 /usr/local/bin/wp-backup.sh:
#!/usr/bin/env bash
set -euo pipefail
SITE_PATH=/var/www/html
BACKUP_DIR=/var/backups/wp
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
KEEP_DAYS=30
mkdir -p "$BACKUP_DIR"
# 1) 数据库
wp db export "$BACKUP_DIR/db-$STAMP.sql" --path="$SITE_PATH"
gzip -9 "$BACKUP_DIR/db-$STAMP.sql"
# 2) 文件(排除缓存和旧备份目录)
tar -czf "$BACKUP_DIR/files-$STAMP.tar.gz" \
--exclude='./wp-content/cache' \
--exclude='./wp-content/*cache*' \
--exclude='./wp-content/*backup*' \
--exclude='./wp-content/*.log' \
-C "$SITE_PATH" ./wp-content ./wp-config.php
# 3) 完整性校验值
sha256sum "$BACKUP_DIR/db-$STAMP.sql.gz" "$BACKUP_DIR/files-$STAMP.tar.gz" \
> "$BACKUP_DIR/sha-$STAMP.txt"
# 4) 清理过期备份
find "$BACKUP_DIR" -maxdepth 1 -type f -name '*.gz' -mtime +$KEEP_DAYS -delete
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'sha-*.txt' -mtime +$KEEP_DAYS -delete
echo "backup ok: $STAMP"
注意事项:
wp db export需要 WP-CLI 有权限读取wp-config.php,用站点属主账号跑,别用 root 跑出 root 属主的文件。- 数据库大的话,导出期间会有较长的事务,建议在低峰期跑,脚本里加
--single-transaction需要自己换成 mysqldump 调用。 - 备份文件不要和站点放同一块盘。至少同步一份到对象存储或另一台机器,用
rclone或云厂商 CLI 都行。 - 备份文件权限设
600,里面是完整数据库,含用户表和表单提交内容。
挂到系统 cron:
0 3 * * * /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1
跑完检查日志里有没有 backup ok,没有就当失败处理。
五、验证:分四层,别跳步
只 ls -lh 看一眼文件存在,是七成事故的来源。恢复验证至少走四层。
第 1 层:文件完整性
cd /var/backups/wp && sha256sum -c sha-20261027T030000Z.txt
gzip -t db-20261027T030000Z.sql.gz && echo "sql gz ok"
tar -tzf files-20261027T030000Z.tar.gz > /dev/null && echo "tar ok"
第 2 层:能导入
不要导进生产库。 建一个临时库:
DB=wp_restore_test
mysql -e "DROP DATABASE IF EXISTS $DB; CREATE DATABASE $DB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
gunzip -c /var/backups/wp/db-20261027T030000Z.sql.gz | mysql "$DB"
mysql -e "SELECT COUNT(*) FROM $DB.wp_posts; SELECT COUNT(*) FROM $DB.wp_options;"
表前缀不是 wp_ 的话记得改。行数和生产库比一下,同一个数量级就正常。
第 3 层:站能跑起来
把文件解到临时目录,改 wp-config.php 指向临时库,用 PHP 内置服务器起一下:
RESTORE=/tmp/wprestore
rm -rf "$RESTORE" && mkdir -p "$RESTORE"
tar -xzf /var/backups/wp/files-20261027T030000Z.tar.gz -C "$RESTORE"
wp config set DB_NAME wp_restore_test --path="$RESTORE"
wp option get home --path="$RESTORE"
wp server --path="$RESTORE" --host=127.0.0.1 --port=8080 &
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/
首页返回 200 只是及格线,还要抽查:
wp post list --path="$RESTORE" --post_type=page --post_status=publish --fields=ID,post_title --format=table | head -20
wp plugin list --path="$RESTORE"
find "$RESTORE/wp-content/uploads" -type f | wc -l
find /var/www/html/wp-content/uploads -type f | wc -l
页面列表要能看到你认识的那些页面,插件列表不能有报错,两个 uploads 文件数应该一致(差异过大说明文件备份漏了)。
第 4 层:功能抽样
这一层最容易被跳过,也最容易在真出事时暴露:
- 打开一个有图片的内页,确认图片不是坏图
- 提交一次测试询盘/测试订单,看邮件能不能发出去
- 打开后台登录页,能登录
- 有表单、支付、搜索的,各点一次
wp server 只适合快速验证,固定链接、重写规则和部分插件在它下面可能不工作。要更接近真实环境,就在一台临时机器上按生产环境装一遍 Nginx + PHP,把域名用本机 hosts 指过去再测。
六、迁移:跟恢复的区别只在换地址
迁移本质上是「恢复到另一台机器 + 换域名」。多出来的风险全在域名替换上。
顺序:
- 提前 24–48 小时把 DNS TTL 降到 300 秒
- 源站全量备份并做一次第 3 层验证
- 新机装好同版本 PHP、MySQL 和扩展,建库建用户
- 导入数据库、解压文件、改
wp-config.php的库连接 - 替换地址
- 本机 hosts 指向新 IP 测试
- 切 DNS
- 收尾检查
替换地址用 WP-CLI,不要用 SQL 直接 UPDATE:
# 先干跑,看会改多少处
wp search-replace 'https://old.example.com' 'https://new.example.com' \
--all-tables --precise --recurse-objects --skip-columns=guid --dry-run --path=/var/www/html
# 确认后真跑
wp search-replace 'https://old.example.com' 'https://new.example.com' \
--all-tables --precise --recurse-objects --skip-columns=guid --path=/var/www/html
为什么不能直接 SQL 替换:WordPress 的很多选项和页面内容里存的是序列化数组,数组里带长度前缀,SQL 替换只改字符串不改长度,整个值会反序列化失败,表现为某些小工具、主题设置、页面构建内容直接空白。--precise 会走反序列化再重写,慢但正确。--skip-columns=guid 是保住文章 GUID,避免订阅源重复推送。
迁移收尾清单:
- [ ]
wp option get siteurl和home都是新域名 - [ ]
wp rewrite flush --hard刷新固定链接 - [ ] 后台 → 设置 → 固定链接重新保存一次(等价于上一条)
- [ ] 清空对象缓存(Redis/Memcached),旧键会带着旧域名
- [ ] 检查
wp-config.php里的盐值和数据库凭据 - [ ]
wp cron event list确认定时任务在,再wp cron event run --due-now跑一遍 - [ ] 文件属主改成 Web 用户(如
chown -R www-data:www-data) - [ ] SSL 证书在新机签好,
curl -I https://new.example.com看 200 - [ ] 邮件/SMTP、支付回调、第三方 API 的域名白名单同步更新
- [ ] 重新生成 sitemap,提交给搜索引擎
- [ ] 旧站保留 301 跳转至少一个 TTL 周期
最后一条别忘了:搜索 sitemap、结构化数据、canonical 这些是跟着域名走的,换完域名都要重新生成一遍才能被正确读。我们有个免费插件 AumViso 专门做这块(sitemap、meta、schema、内链),迁移完想省事可以看:https://aumcreate.com/plugins/aumviso?utm_source=aliyun&utm_medium=content&utm_campaign=aliyun-wp-backup-restore-test-plan
七、几个反复踩的坑
- 只备数据库。恢复后站是空的,图片全没了。附件目录必须单独备。
- 备份文件放在
wp-content里。攻击者拿到的就是全站数据,而且下次备份会把自己打包进去。 - 从没演练过。备份脚本绿灯跑了两年,第一次真恢复才发现数据库用户没权限导入。
- 临时库和生产库同名。恢复演练永远用独立库名和独立目录。
- 忽略版本差异。备份是 PHP 7.4 的站,恢复到 PHP 8.3 的机器上,老插件可能直接报错。备份清单里记版本就是这个用。
- cron 没迁。站看着正常,但定时发布、清理任务、自动对账全部停摆,几周后才被发现。
八、演练记录怎么留
每次演练记一行,比任何文档都有用:
| 日期 | 备份文件 | 恢复耗时 | 第 1–4 层 | 发现的问题 | 处理 |
|---|---|---|---|---|---|
| 2026-10-27 | db/files-20261027T030000Z | 18 分钟 | 全通过 | uploads 少 12 个文件 | 已改为每周全量 |
演练频率跟站点重要性挂钩:有交易的站每月一次,纯展示站每季度一次,但每次大改版、换主机、换插件之前必须加一次。
一句话收尾:备份的价值只在恢复成功那一刻成立,其余时间它只是一堆文件。