一、搬家前必须先盘清楚:到底要搬什么

大部分人以为搬家就是「把网站文件拷过去」。真正出过事的迁移,都是因为漏搬了某个不起眼的东西:数据库里的用户密码、面板里配的定时任务、证书私钥、.env 里的第三方 API 密钥、或者某段写在 crontab 里的脚本。
正确的顺序是:先做一份完整的清单,再动手。 清单要覆盖五个维度:
第一维:运行环境。 操作系统版本、PHP 版本、MySQL/MariaDB 版本、Web 服务器版本、扩展模块列表。这些不一致会直接导致网站报错。
# 在旧机导出完整环境清单
php -v > /tmp/env_php.txt
php -m > /tmp/env_php_modules.txt
mysql --version > /tmp/env_mysql.txt
nginx -v 2>&1 > /tmp/env_nginx.txt
cat /etc/os-release > /tmp/env_os.txt
sudo dpkg -l > /tmp/env_packages.txt
第二维:数据。 网站文件、数据库、上传目录。还要注意有没有对象存储、缓存(Redis/Memcached)、队列(RabbitMQ/MQTT)。
第三维:配置与密钥。 这个最容易漏。逐项清点:
| 配置项 | 位置 | 漏了会怎样 |
|---|---|---|
| 网站数据库账号密码 | wp-config.php / .env |
网站直接连不上数据库 |
| 第三方 API 密钥 | .env / 配置文件 |
支付、邮件、地图等功能静默失效 |
| SSL 证书私钥 | /etc/letsencrypt/live/ |
HTTPS 站点打不开 |
| SSH 私钥 | ~/.ssh/id_* |
登不上新机,只能靠密码 |
| 面板账号密码 | 宝塔/1Panel 数据库 | 面板登不进,只能重装 |
| 定时任务 | crontab -l / /etc/cron.d/ |
定时发布、备份、对账全停 |
| 防火墙规则 | ufw / iptables |
新机端口开错,或漏开必需端口 |
第四维:外部依赖。 你的域名解析在哪个平台、CDN 源站指向哪、有没有第三方白名单(比如支付回调 IP 白名单、微信服务器 IP 白名单)。
第五维:运行时状态。 这一类最容易被忘:
# 队列里堆积的消息(迁移后要确认是清空还是保留)
redis-cli llen redis:queue
# 定时任务队列
mysql -e "SELECT COUNT(*) FROM wp_actionscheduler_actions WHERE status='pending';"
# 未完成的订单/支付回调
mysql -e "SELECT COUNT(*) FROM orders WHERE status IN ('pending','paid');"
清单做完后,把它存到旧机和新机各一份。 迁移过程中你不可能一直记得所有细节,而问题往往在你以为已经搞定的时候冒出来。
还有一件事必须先做:确认旧机上的服务是怎么启动的。 手动装的软件往往没写进 systemd,重启后起不来:
# 列出所有正在运行的服务
systemctl list-units --type=service --state=running
# 找出没有对应 systemd 单元的进程(这些是手工 nohup 起的,重启就死)
ps aux --sort=-%mem | head -30 | awk '{print $11}' | while read p; do
full=$(readlink -f /proc/$(pgrep -f "$p" | head -1)/exe 2>/dev/null)
echo "$p -> $full"
done | sort -u
# 检查 supervisor / pm2 这类进程管理器
supervisorctl status 2>/dev/null
pm2 list 2>/dev/null
用 nohup 手工拉起来的服务是迁移最常见的坑——旧机上跑得好好的,新机上一重启就全没了。找到之后,改写成 systemd unit 再迁。
二、新机基线:先装成和旧机一样,再迁数据

顺序绝对不能反:先配环境,再搬数据。 反过来的结果是文件传过去发现 PHP 版本不对、扩展没装、数据库字符集不一致,然后全部重来。
第一步:装系统并选对版本。 新机的系统版本应该和旧机一致或更新(不要降级)。如果旧机是 Ubuntu 22.04,新机就装 22.04 或 24.04,不要装 Debian。
第二步:装 Web 服务栈,版本对齐。
# Debian/Ubuntu
sudo apt update
sudo apt install nginx php8.2-fpm php8.2-mysql php8.2-mbstring php8.2-xml php8.2-curl php8.2-zip php8.2-gd php8.2-intl php8.2-imagick php8.2-fpm mysql-server
# 确认扩展和旧机一致
php -m | sort > /tmp/new_modules.txt
diff /tmp/env_php_modules.txt /tmp/new_modules.txt
这个 diff 是整个基线配置里最有价值的一步,它能直接告诉你少了哪个扩展。少一个 imagick 或 intl,WordPress 后台就会报错。
第三步:装数据库并对齐配置。 版本、字符集、排序规则都要一致:
# 检查旧机的数据库配置
mysql -e "SHOW VARIABLES LIKE 'character_set_%';"
mysql -e "SHOW VARIABLES LIKE 'collation_%';"
mysql -e "SELECT VERSION();"
# 新机按同样的配置建库
mysql -e "CREATE DATABASE yourdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
字符集不一致是迁移最经典的翻车点:旧库是 utf8mb4_general_ci,新库是 utf8mb4_0900_ai_ci,导入时中文不报错但排序和比较结果变了,搜索功能会出现「明明有这篇文章却搜不到」。
第四步:对齐 PHP 配置。 直接从旧机拷贝 php.ini 的关键项,比手动回忆靠谱:
# 拷贝关键配置(排除路径相关的部分)
sudo cp /etc/php/8.2/fpm/php.ini /tmp/php_old.ini
sudo cp /etc/php/8.2/fpm/php.ini /tmp/php_new.ini
# 对比关键项
diff <(grep -E "^(memory_limit|max_execution_time|upload_max_filesize|post_max_size|display_errors|error_reporting)" /tmp/php_old.ini) \
<(grep -E "^(memory_limit|max_execution_time|upload_max_filesize|post_max_size|display_errors|error_reporting)" /tmp/php_new.ini)
第五步:装完应用本体和依赖。 用 Composer 的项目最简单:
# 拷贝 composer.json 和 lock 文件过去再安装
composer install --no-dev --optimize-autoloader
WordPress 站点则是拷贝 wp-content 目录后要重新装核心:
# 只拷 wp-content,核心文件重新下载
wp core download --version=$(wp core version --path=/var/www/old) --force --path=/var/www/new
新机上的证书怎么办,有两个选择:
选择一:重新签发。 最简单,但切换时要重新验证,Let's Encrypt 的 HTTP 验证需要新机的 80 端口能访问到域名——而 DNS 还指向旧机,所以签不下来。解决办法是先用 hosts 强制指向新机,或者用 DNS-01 验证。
选择二:把证书和私钥一起拷过去。 这样切换瞬间就能用,不依赖任何外部服务:
# 拷贝整个 letsencrypt 目录(包含证书和私钥)
rsync -avz -e "ssh -p 10022" root@旧机IP:/etc/letsencrypt/ /etc/letsencrypt/
# 恢复权限(这一步不能省,否则 nginx 读不了私钥)
sudo chown -R root:root /etc/letsencrypt
sudo find /etc/letsencrypt -type f -name "*.key" -exec chmod 600 {} \;
sudo find /etc/letsencrypt -type f -name "*.pem" -exec chmod 644 {} \;
更推荐的是提前用 DNS-01 验证在新机签发,这样连拷贝私钥这一步都省了,也不存在私钥在两台机器之间流转的风险。
# 用 certbot 的 DNS 插件(需要先在 DNS 服务商处配置 API 凭证)
sudo certbot certonly --dns-cloudflare \
-d yourdomain.com -d www.yourdomain.com \
--agree-tos -m [email protected] --non-interactive
无论用哪种方式,都要确认续期定时器在新机上也在跑(见第九节的老机退租部分同样适用):
sudo systemctl enable --now certbot.timer
sudo certbot renew --dry-run
第六步:新机的网络与安全基线。 别忘了这一节——很多人在新机上裸奔上线:
# 防火墙只放行必要端口
sudo ufw default deny incoming
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
# 检查所有监听端口,确认没有意外暴露的数据库
sudo ss -tulnp
第七步:装监控和日志,先于上线。 这一步经常被推到「上线后再弄」,结果是新机出问题时你完全没有数据可查:
# 装基础监控(轻量,适合 1 到 2 核的机器)
sudo apt install -y netdata
sudo systemctl enable --now netdata
# 访问 http://新机IP:19999 看实时面板
# 装日志收集
sudo apt install -y prometheus-node-exporter
sudo systemctl enable --now prometheus-node-exporter
至少要能在新机上看到这四个指标: CPU 使用率、内存使用率、磁盘使用率、网络进出流量。这四个数是判断「新机是否比旧机慢」的唯一客观依据,没有它们,你的性能对比只能靠感觉。
# 最简版本:一条命令看全部
top -bn1 | head -5
free -m | head -2
df -h / | tail -1
cat /proc/net/dev | awk 'NR>2 {split($1,a,":"); printf "%-8s 收 %8.2fGB 发 %8.2fGB\n", a[1], $2/1073741824, $10/1073741824}'
三、第一次全量同步:rsync 的正确用法

数据同步用 rsync 而不是 scp 或 zip 打包,因为 rsync 支持断点续传、增量传输、保留权限和属主,这三点在搬家场景里都是刚需。
第一次全量同步:
# 完整复制网站目录,保留权限、时间戳、软链接
rsync -avz --info=progress2 \
--exclude='cache/' \
--exclude='*.log' \
-e "ssh -p 10022" \
root@旧机IP:/var/www/html/ /var/www/html/
参数逐个解释:
- -a 是归档模式,等于 -rlptgoD,包含递归、保留软链接、保留权限、保留时间、保留属组属主、保留设备文件。
- -v 详细输出,-z 压缩传输(对文本文件有效,二进制收益不大但无害)。
- --info=progress2 显示整体进度百分比,比逐文件输出更适合大目录。
- --exclude 排除不该传的东西。这个列表必须根据实际情况调整,至少要排除缓存目录、日志文件、上传的临时文件。
关键的排除项清单:
| 排除项 | 原因 |
|---|---|
cache/ |
WordPress 缓存,新机重新生成即可,传输它浪费时间还可能引入脏数据 |
*.log |
日志文件不需要迁移 |
node_modules/ |
体积巨大,新机用 npm ci 重装更快 |
.git/ |
如果不需要在新机保留版本历史,可以排除 |
*.sql.gz |
数据库导出包本身不需要拷到网站目录 |
wp-content/uploads/cache/ |
图片优化插件的缓存 |
关于属主问题。 rsync 传输时会保留原属主,如果新机的 Web 用户名和旧机不同(比如旧机是 www-data,新机是 nginx),文件属主就会错。解决方式有两个:
# 方式一:传完后统一改属主(推荐,简单可靠)
sudo chown -R www-data:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} \;
sudo find /var/www/html -type f -exec chmod 644 {} \;
# 方式二:传输时不保留属主,传完再改
rsync -avz --no-owner --no-group -e "ssh -p 10022" root@旧机IP:/var/www/html/ /var/www/html/
方式一更稳。 直接在旧机上执行一次 rsync 带 --chown 也可以:
rsync -avz --chown=www-data:www-data -e "ssh -p 10022" \
root@旧机IP:/var/www/html/ /var/www/html/
验证同步是否完整。 用文件数量和总大小对比:
# 旧机
ssh -p 10022 root@旧机IP "find /var/www/html -type f | wc -l; du -sb /var/www/html"
# 新机
find /var/www/html -type f | wc -l; du -sb /var/www/html
两个数字都要比,缺一个都可能漏东西。 文件数对得上但大小差很多,通常是某个大文件没传完(常见于 rsync 中断)。
关于 rsync 的一个重要选择:用 -a 还是用 --archive 之外的方式。 默认的 -a 会保留原文件的属主和权限,这在新旧机用户名不同时会造成权限错乱。前面说的 --chown 参数就是解决这个问题的。如果两台机器的用户名完全一致,用 -a 最省事;如果不一致,必须带 --chown。
另一个选择:rsync 走 SSH 的加密开销要不要省掉。 rsync 默认走 SSH,数据是加密的。对大批量小文件(几万个文件、几百 MB)来说,加密会显著拖慢速度。可以用 -e "ssh -c [email protected] -C" 换更快的加密套件,但要注意 -C(压缩)只对文本文件有效,对已经压缩过的图片和视频是负优化。
大文件场景建议单独处理。 你的 uploads 目录如果有几十 GB 的视频文件,rsync 逐个传会非常慢:
# 先同步除大文件外的所有内容(--max-size 排除大文件)
rsync -avz --max-size=100M -e "ssh -p 10022" root@旧机IP:/var/www/html/ /var/www/html/
# 大文件单独用 scp 或 rsync 传输
rsync -avz --min-size=100M --progress -e "ssh -p 10022" \
root@旧机IP:/var/www/html/wp-content/uploads/ /var/www/html/wp-content/uploads/
四、数据库迁移:一致性是核心

文件可以增量同步,数据库不能——除非你用二进制日志做主从复制,否则没有「增量同步」这回事。所以数据库迁移的流程必然是:先导一次全量,最后切之前再导一次增量(或者做短暂停机写)。
方案一:mysqldump 全量迁移(最常用)。
# 旧机:导出(注意 --single-transaction 保证 InnoDB 表一致性)
mysqldump -u root -p --single-transaction --quick --routines --triggers --events --default-character-set=utf8mb4 \
yourdb > /tmp/yourdb.sql
# 压缩后传输(大库能省 80% 以上)
gzip -9 /tmp/yourdb.sql
# 传到新机
scp -P 10022 /tmp/yourdb.sql.gz root@新机IP:/tmp/
# 新机:导入
gunzip -c /tmp/yourdb.sql.gz | mysql -u root -p --default-character-set=utf8mb4 yourdb
--single-transaction 这个参数必须加。 它让 mysqldump 在一个一致性快照里读取 InnoDB 表,不需要锁表,业务可以继续跑。不加的话,导出期间表被写入就会导致 dump 报 "table is being updated" 或者数据不一致。
--quick 也不能省,它让 mysqldump 逐行读取而不是一次性载入内存,避免大表把内存撑爆。
方案二:物理拷贝(最快,适合超大库)。
# 停 MySQL 后直接拷数据目录(最干净,但需要停机)
sudo systemctl stop mysql
sudo rsync -avz --delete -e "ssh -p 10022" \
root@旧机IP:/var/lib/mysql/ /var/lib/mysql/
sudo systemctl start mysql
物理拷贝必须保证两边 MySQL 版本完全一致,否则数据文件格式不兼容直接启动失败。
方案三:用 mysqldump 的表级并行导出(库很大时更快)。
# 列出所有表
mysql -N -e "SHOW TABLES;" yourdb > /tmp/tables.txt
# 逐表并行导出
cat /tmp/tables.txt | xargs -P 4 -I{} sh -c 'mysqldump -u root -pPASS --single-transaction --quick yourdb {} > /tmp/dump_{}.sql'
# 合并后导入
cat /tmp/dump_*.sql > /tmp/yourdb.sql
字符集与排序规则的对齐在导入后必须再验一次:
mysql -e "SELECT @@character_set_database, @@collation_database;"
mysql -e "SHOW VARIABLES LIKE 'character_set_server';"
还有一个容易漏的检查:存储过程、触发器、事件调度器。 普通的 mysqldump 不含它们,必须带 --routines --triggers --events(上面命令里已经带了)。导入后确认:
mysql -e "SHOW PROCEDURE STATUS WHERE Db='yourdb';"
mysql -e "SHOW TRIGGERS FROM yourdb;"
mysql -e "SELECT * FROM information_schema.EVENTS WHERE EVENT_SCHEMA='yourdb';"
MySQL 用户和权限也要单独迁移。 mysqldump 导的是业务库,不含 mysql 系统库里的账号:
# 旧机导出账号(只导用户相关表)
mysqldump -u root -p --single-transaction mysql \
mysql.user mysql.db mysql.tables_priv mysql.columns_priv mysql.global_grants > /tmp/mysql_users.sql
# 新机导入
mysql -u root -p < /tmp/mysql_users.sql
最后一步:切换前的最终增量同步。 这是把停机时间压到最短的关键:
# 1) 停掉旧机上所有会写数据库的应用(不是停 MySQL,是停应用)
sudo systemctl stop php8.2-fpm
sudo systemctl stop nginx
# 2) 导出最后的增量数据
mysqldump -u root -p --single-transaction --quick --routines --triggers --events \
yourdb | gzip -9 | ssh -p 10022 root@新机IP "gunzip -c | mysql -u root -p --default-character-set=utf8mb4 yourdb"
# 3) 再同步一次文件增量(这时基本没变化了,几秒完成)
rsync -avz --delete -e "ssh -p 10022" root@旧机IP:/var/www/html/ /var/www/html/
# 4) 现在才改 DNS
这个流程的停机时间就是第 1 步到第 4 步之间的数据量。 如果你的站点写入量不大,通常在 1 到 3 分钟。关键是把 DNS 切换放在最后一步,这样停机窗口是可控的。
如果不能接受任何停机,就用主从复制,参考第七节的方案三。
五、DNS 切换:TTL 要提前降

DNS 是整个搬家过程里唯一不受你控制的部分。文件几分钟就传完了,但 DNS 生效可能需要几十分钟到几小时,而且不同用户的递归解析器缓存时间不一样。
必须在搬家前 24 到 48 小时就把 TTL 降到 60 秒。 这是整个迁移里最重要的一条时间管理建议。
# 查看当前 TTL
dig +nocmd yourdomain.com A | grep -A1 "ANSWER SECTION"
# 在 DNS 服务商后台把 A 记录的 TTL 改成 60
为什么必须提前降 TTL: 递归 DNS 服务器会把解析结果缓存到 TTL 结束为止。如果你在切换时才降 TTL,那些在切换前 30 分钟才缓存过解析的服务器仍然会继续把流量导向旧机,而且你不知道它们什么时候才更新。这就是「明明改了 DNS,怎么还有流量走老服务器」的根源。
切换时的操作顺序:
第一步:确认新机完全就绪。 在真正改 DNS 之前,用本地 hosts 文件强制解析到新机测试:
# 在你自己的电脑上测试新机(不影响其他人)
echo "新机IP yourdomain.com" | sudo tee -a /etc/hosts
curl -I https://yourdomain.com
# 测试完记得删掉这行
或者更干净的做法:直接改新机上的 Nginx server_name 加上测试域名。
第二步:把 TTL 降到 60 秒后再等 10 分钟,让之前的缓存过期。
第三步:切换 A 记录指向新机。
第四步:确认流量真的切过来了。 在旧机上监控访问日志:
# 旧机上持续观察,看请求量是否下降
sudo tail -f /var/log/nginx/access.log | awk '{print $NF}' | uniq -c
# 或者按分钟统计请求数
watch -n 10 "sudo awk '{print \$4}' /var/log/nginx/access.log | cut -c2-16 | uniq -c | tail -5"
第五步:TTL 什么时候调回 3600? 建议切换后至少观察 48 小时,确认流量完全稳定、旧机没有任何请求之后再调回。过早调回会让后续的第二次切换(比如回滚)又变慢。
关于 CNAME 和 CDN 的特殊情况:
- 如果域名用了 CDN,改的是 CDN 面板里的源站 IP,不是 DNS 的 A 记录。而且 CDN 有多层缓存(边缘节点 + 中间层),切换生效更慢。
- 如果用的是 Cloudflare,改的是源站设置,而且要注意「已缓存的页面」需要手动刷新。
- 如果用了负载均衡(多 IP 的 A 记录),轮询会持续把流量打到两台机器上,切换期间两台都在服务旧数据会导致数据不一致。
# 检查当前解析到哪些 IP
dig +short yourdomain.com
# 查看所有权威服务器的解析(检查是否已同步)
dig +trace yourdomain.com | grep -E "^\S+\s+\d+\s+[A-Z]+\s+[A-Z]\s+\S+\s+\S+\s+\S+\s+A"
六、切换后的验证清单:逐条打勾,别凭感觉

迁移完成的判断标准不是「网站能打开」,而是「每一个功能都验证过」。 下面这份清单建议逐条执行并记录结果。
第一组:基础设施层。
# 1. DNS 是否已经指向新机
dig +short yourdomain.com
# 应该只返回新机 IP
# 2. 实际连接到了哪台机器(看响应头里的服务器标识或做个标记)
curl -sI https://yourdomain.com | grep -i "server\|x-powered-by"
# 3. 在新机确认请求真的到了
sudo tail -20 /var/log/nginx/access.log
# 4. 所有必需端口正常
sudo ss -tulnp | grep -E ":80|:443|:3306"
第二组:网站功能层。 这部分必须人工点,因为脚本测不出来:
| 检查项 | 怎么测 | 合格标准 |
|---|---|---|
| 首页 | 浏览器打开 | 内容完整,样式无错乱 |
| 文章详情页 | 打开 3 到 5 篇不同时间的文章 | 内容正确,图片正常 |
| 后台登录 | 登录 WordPress 后台 | 能登录,无数据库错误 |
| 发文章 | 在后台发一篇测试文章并预览 | 发布成功,前台能访问 |
| 上传图片 | 上传一张图片 | 上传成功,前台显示正常 |
| 插件功能 | 逐个点开已启用插件的页面 | 无报错 |
| 表单提交 | 提交一次留言/搜索 | 提交成功,数据入库 |
| 邮件发送 | 触发一次发信 | 邮件能收到(SMTP 配置要对) |
| 定时任务 | 后台确认 cron 任务状态 | 显示正常执行,无报错 |
| HTTPS | 访问 https:// | 证书有效,无告警 |
| 混合内容 | 浏览器 F12 Console | 无 mixed content 报错 |
第三组:数据一致性验证。 这条最关键——数据不一致往往不会报错,只会在几天后以「用户反馈文章不见了」的形式出现。
-- 对比表行数(两边都要跑,数字必须一致)
SELECT COUNT(*) FROM wp_posts;
SELECT COUNT(*) FROM wp_users;
SELECT COUNT(*) FROM wp_comments;
SELECT COUNT(*) FROM wp_options;
-- 对比最新一条记录的时间戳,确认数据是新的
SELECT MAX(post_date) FROM wp_posts;
-- 检查是否有重复的 slug(重复导入的典型症状)
SELECT post_name, COUNT(*) FROM wp_posts WHERE post_type='post' GROUP BY post_name HAVING COUNT(*)>1;
-- 对比文章总数与总数校验和(更严格)
SELECT COUNT(*), SUM(post_date_gmt) FROM wp_posts;
# 文件层面的一致性
# 对比文件数和总大小
find /var/www/html -type f | wc -l
du -sb /var/www/html
# 抽查几个关键文件的内容
md5sum /var/www/html/wp-config.php
第四组:性能对比。 新机的表现应该和老机持平或更好,如果明显更差说明有配置没对齐:
# 响应时间对比
curl -o /dev/null -s -w "TTFB: %{time_starttransfer}s TOTAL: %{time_total}s\n" https://yourdomain.com/
# 在新机上确认 Opcache 生效
php -i | grep -A5 "opcache.enable"
第五组:自动化校验脚本。 上面这些检查如果每次手动敲,验证会流于形式。写成一个脚本,切换后跑一次就有明确结论:
cat > /tmp/verify_migration.sh << 'EOF'
#!/bin/bash
echo "=== 迁移验证 ==="
echo "[1] DNS 解析:"
dig +short yourdomain.com
echo "[2] HTTP 状态码:"
curl -o /dev/null -s -w " HTTP %{http_code} TTFB %{time_starttransfer}s TOTAL %{time_total}s\n" https://yourdomain.com/
echo "[3] HTTPS 证书:"
echo | openssl s_client -connect yourdomain.com:443 2>/dev/null | openssl x509 -noout -dates | sed 's/^/ /'
echo "[4] 关键端口:"
ss -tuln | grep -E ":80|:443" | sed 's/^/ /'
echo "[5] 站点内容抽查:"
curl -s https://yourdomain.com/ | grep -o "<title>.*</title>" | sed 's/^/ /'
echo "[6] 数据库连通性:"
php -r '$m=new mysqli("127.0.0.1","root","","yourdb"); echo $m->connect_error ? " FAIL: ".$m->connect_error."\n" : " OK: connected\n";'
echo "[7] 磁盘与内存:"
df -h / | tail -1 | sed 's/^/ /'
free -m | head -2 | tail -1 | sed 's/^/ /'
EOF
chmod +x /tmp/verify_migration.sh
/tmp/verify_migration.sh
这个脚本的价值在于它可以反复跑。 切换后立刻跑一次、切 DNS 一小时后跑一次、第二天再跑一次,三次结果一致才算迁移真正稳定。很多问题只在某次缓存刷新或定时任务运行时才暴露。
七、回滚方案:三条路,按可用性排序

没有回滚方案的迁移不是迁移,是赌博。 在动手之前,你必须知道出问题时能退回去,退回去要多久。
方案一:DNS 回切(最快,1 到 10 分钟)。
前提是:旧机在你观察期内保持运行,且数据没被新机写乱。 这个方案适用于「新机是纯净环境、几乎没有写入新数据」的场景,比如刚做完切换还没开放后台编辑。
# 把 DNS A 记录改回旧机 IP
# 然后清理本地 DNS 缓存加速生效(测试用)
sudo systemd-resolve --flush-caches
# 或
sudo kill -HUP $(pgrep systemd-resolved)
风险点:DNS 生效时间不确定。 保守估计 10 分钟内大部分流量回切,但这期间新老两机都在跑,如果新机上有写入操作,会产生数据分叉。所以方案一只适用于「切换后短时间内无写入」的情况。
方案二:反向同步回旧机(较慢,30 分钟到 2 小时)。
如果新机已经跑了几天并产生了新数据,就不能简单 DNS 回切了——那样会丢掉新数据。正确做法是先把新机的数据同步回旧机,再切 DNS:
# 把新机的数据反向同步回旧机
rsync -avz --delete -e "ssh -p 10022" \
/var/www/html/ root@旧机IP:/var/www/html/
# 数据库反向导入
mysqldump -u root -p --single-transaction --quick yourdb | \
ssh -p 10022 root@旧机IP "mysql -u root -p yourdb"
反向同步时用 --delete 要格外小心,它会删除目标端多出来的文件。执行前先做一次不带 --delete 的 dry-run:
rsync -avzn --delete --itemize-changes -e "ssh -p 10022" \
/var/www/html/ root@旧机IP:/var/www/html/ | head -50
-n 是 dry-run,--itemize-changes 会列出将要发生的每一处改动。看清楚列表里的删除项,确认都是有预期的,再去掉 -n 真正执行。
方案三:旧机保持运行做热备(最稳,但成本最高)。
这是最专业的做法:旧机不关机,和新机并行运行,通过数据库主从或定时同步保持一致。 这样任何时候切回去都是「切到一台一直热着的机器」,停机时间接近零。
# 旧机作为主库,新机作为从库(MySQL 主从复制)
# 旧机 my.cnf
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog_format = ROW
# 新机 my.cnf
[mysqld]
server-id = 2
relay-log = relay-bin
read_only = ON
这个方案的前提是旧机能多跑几天,意味着你要为两台机器付双份费用。但对业务关键的系统,这笔钱远比一次数据丢失便宜。
回滚决策表:
| 情况 | 回滚方案 | 停机时间 |
|---|---|---|
| 切换 5 分钟内发现配置错 | DNS 回切 | 1 到 10 分钟 |
| 切换后当天发现功能问题 | 反向同步 + DNS 回切 | 30 分钟到 2 小时 |
| 切换后 2 到 3 天发现数据问题 | 主从方案下切回从库 | 分钟级 |
| 新机跑了一周后要彻底换回 | 反向同步 + 校验 + DNS 回切 | 数小时 |
八、容易漏掉的细节:那些迁移后才暴露的坑

坑一:文件权限和 SELinux/AppArmor 上下文。 传过去的文件属主对了,但 SELinux 标签不对照样 403:
# CentOS 系:传完后重建上下文
sudo restorecon -R -v /var/www/html
# 或者
sudo chcon -R -t httpd_sys_content_t /var/www/html
# Ubuntu 系:AppArmor 不需要手动处理,但要看有没有报错
sudo aa-status | head -20
坑二:上传目录不能有执行权限。 上传目录里如果允许执行 PHP,webshell 就能直接跑:
# uploads 目录禁止执行 PHP
sudo find /var/www/html/wp-content/uploads -type f -name "*.php" -exec chmod 644 {} \;
sudo find /var/www/html/wp-content/uploads -type d -exec chmod 755 {} \;
坑三:邮件服务器的发信配置。 很多 VPS 默认 25 端口被封,迁移后邮件发不出去:
# 测试本机发信
sudo swaks --to [email protected] --from [email protected] --server localhost
# 检查 25 端口是否被封
nc -zv smtp.yourprovider.com 25
nc -zv smtp.yourprovider.com 587
用 587 端口提交认证通常比 25 端口可用。如果只有 25 可用但发不出去,通常是 IP 被列进了信誉黑名单,这时候换台机器不一定能解决,得找服务商解封。
坑四:第三方 IP 白名单。 支付回调、微信支付、短信服务、对象存储的 bucket policy、云厂商的安全组——这些都绑定在旧机的 IP 上,换机后全部失效:
# 检查云厂商安全组(不同厂商命令不同,登录控制台确认)
# 找到所有引用了旧 IP 的配置
sudo grep -rn "旧机IP" /var/www/html --include="*.php" --include="*.env" --include="*.json" 2>/dev/null
这条命令非常有用,它能把网站目录里所有硬编码了旧 IP 的地方一次找出来。
坑五:数据库连接数没对齐。 新机数据库 max_connections 用默认值,但 PHP 的连接池按旧机配置设的,结果新机数据库连接被打满:
# 检查两个数字
mysql -e "SHOW VARIABLES LIKE 'max_connections';"
mysql -e "SHOW STATUS LIKE 'Max_used_connections';"
坑六:cron 任务在新机重复执行。 如果新旧机同时跑同一个定时任务(比如发邮件、对账、爬取),会重复发、重复扣款。切换前一定要确认旧机的 cron 已经停掉:
# 停掉旧机的 cron
sudo systemctl stop cron
# 或者只停掉特定的 crontab 行
crontab -l | grep -v "要保留的" | crontab -
坑七:宝塔/1Panel 面板的面板数据库没迁移。 面板本身只是壳,站点、计划任务、防火墙规则、数据库账号全在面板的数据库里。必须单独导出导入:
# 宝塔的面板数据库位置
/etc/panel/data/default.db
sqlite3 /etc/panel/data/default.db .tables
# 导出
sqlite3 /etc/panel/data/default.db ".dump" > /tmp/panel_dump.sql
# 新机导入
sqlite3 /etc/panel/data/default.db < /tmp/panel_dump.sql
坑八:备案和绑定的关系。 如果旧机在大陆机房且网站做了 ICP 备案,换到海外 IP 后备案可能失效,需要去工信部更新备案信息。备案信息里绑的是 IP,海外服务器无法备案,这部分要提前和主管部门确认。
九、迁移后的老机什么时候关:别急着退

很多人在切换成功后 5 分钟就退掉了旧机,这是最危险的做法。 旧机应该至少保留 7 天,观察期覆盖所有业务周期。
保留期间的正确做法:
第一步:让旧机保持可回退状态。 不要在旧机上做任何写入(不要在上面继续发文章),但保留所有数据和配置不动。
第二步:监控旧机的流量归零。 这是判断是否可以退款的唯一可靠依据:
# 在旧机上统计最近一小时的请求数
sudo awk '$4 ~ /05\/Nov\/2026/ {print substr($4,2,6)}' /var/log/nginx/access.log | sort | uniq -c | tail -10
# 实时观察(每 10 秒刷新)
watch -n 10 "sudo ss -tn state established | wc -l"
要连续 24 小时看到请求数为 0,才能认为流量真的切干净了。 有些客户端(比如手机 App 的后台轮询、移动浏览器的 DNS 缓存)可能好几天才请求一次。
第三步:观察期结束后,做一次完整的最终校验。
# 用旧机的数据做交叉校验(此时旧机应完全静止)
sudo tar czf /tmp/final_backup_$(date +%F).tgz /var/www/html
mysqldump -u root -p --single-transaction --quick yourdb > /tmp/final_db.sql
# 把最终备份存到异地(这是最后的保险)
第四步:退订前检查账单周期。 如果是按年付费,退订通常不退剩余款项;如果是按月付费,退订前确认下个计费日。很多人在这一点上白扔钱——因为「先留着观察」拖过了计费日。
算一笔账再决定观察期多长。 按月付通常有月付溢价,年付一般能便宜 20% 到 35%。观察期每延长一天,旧机就多花一天钱。 合理的算法是:
观察期天数 = 业务的完整周期 + 2 天缓冲。
业务的完整周期是指什么?对内容站来说是一个周更或月更的完整周期;对电商来说要覆盖一次大促;对带账期的系统要覆盖最长账期。如果你的业务是每天都有流量波动,那就至少观察 7 天;如果有明显的周周期(比如周末流量是工作日的 3 倍),就得观察完整的 14 天,否则你可能正好在流量低谷时判定迁移成功。
# 看旧机过去 14 天的流量分布,确定业务周期
sudo awk '{split($4,t,":"); print t[1]" "t[2]}' /var/log/nginx/access.log | sort | uniq -c
第五步:销毁密钥。 这条经常被忽略但很重要:
# 删除旧机上的所有 SSH 密钥对
rm -f ~/.ssh/id_*
# 清空 authorized_keys
> ~/.ssh/authorized_keys
# 重置 root 密码(如果还想保留机器做其他用途)
sudo passwd root
如果旧机可能落入他人之手,销毁密钥比删除文件更重要——因为密钥一旦泄露,别人拿到 IP 就能进来。
第六步:保留一份离线的最终备份。 在删机器之前,把最终的数据备份存到对象存储或本地其他磁盘。「机器可以随时重建,数据丢了找不回来」是很多事故的真正原因。
按你的情况,下一步该看哪家
上面讲的是判断标准,但多数人最后还是想问一句「那我该买谁」。按你的实际情况对照下面几条,直接去看对应的机型页比继续比价更快:
- 只想搭个站、预算优先:先看价格最低那档够不够用,别为用不上的配置付钱。看 justg 当前机型与价格
- 做跨境或店群、需要多 IP:优先看 IP 类型与换 IP 政策,别只看配置。看 vpckr 当前机型与价格
- 只是开发测试、临时用:按小时或月付,用完就删,别买年付。看 zji 当前机型与价格
价格与库存以商家页面当日显示为准,下单前建议先确认退款窗口与是否支持按月付费。
十、常见问题

迁移过程中网站要停多久? 分三种情况。如果数据库能接受短暂停机(用 mysqldump 导完最后一份增量后停写几分钟),停机时间通常在 5 分钟以内。如果用主从复制,可以做到几乎零停机——写操作在旧机主库,切 DNS 后新机变主库。如果只是 rsync 全量同步不停机,会有几分钟到几十分钟的数据不一致窗口(用户在这期间提交的数据会丢失),只适合后台管理系统这类能接受少量数据丢失的场景。
DNS 切换后为什么还有流量走旧机器? 三个原因,按可能性排序:一是你没提前降 TTL,递归服务器还在用旧缓存;二是有些客户端或路由器内置的 DNS 缓存不会立刻更新;三是如果是 CDN 或云负载均衡,改的不是 DNS 而是源站配置。解决办法是提前 48 小时降 TTL,并在切换后同时检查 DNS 面板和 CDN 面板两处配置。
rsync 传完之后网站还是显示旧内容? 最常见的原因是 WordPress 的缓存没清。WordPress 会把页面 HTML 缓存到对象缓存或数据库里,wp-content/cache 目录虽然通常被排除了,但 Redis 里的缓存还在。在新机上执行 wp cache flush,同时清理 Redis 的数据库。 另外检查 wp-config.php 里的 WP_HOME 和 WP_SITEURL 是不是还写着旧域名。
数据库导入后中文变成乱码? 字符集问题。三处要对齐:mysqldump 时带 --default-character-set=utf8mb4,导入时也带同样的参数,建库时用 CHARACTER SET utf8mb4。如果已经导进去了乱码,重建库重新导入是最干净的办法,事后用 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 修补很容易留下混合编码。
新机的定时任务不执行怎么办? 三个检查点:一是 systemctl status cron 确认服务在跑;二是 crontab 里的命令路径是否用绝对路径(相对路径在新机的目录结构下会失效);三是脚本本身有没有执行权限(chmod +x)。另外注意时区:timedatectl 确认时区设置,服务器 UTC 和本地 UTC+8 差 8 小时,这会让定时任务的执行时间全部偏移。
迁移后邮件发不出去? 检查三件事:25 端口是否被服务商封(试着用 587 提交);SMTP 的认证信息是否正确;发信服务器的 IP 是否在你原来那个 IP 的白名单里(如果你用的是企业邮箱或邮件服务商的白名单机制)。另外 WordPress 的邮件要装 SMTP 插件并配置好,否则默认的 mail() 函数在很多环境下根本不工作。
需要把旧机的 IP 保留下来吗? 取决于有没有东西绑在旧 IP 上。有三类东西会绑 IP:第三方服务的 IP 白名单(支付回调、微信/支付宝、短信服务)、已经签发的 SSL 证书(某些 OV 证书验证绑 IP)、以及你自己写的代码里硬编码的地址。如果这些都迁移了(改成域名而不是 IP),旧 IP 就可以立即释放。
能不能直接用面板的一键迁移功能? 面板的「一键迁移」通常只做文件传输和数据库导入,不做配置对齐、不做数据校验、不管 DNS 切换。它的定位是省掉手工传文件的麻烦,不是省掉整个迁移流程。用它传数据可以,但配置对齐和验证清单这两步你一步都不能省。
迁移过程中发现源站有安全问题怎么办? 先解决安全问题再迁,否则会把问题一起带到新机。如果你已经完成了迁移才发现,那就趁新机还没开放对外访问时重装一次——反正数据已经同步过去了,重装系统不会丢数据。这是新机唯一的优势:它的初始状态是干净的。

喜欢
高兴
鬼脸
呵呵
无聊
伤心