教程 · 运维与排障
把服务器迁移到另一个机房:一次不丢数据的搬家
难度
高级
预计时长
60 分钟
步骤
9 步
最近修订
基准系统
Debian 12
要点
把服务器迁移到另一个机房:一次不丢数据的搬家
迁移的正确顺序是:提前 24 小时把 DNS 的 TTL 降到 300 秒、在新机器上装好版本对齐的环境、用 rsync 预同步文件、停写之后做最后一次增量同步与数据库导入、切 DNS、观察 48 小时再销毁旧机器。真正的风险不在传输,而在切换期间新旧两台机器同时被写入造成的数据分叉。
前置条件
开始之前,先确认这些都具备
- 新旧两台服务器都能 SSH 登录,且已配置密钥
- 域名的 DNS 管理权限
- 一次可以接受的短暂停写窗口(通常几分钟到半小时)
操作步骤
共 9 步
步骤 01 / 09
先把要搬的东西列全 #
迁移事故几乎都出在「忘了带走某样东西」上,而不是文件传丢了。清单至少包括:站点文件、数据库、定时任务、systemd 单元、环境变量与 .env、TLS 证书、防火墙规则、以及所有在第三方服务里登记过本机 IP 的地方(API 白名单、数据库远程访问白名单、邮件服务)。换机房意味着 IP 一定会变,这一条最容易被漏掉。
Shell # 在旧机器上把这些导出来存好 crontab -l > ~/migrate/crontab.txt sudo systemctl list-units --type=service --state=running > ~/migrate/services.txt sudo ufw status numbered > ~/migrate/ufw.txt dpkg --get-selections > ~/migrate/packages.txt ls /etc/letsencrypt/live/ > ~/migrate/certs.txt步骤 02 / 09
提前 24 小时把 DNS TTL 降下来 #
TTL 决定了全球的 DNS 缓存多久之后才会看到新 IP。平时设成 3600 秒没问题,但切换前必须提前降到 300 秒,而且要提前一个旧 TTL 的时长 —— 现在就改,明天再切。这一步没做,切换后会有访客持续几小时还在访问旧机器。
Shell # 在 DNS 服务商处把 A 记录 TTL 改为 300,然后确认 dig +noall +answer example.com @1.1.1.1步骤 03 / 09
在新机器上装好版本对齐的环境 #
版本不一致是迁移后「数据导进去了但跑不起来」的主要原因:PHP 8.2 的站点跑在 8.3 上、PostgreSQL 15 的导出文件恢复到 14 上,都会出问题。照着旧机器的版本装,先把环境跑通,再谈数据。
Shell # 在旧机器上确认版本 php -v; nginx -v; psql --version; node -v # 在新机器上装同样的大版本,并做一次空跑测试 sudo nginx -t步骤 04 / 09
预同步:先搬大头 #
在业务照常运行的情况下先同步一次,把绝大部分数据挪过去。这一次不用停机,可以慢慢传。-a 保留权限与时间戳,-H 保留硬链接,--numeric-ids 避免因两边用户 ID 不同而把属主搞乱。传输中断了重跑同一条命令即可续传。
Shell rsync -aHAX --numeric-ids --partial --info=progress2 \ -e 'ssh -p 22022' \ /var/www/ deploy@新机器IP:/var/www/ rsync -aHAX --numeric-ids --partial --info=progress2 \ -e 'ssh -p 22022' \ /etc/letsencrypt/ deploy@新机器IP:/etc/letsencrypt/步骤 05 / 09
停写窗口:最后一次增量 + 数据库导入 #
这是唯一需要停服务的一段时间。先停掉会写数据的服务(Web 应用、队列、定时任务),再做最后一次 rsync 和数据库导出导入。数据库必须在停写之后导出,否则导出的那一刻之后又产生的写入会永久丢失。
Shell # 旧机器:停止写入 sudo systemctl stop nginx pm2 stop all crontab -r # 或者临时注释掉所有任务(先确认已备份 crontab) # 最后一次增量同步 rsync -aHAX --numeric-ids --delete -e 'ssh -p 22022' /var/www/ deploy@新机器IP:/var/www/ # 数据库:导出并传输 sudo -u postgres pg_dump -Fc appdb -f /tmp/appdb-final.dump scp -P 22022 /tmp/appdb-final.dump deploy@新机器IP:/tmp/ # 新机器:导入 sudo -u postgres createdb appdb sudo -u postgres pg_restore -d appdb /tmp/appdb-final.dump步骤 06 / 09
切 DNS 之前,先用本地 hosts 验证新机器 #
改本地 hosts 文件把域名强行指向新机器,就能在不影响任何访客的前提下完整地验证一遍:首页、登录、上传、支付回调、后台。全部通过了再动 DNS。这一步能拦下大部分「切过去才发现少装了个扩展」的事故。
Shell # 本地电脑的 hosts 文件 # Linux/macOS: /etc/hosts # Windows: C:\Windows\System32\drivers\etc\hosts # 加一行: 新机器IP example.com www.example.com # 验证完记得删掉这一行步骤 07 / 09
切 DNS 并观察 #
改 A 记录,然后从多个公共解析器确认已经更新。旧机器的 Web 服务保持停止状态 —— 让还在用旧 IP 的访客看到错误,也好过让他们在旧机器上继续写数据造成分叉。观察 48 小时,确认没有请求再落到旧机器上。
Shell dig +short example.com @1.1.1.1 dig +short example.com @8.8.8.8 # 在新机器上确认流量确实进来了 sudo tail -f /var/log/nginx/example.com.access.log # 在旧机器上确认没有新请求了 sudo tail -n 100 /var/log/nginx/example.com.access.log步骤 08 / 09
证书与自动续期 #
如果整份 /etc/letsencrypt 都同步过来了,证书本身可以直接用。但 DNS 切换完成后建议重新跑一次续期演练,确认新机器上的验证路径通畅 —— 证书是那种「三个月后才会暴露问题」的东西。
Shell sudo certbot certificates sudo certbot renew --dry-run systemctl list-timers | grep -i certbot步骤 09 / 09
旧机器至少保留一周 #
省下几美元而立刻销毁旧机器,是这套流程里最不划算的决定。保留一周,期间你随时可以回滚,也可以回去取那些当初漏掉的文件。确认无误之后再删除 —— 注意删除之前先把数据备份到第三处。
Shell # 销毁前的最后一次全量备份(存到第三个位置) restic backup /etc /var/www /home /var/backups --exclude-caches restic snapshots
常见错误
这一步最容易踩的坑
下面每一条都对应一个真实会发生的故障。先读完再动手,比出问题之后再回来查要省时间。
- 切换期间新旧两台机器都在接收写入,数据从此分叉。这是迁移里最难修复的事故 —— 必须有一个明确的停写窗口。
- DNS 的 TTL 没有提前降低。切换后仍有访客被导向旧机器,持续时间等于旧的 TTL。
- 忘了搬定时任务、systemd 单元、环境变量或证书。它们不在 /var/www 里,rsync 抓不到。
- 数据库大版本不一致。导出文件恢复不进去,而这通常是在停写窗口里才发现的。
- 忘记 IP 变更会影响第三方白名单:支付回调、API 白名单、数据库远程访问、邮件发信。新 IP 的邮件发信声誉也需要重新积累。
- 切完立刻销毁旧机器,出问题时无法回滚。至少保留一周。
- rsync 没加 --numeric-ids,两边用户 ID 不同导致文件属主全乱,表现为各种莫名其妙的权限错误。
这篇教程的边界
- 命令以 Debian 12 为准。Ubuntu 与 Rocky Linux 的差异只在正文明确标注之处,未标注的部分请以你所用发行版的官方文档为准。
- 示例中的 IP 来自文档保留段 203.0.113.0/24,域名为 example.com,端口为示意值。复制后必须替换成自己的值, 原样执行不会有任何效果。
- 教程不能替代备份。任何会改动数据或线上流量的步骤,执行前先确认有一份验证过能恢复的备份。
- 本站不提供任何用于绕过网络审查的配置或说明,这篇也不例外。
发现命令过时或有误,请发邮件到 [email protected]。 指出错误的邮件比沉默的旧文档有价值得多。