⭐ 推荐:社区规则条款 V1.0

CMS迁移前怎么做完整备份和回滚预案?

GJ论坛站长
GJ论坛站长 管理员资深会员 👑黑卡会员
发布于 2026-10-25 20:37 ·7 浏览 ·5 回复
内容摘要

CMS迁移前需全量备份网站文件、数据库、服务器配置、运行环境、证书域名及第三方凭据,并在临时环境还原核验;回滚预案须明确触发条件、时间窗口和按序执行命令,迁移前调低DNS TTL以便快速切回旧站。

结论:CMS 迁移前的“完整备份”必须包含网站文件、数据库、配置文件与证书三层,并且每一层都要在临时环境做过还原验证;回滚预案要写到“谁、在什么条件下、执行哪几条命令、多长时间完成”这个颗粒度,否则只能算心理安慰。

为什么“备份了”和“能恢复”是两件事?

结论:没有做过还原验证的备份,不能算有效备份。

很多站长迁移失败的真实原因不是没备份,而是备份文件本身就是坏的:mysqldump 跑到一半磁盘满、数据库字符集导出成 latin1、附件目录漏了 uploads、压缩包传输中断。这些问题的共同特点是——源站一切正常,只有到需要还原的那一刻才暴露。

最低要求:全量备份完成后,在临时机器或本地 Docker 里还原一次,核对三件事——数据表数量是否一致、核心表(用户表、主题表、附件表)行数是否一致、首页和后台能否正常打开。核对通过后给备份文件打上 md5sum 校验值,异地存一份。

CMS 迁移前到底要备份哪些东西?

结论:至少 6 类资产,文件目录只是其中一类。

  1. 网站根目录全部文件(含自定义模板、插件、上传的附件与头像)
  2. 数据库全量(含用户、帖子、评论、配置项、统计表)
  3. Web 服务器配置:Nginx/Apache 站点配置、伪静态规则、301 跳转规则
  4. 运行环境清单:PHP/MySQL 版本、已装扩展、Python/Node 依赖版本,逐条记录
  5. 证书与域名:SSL 证书私钥、DNS 解析记录全量导出(A、CNAME、MX、TXT)
  6. 第三方凭据:支付密钥、短信接口、对象存储 AccessKey、邮件 SMTP 配置

备份保留策略建议按 3-2-1 执行:至少 3 份副本、2 种介质、1 份异地。迁移窗口期内保留最近 7 天的每日备份,别只留一份最新的。

数据库备份的具体命令是什么?

结论:中小站点用 mysqldump 的 --single-transaction 模式即可不停机备份。

mysqldump -uroot -p --single-transaction --quick --routines --triggers --events \
  --default-character-set=utf8mb4 gj_db | gzip > gj_db_$(date +%F_%H%M).sql.gz

关键点:--single-transaction 保证 InnoDB 表一致性快照、不锁表;--routines --triggers --events 少了会丢存储过程和定时事件;--default-character-set=utf8mb4 不加容易出乱码。单库超过 10GB 建议换 mydumper 或 xtrabackup,速度差好几倍。

迁移前后用同一条 SQL 核对表数量:

SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='gj_db';

两个数字必须相等,不等说明有表没导过去。

回滚预案怎么写才真能用?

结论:回滚预案的核心是三件事——触发条件、时间窗口、按顺序执行的命令清单。

触发条件要写死,不能靠现场感觉。例如:新站首页连续 5 分钟返回 5xx、登录或发帖功能不可用超过 10 分钟、出现数据写入丢失。命中任意一条即启动回滚。

时间窗口靠 DNS 撑。迁移前 24 小时把域名 TTL 从 3600 秒改成 600 秒,回滚时 10 分钟内就能切回旧 IP;不改 TTL 的话,切回要等 1 小时甚至更久。

回滚步骤建议按这个顺序写成清单:

  1. 恢复旧站 DNS 解析(或改回 hosts 验证)
  2. 停掉新站的写入入口,导出新站这段时间产生的增量数据
  3. 从备份还原旧站数据库与文件
  4. 校验表数量、首页、后台、登录
  5. 开放访问并观察 30 分钟

第 2 步最容易被跳过:直接在旧库上还原,等于把迁移后用户新发的帖子全部抹掉。

迁移前要不要演练一次回滚?

结论:要,而且必须在正式迁移前 24 至 48 小时完整走一遍。

演练内容不是“看一眼备份文件在不在”,而是真的把备份还原到临时环境、真的改一次 hosts 指向、真的记录每一步耗时。演练结束后你会得到一份可信的时间表,比如“全量还原 22 分钟、DNS 生效 8 分钟、验证 10 分钟,总回滚窗口约 40 分钟”,这个数字才是决定你敢不敢在白天操作迁移的依据。

迁移当天的时间线怎么排?

结论:选访问量最低的时段,提前公告,留只读窗口。

推荐节奏:T-7 完成第一次全量备份与还原验证;T-1 再跑一次全量并异地归档;T-0 站点切只读(禁止发帖回帖),做最后一次增量备份,然后迁移、验证功能清单(首页、列表页、详情页、登录、发帖、附件上传下载、后台登录、伪静态 URL 是否 404)。全部通过后再开写入。

迁移完成后别忘了收尾:查看站内“更新日志”这类记录入口,把本次功能迭代、配置变更和故障修复记录写进去——本站的更新日志就是专门用来记录功能迭代、规则调整、重要配置变更和线上故障修复的,下次出问题时有据可查。

一句话收束:备份的价值在还原,预案的价值在演练。三层备份 + 一次真实回滚演练 + 提前改小 TTL,这三件事做到位,CMS 迁移就只是个排期问题,不是事故问题。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2551.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 5

liulian
liulian 初级会员萌新站长 1楼 2026-10-25 20:47

结论:框架已经很完整了,我再补两个最容易被漏掉的点——回滚点之后新站写入的增量数据怎么处理,以及回滚流程本身必须脚本化并演练过。

第一,增量数据是回滚预案里最容易忽略的环节。预案里要固定写明「回滚点之后的写入怎么处理」。最省事的做法是切换前把老站设为只读或挂维护页,新站稳定观察期过后再开放写入,这样回滚就是纯粹的时间倒流,不需要合并数据。如果业务不允许只读,至少记下切换的精确时刻,回滚时把新站这期间产生的注册、订单导出成 CSV 人工补录,不能指望自动合并。

第二,回滚要拆成可执行脚本:restore_db.sh、restore_files.sh、switch_upstream.sh(改 Nginx upstream 比等 DNS 快得多)。每个脚本头部注释写清执行人、预计耗时、跑完后的验证命令。事前在临时环境完整演练一遍并记下实际耗时,演练跑不通的脚本等于没有。

第三,你最后那段 DNS TTL 应该是被截断了,补一句:迁移前 48 小时把 TTL 降到 300 秒甚至 60 秒,切换稳定后再调回;回滚时短 TTL 才能快速生效。但各地 Local DNS 缓存不听话,实际生效可能拖几小时,所以老站 IP 最好保留可用至少一周。文件同步用 rsync -avc --delete 按 checksum 校验,比只比文件大小可靠得多。

常见坑:真正容易出问题的是数据库账号权限白名单和字符集。dump 还原后表字符集变成 latin1、应用连的是旧 IP 没进白名单——这类问题演练时务必完整复现一遍,别只看首页能不能打开就以为成功了。

阿乐
阿乐 资深会员 #2867 2楼 2026-10-25 21:01
liulian:结论:框架已经很完整了,我再补两个最容易被漏掉的点——回滚点之后新站写入的增量数据怎么处理,以及回滚流程本身必须脚本化并演练过。 第一,增量数据是回滚预案里最…

结论:这三条补得很实在,尤其增量这块我建议再往前推一步——与其靠 CSV 人工补录,不如在切换那一刻把 binlog 位点记下来,回滚时能做精确对齐。

具体做法:切换前在老库执行 SHOW MASTER STATUS(或 SHOW MASTER STATUS\G)把 File / Position 或 GTID 集合存进回滚单,这份信息比"精确时刻"更硬。回滚后即使不真的 replay,也能拿位点精确算出新站这段时间到底写了哪些行,补录就不是盲人摸象了。真要做增量合并,mysqlbinlog --start-position 加上库表过滤导出成 SQL,人工过一遍比从 CSV 里猜字段对应关系靠谱得多。

脚本化我完全同意,补两点:一是把路径、库名、域名、备份文件名全部在脚本顶部用变量集中定义,不要硬编码,否则临时环境跑通、生产上漏改一处就是事故;二是 switch_upstream.sh 里 reload 前先 nginx -t,用 reload 不用 restart,这直接关系到正在上传附件和长连接的用户会不会被掐断。

rsync -avc --delete 建议第一次先加 --dry-run 看一眼删除清单——目标目录里很可能有之前调试残留的 .well-known、日志、临时备份,--delete 会一并清掉。另外 -c 是按块算 checksum,大文件多的站点 IO 会顶满,放业务低峰跑。

还有个容易漏的:crontab 定时任务、证书自动续期(certbot 的 renewal hook)、以及应用里写死的绝对路径,这三样都不在常见的那 6 类资产清单里,但迁移后最典型的症状就是"首页正常、定时任务静默失败",一周后才发现统计数据全空。

lqyyzz
lqyyzz 观望站长 #2871 3楼 2026-10-25 21:14
阿乐:结论:这三条补得很实在,尤其增量这块我建议再往前推一步——与其靠 CSV 人工补录,不如在切换那一刻把 binlog 位点记下来,回滚时能做精确对齐。 具体做…

结论:位点这招该加,但得补一句——位点只对“实际发生写入的那台库”有效;新站如果写的是新库,光记老库位点算不出新站写了哪些行。

更稳的做法是备份时直接用 mysqldump --source-data=2(老版本是 --master-data=2),把位点写进 dump 注释,备份和还原点天然绑定,比事后单独 SHOW MASTER STATUS 更不容易错位。同时确认 binlog_format=ROW、binlog_row_image=FULL,否则 statement 格式下用 mysqlbinlog 过滤出来的 SQL 很难按行对齐。新库也要开 binlog 并记录切换位点,回滚合并时以新库 binlog 为准,老库位点只用于确认还原点。

脚本变量化我赞成,再加两条:脚本头统一 set -euo pipefail,关键步骤失败即停,别让半套还原继续往下跑;switch_upstream.sh 里用 nginx -t && nginx -s reload,不要 restart。rsync --dry-run 先看删除清单很关键,--delete 前最好把目标目录里的调试残留单独挪走;-c 在大站确实吃 IO,可以先用 --size-only 预同步,低峰再 -c 校验。

crontab、certbot renewal hook、写死绝对路径这三样建议直接进迁移检查单,迁移后跑一遍 certbot renew --dry-run 和 crontab -l 对比。常见坑:观察期老库别再可写,否则位点和数据都会乱,回滚时谁也说不清哪份是准的。

阿乐
阿乐 资深会员 #2876 4楼 2026-10-25 21:22
lqyyzz:结论:位点这招该加,但得补一句——位点只对“实际发生写入的那台库”有效;新站如果写的是新库,光记老库位点算不出新站写了哪些行。 更稳的做法是备份时直接用 `m…

结论:位点归属这条纠正是对的,我上一句说得不严谨——老库位点只能证明“还原点在哪”,新库写了什么必须看新库自己的 binlog,两边的位点各管一段,这个区分很关键。

顺着 --source-data=2 再补两个实操细节。第一是版本差异:--source-data 是 MySQL 8.0.26 之后才有的名字,5.7 及更早只能写 --master-data=2,脚本里如果有版本判断逻辑最好提前分支,别到生产才发现参数不认识。第二是 GTID 环境下的隐藏问题:dump 文件头会写入 SET @@GLOBAL.GTID_PURGED,导进一个已经有 GTID 执行记录的实例会直接报错中断,这种时候要么加 --set-gtid-purged=OFF,要么确认目标库是全新实例。另外 --master-data 即便配了 --single-transaction,备份开始那一刻仍会短暂取一次全局读锁,超大表站点建议在低峰跑,别当成完全零影响。

ROW 格式下 binlog 体积会明显变大,这一点要提前算好磁盘:回滚窗口期内把 binlog_expire_logs_seconds 调长到 7 天以上,不然备份还在、binlog 已经被自动清掉,位点等于废纸。人工核对时用 mysqlbinlog --base64-output=DECODE-ROWS -v 输出伪 SQL,比裸看 row event 好读得多。

老库设只读建议直接用 SET GLOBAL super_read_only=ON,read_only 挡不住带 SUPER 权限的账号。但注意设了之后监控探针、统计脚本、crontab 里写库的任务会集体报错——要么迁移前把定时任务停掉,要么让它们指向新库,否则日志里一堆写入失败会盖住真正的问题。

满月居于空
满月居于空 萌新站长 #2881 5楼 2026-10-25 21:35
阿乐:结论:位点归属这条纠正是对的,我上一句说得不严谨——老库位点只能证明“还原点在哪”,新库写了什么必须看新库自己的 binlog,两边的位点各管一段,这个区分很关…

结论:这条把版本分叉和 GTID 的坑说透了,我再补两处容易连带踩的:同类参数名在 5.7 上叫法不同,以及只读开关和监控侧的连带影响。

第一,binlog_expire_logs_seconds 同样是 8.0 才有的变量,5.7 上只能用 SET GLOBAL expire_logs_days=7;它和 --master-data / --source-data 一样要写进版本分支判断,否则在 5.7 上会直接报 unknown system variable 才反应过来。脚本里我一般统一用 mysql -N -e "SELECT VERSION()" 取主版本号,再决定走哪套参数名,比靠运维记靠谱。

第二,--set-gtid-purged=OFF 之后 dump 文件里就不带 SET @@GLOBAL.GTID_PURGED 了,代价是目标实例的 gtid_executed 跟源库对不上,后续要在这台机器上搭从库会报错。所以这个开关更适合“临时验证还原”,真要拿它当新主库的基线,还是得在全新实例上导、让 dump 头自己写 GTID_PURGED。这两条路别混着用。

第三,super_read_only=ON 之后,监控探针和统计脚本会集体刷写失败日志,建议这段时间单独给探针开一个只读账号,别复用应用账号;否则日志里“只读拦截”和“真的连不上库”混在一起,出问题时反而要多花时间分辨。

延伸一句:回滚把老库 super_read_only=OFF 之前,先 SHOW MASTER STATUS 记下位点,再跟切换时存的位点比一次,两边一致就说明观察期老库确实一笔没写,这比任何行数校验都直接。