你运行的每一台服务器,迟早都要搬一次家。主机商涨价了,或者开始索要你不想提交的证件,或者因为邻居的滥用投诉,把整个 /24 网段封掉一下午。问题从来不是你会不会迁移 — 而是这件事发生在你自己挑的某个周二早上,还是别人替你挑的某个周六深夜。
机制本身不是难点。拷贝文件就是 rsync,这你早就知道。真正让迁移出岔子的是时机:搬家过程中同时跑着三个互不同步的时钟,每一次真实的故障,都发生在它们之间的缝隙里。这份指南讲的就是这些缝隙 — 你本该一周前就调低的 DNS TTL、仍在被写入时就被拷走的数据库、只存在于你刚刚断电那台机器上的证书,以及旧主机商始终都能读到的那些凭据。
下面的命令假定两端都是 Debian 或 Ubuntu,应用是一个带数据库的 Web 应用,因为大多数人要搬的正是这个。无论是 Nextcloud、一个游戏服务器、一个机器人,还是一个 BTCPay 实例,走的都是同一套流程 — 只是数据那一步不同。
三个时钟,以及为什么“零停机”是一个错误的目标
一次迁移不是单一事件,而是三个走得不一样快的计时器,整套手艺就在于不让它们以糟糕的方式重叠:
- DNS 时钟。从你修改 A 记录的那一刻起,解析器会继续用旧地址作答,直到它们缓存的副本过期为止。切换那一刻,你已经控制不了这个时钟 — 你能控制它的时候,是几天前设置 TTL 的时候。
- 数据时钟。你手上最后一份数据副本,只是某一瞬间的快照。那一刻之后写入的一切,都只存在于旧机器上,除非你重放它或者提前让它停止发生,否则就会丢失。
- 会话时钟。正在进行中的上传、开着的 WebSocket、一个两分钟前解析了你主机名的支付处理商,四十秒后即将送达的付款回调。这些请求会落在发送方解析到的那台机器上,而不是你希望的那一台。
死磕字面意义上的零停机,意味着让这三个时钟同时运转,实际上就是让应用同时在两台服务器上运行,同时写入两个数据库。这是一个真正棘手的问题,也是不该拿来为单台 VPS 求解的问题。老老实实的目标要窄得多,也好达成得多:
没有访客看到错误,也没有一次写入丢失。一个九十秒的窗口,网站在线但只读,这两条都能满足,而且几乎没人会察觉。一次没有维护窗口的实时切换,悄悄丢掉最近四十分钟的表单提交,这两条一条都满足不了,而你会是从客户那里才知道这件事。
所以计划是:把只读窗口压到尽可能短,让它平淡无奇,并且可以撤回。下面的一切,都是为这三件事服务的。
你离开之后,旧主机商还留着什么
这部分常常被跳过,而它恰恰是很多人当初决定迁移的原因,所以值得说清楚。你的服务器住在别人硬件上的这段时间里,那家服务商能看到的东西包括:
- 整块磁盘。除非这块卷用LUKS 加了密、而且只有你自己能解锁,否则 hypervisor 能读到上面的每一个字节 — 密钥、令牌、数据库内容,全都不例外。即使加了密,一台正在运行的虚拟机的内存里,也装着解锁后的密钥。
- 这台机器用过的每一个凭据。环境变量文件里的 API 令牌、SMTP 密码、钱包守护进程的 RPC 密钥、你的 SSH 公钥,如果你曾经把私钥粘贴进过救援控制台,还不止这些。
- 开户时被要求提供的一切。姓名、银行卡、地址、用于短信验证的手机号、你登录时用过的那些 IP 地址。关掉账户并不会让这份清单变小,在大多数司法辖区,服务商还被要求把其中一部分保留数年。
这些都算不上什么阴谋 — 在别人的硬件上运行一台虚拟机,到哪儿都是这个意思,这里也不例外。真正要紧的是,迁移是你唯一能拿到的干净断点。新机器从全新的密钥、全新的令牌和全新的 IP 开始;如果你把旧的凭据一并搬了过去,你也就把旧的暴露面一并搬了过去,那次“断开”就只是做做样子。
所以,把旧机器上的每一个凭据都当成默认已经泄露,趁着搬家的机会重新签发。一次性花掉你一个小时。事后再单独去做这件事,是一件没人会真的抽出时间去做的事。而如果你搬家的部分原因,正是账户本身就是泄露源,用 Monero 支付新账户的费用,搭配一个无需 KYC 的账户,才是让这次断开变得真实、而非象征性的那一步 — 不过也要对自己诚实,想清楚这么做到底买来了什么、没买到什么,这是另一个话题了。
TTL 时钟,在搬家前一周就开始走
存活时间,是解析器在再次查询之前,被允许保留你这条记录的秒数。如果你的 A 记录用的是默认的 3600 秒 TTL,那么在你修改它的那一刻,一秒钟前刚查询过的解析器,还会在接下来的五十九分五十九秒里,继续把访客送到旧服务器。要是遇上很多注册商仍然默认使用的 86400,那就变成整整一天。
去查一下你实际上设的是多少 — 而不是你以为自己设了多少:
dig +noall +answer example.com A
dig +noall +answer www.example.com A
dig +noall +answer example.com MX
dig +noall +answer example.com NS每条应答的第二列就是 TTL,正在倒数。把搬家涉及的每条记录都调到 300 秒,而且至少要在切换前、以当前 TTL 两倍的时间提前做这件事:如果记录目前是 86400,光是这次修改本身要让全世界都看到,就得花上一天,这正是 DNS 迁移里那个绕不开的递归笑话。
有两个细节最容易让人栽跟头:
- NS 记录有它自己的 TTL,通常很长,而且是在注册商那里设置的,不在你的区域文件里。这只有在你同时也要更换名称服务器时才要紧 — 而这件事,你应该避免和搬家安排在同一周里做。先换主机,稳定下来,如果还想换 DNS 服务商,再换。两个变量,两个周末,分开处理。
- “DNS 传播”根本不存在这回事。没有什么东西在“传播”;只是缓存过期了而已。这里没有队要排,也没有哪个按钮能让更新推送得更快。唯一的杠杆就是 TTL,而到了切换那一刻,这个杠杆早就已经扳过了。
趁着还在区域文件里,把每一条指向服务器 IP 地址、而不是指向某个名字的记录都记下来。通常都会比你记得的多一条:mail、webmail、裸的 @、一个被遗忘的旧 staging、带着 ip4: 字面量的 SPF 记录,还有你当初刚拿到 IPv6 网段时加上、后来就忘了的 AAAA 记录。每一条都需要一个应对方案,而 AAAA 正是那个典型的沉默故障 — 你翻转了 A 记录,从自己笔记本上看一切正常,可每一个 IPv6 能用的访客,却还在不断落到那台你已经清空的服务器上。
给新机器选型,以及下单前要检查的事项
抵住那个“照现在的配置原样再订一台”的冲动。你是在按发票上的数字买东西,而不是按工作负载实际用了多少买东西。先花两分钟测量一下:
# peak RAM actually in use, not allocated
free -m
# what has been paging, if anything
vmstat -s | grep -i swap
# real disk consumption, biggest first
du -xh --max-depth=2 / 2>/dev/null | sort -rh | head -20
# load relative to core count
uptime; nproc有两条经验法则很管用。如果你的负载均值低于核心数,swap 也从来没被用过,说明这台机器不是卡在 CPU 或内存上,你可以平移甚至降配。如果磁盘用量超过百分之七十,新机器的容量就该按你一年后会需要多少来定,而不是按你今天用了多少 — 事后再扩容一个卷,本身就是一次小型迁移,而你现在正在做的,正是这么一件事。
放到我们这边,对应得很清楚:Pup(1 vCPU、1 GB、25 GB)能跑一个静态站点、一个 VPN 或一个小机器人;Cub(1 vCPU、2 GB、40 GB)是能在不触发换页的情况下,同时托管应用和它自己数据库的最小规格;Scout(2 vCPU、4 GB、70 GB)是有真实流量的真实站点的舒适默认选项;Hunter(4 vCPU、8 GB、140 GB)往上,才轮到容器、CI 或者一台机器上跑好几个服务这种场景。这些套餐全部是全 NVMe、流量不限,所以带宽超额 — 当初搬家的常见理由之一 — 不再是一个变量。完整梯度在这里。
然后,在挪动第一个字节之前,先检查一下你刚拿到手的这台机器的三件事。这三件事,现在查便宜,切换之后再查就贵了:
- 这个 IP 的口碑。一个带着别人历史记录的回收地址,会让你的邮件被拒收,也会让你的访客被盘查。我们的地址在签发前都经过筛查、按风险分段,但现在跑在上面的服务是你的,所以自己动手核实一遍 — 五分钟版本在这里。要在 DNS 翻转之前做这件事,那时候答案还完全不花你一分钱。
- 反向 DNS。如果这台机器以后要发邮件,PTR 记录就必须解析到一个主机名,而这个主机名又要能解析回同一个 IP。定制反向 DNS 可以申请获取;现在就去申请,好让它在切换前就已经生效。
- 从你的用户所在地过来的路由。从你用户所在地区的一台机器跑一次
mtr,能告诉你的关于司法辖区选择的信息,比任何参数表都多。你亲自测量到的延迟,胜过你臆测的延迟。
分步操作
- 提前好几天,先把每条 TTL 都调低
这一步排在最前面,是因为它是唯一一个有强制等待期的步骤。其余的事情一个下午就能搞定;唯独这一步急不来,跳过它,会把一次五分钟的切换变成一次要等两天的切换。
登录你的区域文件所在的地方,把每一条会发生变化的记录都设成 300 秒 TTL:顶点
A记录、AAAA、www、mail,以及其他任何指向 IP 字面量的记录。然后从外部确认一遍,因为控制面板显示的和实际情况有时候对不上:dig +noall +answer example.com A @1.1.1.1 dig +noall +answer example.com AAAA @1.1.1.1 dig +noall +answer www.example.com A @8.8.8.8IN前面那个数字,就是剩余 TTL。一分钟后再查一次:它应该是从 300 开始倒数,而不是从一个更大的数开始。如果它还在从 3600 倒数,说明旧值还缓存着,你只能等它耗完 — 这正是为什么这件事要在搬家前第七天做,而不是搬家当天早上才做。整个迁移期间,以及之后的一周里,都把 TTL 留在 300,这样你的回滚也能又快又稳。等旧机器彻底退场之后,把它改回一个正常的值 — 3600 就挺好 — 就够了。
- 在拷贝任何东西之前,先给旧服务器做个清点
你迁移的不是一块磁盘,而是一套正在运行的系统,而人们会漏掉的那些部分,从来都不在
/var/www里。它们是每月 4 号才跑一次的定时任务,是某次应急处理时临时加的防火墙规则,是两年前从第三方仓库装上的某个软件包。把机器的状态记录成文本,和其他一切一起搬过去:mkdir -p /root/mig dpkg --get-selections > /root/mig/packages.txt systemctl list-units --type=service --state=running --no-pager > /root/mig/services.txt systemctl list-timers --all --no-pager > /root/mig/timers.txt crontab -l > /root/mig/cron-root.txt 2>/dev/null for u in $(cut -d: -f1 /etc/passwd); do crontab -lu "$u" 2>/dev/null | sed "s/^/[$u] /"; done > /root/mig/cron-users.txt ss -tlnp > /root/mig/listening.txt nft list ruleset > /root/mig/firewall.txt 2>/dev/null || iptables-save > /root/mig/firewall.txt cp -a /etc/hosts /etc/fstab /root/mig/ du -xh --max-depth=2 / 2>/dev/null | sort -rh | head -40 > /root/mig/disk.txt接下来逐行读一遍
listening.txt,把每一个端口都对上号。这份文件才是“这台服务器到底在干什么”的权威答案,而且结果常常出人意料 — 一个指标导出器、一份被遗忘的 staging 副本、一个原本不该监听公网接口的数据库。同时把排除列表也写好,这样第一次同步就不会把一个小时都花在你根本不想要的数据上:
cat > /root/mig/excludes <<'EOF' /dev /proc /sys /run /tmp /var/tmp /var/cache /var/lib/apt/lists /swapfile /var/lib/docker/overlay2 /var/lib/mysql /var/lib/postgresql **/node_modules **/.cache EOF注意,数据库目录是被特意排除在外的。它们有自己专属的一步,在这里就把它们拷过去,正是第六步存在的意义所要防止的那个错误。
- 部署新服务器,在它承载任何东西之前先加固
下单订机器,选好位置,如果可以的话,操作系统大版本选得和旧机器一样。一边迁移主机、一边把 Debian 12 换成 Debian 13,意味着一旦出问题,你根本分不清是哪个改动导致的。先迁移,再升级。
在任何敏感数据落到这台机器上之前,先给它做一遍十分钟加固:只用密钥登录 SSH、禁用 root 登录、默认拒绝的防火墙、无人值守的安全更新。现在花十分钟就够了,等服务已经上线运行之后再补做,会真的很麻烦。
然后在旧服务器上,专门为这次迁移创建一把密钥,这样以后撤销迁移用的访问权限时,就永远不用碰到你自己的登录方式:
# on the OLD server ssh-keygen -t ed25519 -f /root/.ssh/id_migrate -C 'migration' -N '' cat /root/.ssh/id_migrate.pub把这把公钥放进新服务器的
/root/.ssh/authorized_keys里,然后确认好传输方向 — 旧机器推送到新机器,这样凭据就留在你即将离开的那台机器上,随它一起报废:ssh -i /root/.ssh/id_migrate root@NEW_IP 'hostname; df -h /; free -m'最后,把新 IP 写进旧服务器的
/etc/hosts,起个像newbox这样的名字。接下来的每一条命令都会因此变短,更有用的是,凌晨一点也不容易再对错机器。 - 在把任何东西托付给新 IP 之前,先检查它
你现在有了一个从来没人替你的服务用过的地址,而这是最后一个能免费发现它有问题的时刻。三项检查,五分钟:
黑名单。拿这个地址跑一遍多个 RBL 检查 — 完整流程和结果解读在这里。命中像 Spamhaus PBL 这样的策略列表,对一个数据中心 IP 来说很正常,对网页流量也基本没什么影响。命中 SBL 或 XBL 才是真的问题,而提出这个问题的时机是现在,不是等你的用户已经用上它之后。
反向 DNS。查一下这个地址现在的应答是什么:
dig +short -x NEW_IP如果这台机器要发邮件,就去申请你想要的那个 PTR 记录,并确认它对应的主机名,其 A 记录又指回同一个 IP。正向和反向必须一致;对不上,比用一个通用名字还糟糕。
可达性与路由。从外部确认你需要的端口确实端到端开放,而不是想当然地以为防火墙就是路径上唯一的关卡:
# from a third machine, or your laptop nc -vz NEW_IP 22 mtr -rwc 20 NEW_IPmtr的输出才是值得留存的东西。最后一跳的丢包才要紧;中间某一跳的丢包,通常只是路由器把 ICMP 降低了优先级,没什么意义。如果从你用户所在地区过来的延迟,明显比旧主机差,最好现在就发现,这时候改主意,代价也就是一份订单,不涉及任何数据。 - 用 rsync 拷贝文件系统——先跑一次演练
现在是批量传输。分两轮做:提前好几天先做一次完整拷贝,需要多久就让它跑多久,之后再做几轮短促的增量传输,只搬动发生变化的部分。永远先跑一次演练 — 输出的内容,就是接下来即将发生的事情的确切清单,把它读一遍,比这里任何其他习惯救过的迁移都多。
# on the OLD server, dry run rsync -aHAXx --numeric-ids --info=progress2 --dry-run \ --exclude-from=/root/mig/excludes \ -e 'ssh -i /root/.ssh/id_migrate' \ /var/www/ root@newbox:/var/www/然后去掉
--dry-run,跑同一条命令。对每一个重要目录都重复这个过程:/etc要有选择地拷贝而不是整个搬过去、/home、/srv、/opt,以及你的应用实际存放上传文件的地方。这些参数都各有各的用处。
-a保留权限、属主、时间戳和符号链接;-H保留硬链接;-A和-X携带 ACL 和扩展属性,如果你的上传目录曾经设置过这些,就需要它们;-x防止 rsync 跑到其他挂载的文件系统里去。--numeric-ids是最容易被漏掉、事后又最让人后悔的一个:不加它,rsync 会按名字来映射属主,如果www-data在两台机器上的 UID 不一样,每个文件到了新机器上都会被归到错误的用户名下,事后理清楚会很麻烦。两个警告。把
/etc整个拷贝到一台正在运行的系统上,会覆盖掉新机器自己的网络配置、fstab 和 SSH 主机密钥 — 只拷贝你需要的那部分具体配置,而不是整个目录。还有,把--delete留到最后一轮再用:用它让新机器和旧机器完全对齐是正确的,但如果你已经在目标机器上创建过什么东西,它就是破坏性的。第一轮完整传输结束后,比较两边的情况,确认它做的和你以为的一致:
du -sh /var/www ssh -i /root/.ssh/id_migrate root@newbox 'du -sh /var/www' - 用转储来搬数据库,绝不要直接拷贝文件
这一步决定了你的迁移是平淡无奇,还是让人记一辈子。数据库的数据目录,只有在服务器停止运行时才是一致的。带着它在运行中原样拷贝,你会得到一堆看起来没问题、传输起来也没问题的文件,可它们恢复出来的数据库,却带着一种细微而永久的损坏 — 往往要过好几个星期才会报错。
要老老实实地转储它。对于 MySQL 或 MariaDB,
--single-transaction能在不锁住整个库的情况下,给你一份一致的快照:mysqldump --single-transaction --quick --routines --triggers --events \ --default-character-set=utf8mb4 --databases appdb \ | zstd -T0 > /root/mig/appdb.sql.zst对于 PostgreSQL,自定义格式很值得一用 — 它会压缩,需要的话还能选择性地恢复:
pg_dump -Fc -Z6 appdb > /root/mig/appdb.dump把它传过去,再恢复:
scp -i /root/.ssh/id_migrate /root/mig/appdb.sql.zst root@newbox:/root/ # on the NEW server zstd -dc /root/appdb.sql.zst | mysql # postgres equivalent pg_restore -d appdb -j4 /root/appdb.dump然后要验证一下,因为“恢复完成了”和“数据真的都在”,是两个不同的说法。在两边都对比一下要紧的那些表的行数:
mysql -N -e "SELECT COUNT(*) FROM appdb.orders; SELECT COUNT(*) FROM appdb.users;"转储里藏着两个容易被忽略的东西:一个是字符集和排序规则,乱码的重音字符往往就是从这儿来的 — 如果旧数据库用的是
utf8而不是utf8mb4,就该有意识地决定,这次搬家是不是也顺便把它修一修;另一个是数据库用户和权限,这些mysqldump --databases是不会带上的。要在新机器上显式地重新创建应用要用的用户和密码,然后记得配置里的连接字符串也得跟着对上。如果你的应用用的是 SQLite,那个文件就是数据库,同样的规则照样适用 — 不要在它运行时原样拷贝。用
sqlite3 app.db ".backup /root/mig/app.db",它能安全地生成一份一致的副本。 - 把技术栈跑起来,借助 hosts 文件覆盖来预演
新服务器现在已经有了文件和数据。把一切启动起来,在它真实的主机名下测试它 — 这时候全世界其他人还在开开心心用着旧机器。这是整个流程里最有价值的一招,而且只需要一行配置。
在你自己的笔记本上,把新服务器的 IP 加进
/etc/hosts(或者C:\Windows\System32\drivers\etc\hosts):203.0.113.10 example.com www.example.com你的浏览器现在会把真实域名解析到新服务器,而其他任何人都不会。每一个 URL 都是对的,每一个 cookie 域名都能对上,每一条重定向和回调路径的表现,都会和切换之后一模一样 — 这恰恰是拿一个临时的
new.example.com子域名去测试所抓不到的问题,因为迁移里出问题的地方,有一半都跟主机名绑在一起。如果只是想不改任何东西、快速检查一次,
curl也能就地做到同样的事:curl -sSI --resolve example.com:443:203.0.113.10 https://example.com/ curl -sS --resolve example.com:80:203.0.113.10 http://example.com/ -o /dev/null -w '%{http_code}\n'把整个应用都走一遍,不要只看首页:登录、提交一个表单、上传一个文件、触发一封密码重置邮件、打开一个后台页面、访问一下支付处理商会调用的那个接口。然后哪怕一切看起来都没问题,也去读一遍错误日志 — 缺失的 PHP 扩展、缓存目录上错误的文件权限,还有一个尚不存在的数据库用户,全都会先出现在日志里,然后才会出现在屏幕上。
事后记得把 hosts 里那一行删掉。所有人都会忘记一次,然后花上二十分钟纳闷回滚怎么看起来没生效。
- 在翻转任何东西之前,先让新机器上的 TLS 工作起来
证书绑定的是名字,不是 IP 地址,所以迁移这件事本身,不会让你已经持有的证书失效。会出问题的是签发这个环节:常见的 HTTP-01 挑战,要求 Let's Encrypt 通过 80 端口,去被认证的那个名字上抓取一个文件,而这个名字这时候还指向旧服务器。鸡生蛋、蛋生鸡,难点就在这里,而干净利落的解法有三种。
直接拷贝现有证书。最简单,通常也是对的做法。私钥和证书链,就像其他文件一样搬过去:
rsync -aHAX -e 'ssh -i /root/.ssh/id_migrate' \ /etc/letsencrypt/ root@newbox:/etc/letsencrypt/新服务器可以立刻提供有效的 TLS,一旦 DNS 指向它,续期也会自己开始正常工作。确认一下那边的定时器已经启用:
systemctl list-timers | grep certbot。用 DNS-01 挑战。通过一条 TXT 记录、而不是 HTTP 请求来证明你对域名的控制权,所以哪怕还没有任何东西指向这台服务器,它也能用。如果你想在切换前,就在新机器上拿到一张真正独立的证书,这是理想的选择,也是通配符证书唯一的选择。
切换之后再签发。可行,但这会留下一段空当:网站已经在新 IP 上运行,却没有证书,对任何用了 HSTS 的站点来说,这不是一个访客能点击穿过去的警告。只有面向一个还没人访问过的全新域名时,这么做才说得通。
不管你选哪一种,切换之前都要直接对着新 IP 验证一遍,用同样的
--resolve技巧:curl -sSI --resolve example.com:443:203.0.113.10 https://example.com/ | head -1 echo | openssl s_client -connect 203.0.113.10:443 -servername example.com 2>/dev/null \ | openssl x509 -noout -subject -dates检查一下日期,以及证书主体是不是覆盖了你提供服务的每一个名字,包括
www。而且如果你用了 max-age 很长的 HSTS,就该把证书当成翻转之前而不是之后唯一必须万无一失的东西 — 那个响应头,是你已经对每一个回访的访客许下的承诺。 - 切换:冻结写入、最后一次增量、翻转记录
真正动手也就十分钟,而且是唯一一段有时钟压着的时间。选在早上做,用你自己的时区,挑一个手头没别的事的日子。绝不要选星期五。
冻结。把旧应用切换到维护或只读模式。停掉后台工作进程、队列消费者和定时任务 — 所有不经过浏览器就会写入数据的东西。从这一刻起,旧机器上不再写入任何新数据,这也是之后一切操作都安全的前提:
# on the OLD server systemctl stop app-worker.service systemctl stop cron touch /var/www/maintenance.flag最后一次增量。再跑一次 rsync,这次带上
--delete,让目标端完全对齐,再做一次转储。因为第一轮传输早在几天前就完成了,这次要搬的东西很少,几秒钟就能跑完:rsync -aHAXx --numeric-ids --delete --exclude-from=/root/mig/excludes \ -e 'ssh -i /root/.ssh/id_migrate' /var/www/ root@newbox:/var/www/ mysqldump --single-transaction --quick --routines --triggers --events \ --default-character-set=utf8mb4 --databases appdb | zstd -T0 \ | ssh -i /root/.ssh/id_migrate root@newbox 'zstd -dc | mysql'检查。两边核对一下行数,再通过 hosts 文件覆盖,把应用完整走一遍,然后把新机器上的维护标记去掉。
翻转。把
A记录改成新 IP。AAAA记录也要一并改 — 这是切换只做了一半这种情况里最常见的一种。用一个你控制不了的解析器确认一下:dig +short example.com A @1.1.1.1 dig +short example.com AAAA @1.1.1.1然后同时盯着两台机器。流量应该会在一两分钟内出现在新机器上,并在接下来的五分钟里从旧机器上逐渐消退:
tail -f /var/log/nginx/access.log # on both, side by side让旧应用继续留在维护模式,而不是直接关掉它。落单请求碰到它时,看到的会是一个客气的提示页面,而不是连接错误,同时你也留着这台机器,以备你大概率用不上的回滚之需。
- 观察一周,再妥善下线
DNS 能解析了,不代表迁移就完成了。真正完成的标志,是一整个计费周期和定时任务周期都平安无事地过去了。
头几个小时,盯着错误率,而不是在线率 — 一台服务器完全可以处于正常在线状态,同时却把三分之一的请求都返回 500:
journalctl -u nginx -u php8.4-fpm -f awk '{print $9}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head然后按一份简短的清单逐项排查,因为这些才是搬家之后真正会出问题、而且都不会主动吭声的地方:跑在新机器上的定时任务和 systemd 计时器(拿
systemctl list-timers对照你在第二步做的清点);外发邮件有没有正常送达而不是进了垃圾箱;定时备份指向的是不是新服务器,而不是还在归档旧的那台;有没有第三方把你的 IP 地址放进了白名单 — 支付处理商、某个 API、合作伙伴的防火墙;还有那个只有到月初才能证明自己是否正常的月度任务。让旧服务器继续运行,保持维护模式,不去动它,至少留一周。它是你的回滚方案,也是你的参照副本,比它花的那几美元值钱得多。然后按顺序把它彻底收尾:轮换它可能读到过的每一个凭据,把最后一份加密归档存到第三个地方,覆写应用和数据库目录,跑一遍服务商的销毁或重装流程,最后才取消 — 取消前先看一眼续费日期,因为多花一个月买回滚保险,通常是更划算的选择。
最后,既然你已经不再需要五分钟级别的可回滚性,就把 TTL 调回 3600,再把你保留的文档更新成新地址。将来某个凌晨两点的你,会因为运维手册和服务器对得上号而由衷感激。
邮件不会跟着其他东西一起搬家
如果旧服务器发过邮件 — 哪怕只是发发密码重置邮件 — 也要把这当成一次和主迁移并行、但节奏更慢的第二次迁移。送达率本质上是一套信誉体系,而信誉是挂在 IP 地址和域名上的,不是挂在你搬过去的那套软件上。
有四条记录决定着你的邮件是被正常阅读,还是被直接丢弃,其中三条的内容会随着服务器变化而需要跟着变:
- SPF 列出了谁可以代表你的域名发信。如果你的记录里写着一个
ip4:字面量,它现在就是错的。在切换之前加上新 IP,切换一周后再删掉旧的 — 让两个地址同时被授权几天,不花你一分钱,还能覆盖过渡期。 - DKIM 用私钥给邮件签名。把这个密钥连同其余配置一起搬过去,选择器就能继续正常工作。要是改成生成一把新密钥,你就得发布新的选择器,还得等它生效,所以除非有理由不这么做,否则就直接把密钥搬过去。
- DMARC 告诉收信方,当前面两项都失败时该怎么办。如果你用的是
p=reject,切换窗口期里 SPF 一旦出问题,那就不是一个警告,而是邮件被悄无声息地删除。可以考虑在迁移那一周把它降到p=none,事后再改回去。 - PTR 就是上面说的反向记录。新 IP 在你申请之前,只有一个通用的反向记录,而好几家大服务商会直接拒收来自通用反向名称的邮件。
老实说该有的预期是:一个全新的 IP 一开始完全没有信誉,而这和“信誉良好”不是一回事。发信量要按天、而不是按小时慢慢爬升。如果邮件对你来说是关键业务,就让旧服务器在搬家之后再多活、多发一周信,而不是一步切换到位 — 如果这是生意意义上的关键业务,一台专用的中继服务器,会是比两台机器都更好的答案。
回滚,但不要让情况变得更糟
准备回滚方案的意义,不在于你预期会用上它。而在于有了它,你才能在早上十点从容地切换,而不是在凌晨两点提心吊胆地切换,而从容才是真正能防止犯错的东西。
你的回滚方案很简单,而且只要旧数据库仍然是权威版本,它就会一直保持简单:把 A 记录改回去。配合 300 秒的 TTL,五分钟就能恢复。那段窗口 — 从翻转那一刻,到新机器上第一次出现只存在于那里的写入之间 — 就是你免费的撤销机会,这也是为什么旧服务器要保持运行、原封不动、不被清空,至少留一周。
真正会毁掉这一切的是脑裂:写入同时落到两台机器上。这样一来,两边的数据库都不再正确,手工调和它们,比你当初想避免的任何停机都更糟。三个习惯能彻底防住这种情况:
- 在窗口一开始,就把旧应用切换到只读或维护模式,而不是指望 DNS 已经停止把流量导过去。DNS 只是一个提示;停掉的服务才是事实。
- 翻转之后,盯着旧服务器的访问日志看,而不是新的那份。还在往那边打的请求,就是你的落单流量,等这股细流停下来,迁移才算真正完成。
tail -f /var/log/nginx/access.log就是全部要用到的工具。 - 一旦你在新机器上接受了第一次真实写入,回滚就不再是改一下 DNS 那么简单 — 而是要做一次恢复。什么时候跨过这条线,要清醒地自己决定,如果是两个人一起做,就把这话说出声。
动手之前,给自己定一条止损规则:如果新的技术栈在,比如说,三十分钟之内没能正常提供服务,那就把记录改回去,把这个晚上收回来,在没有时钟压着的情况下慢慢修。迁移之所以会搞砸,往往是因为有人觉得回头等于失败,所以一味硬着头皮往前推。其实不是;回头是那个便宜的选项,而且它只在严格限定的时间内可用。
下线收尾:轮换、擦除、核实,再取消
切换一周之后,旧服务器就成了一份完整的、仍在运行、却无人看管的数据副本,架在一套你已经不再留意的基础设施上。这时候它同时也是你手上补丁打得最不齐全的一台机器。按这个顺序把收尾工作做完:
- 轮换旧机器能读到的一切。应用密钥、API 令牌、数据库密码、SMTP 凭据、webhook 签名密钥,还有任何钱包 RPC 密码。默认当它们已经泄露,因为你没法证明没有。如果你为这次迁移专门生成过一把新的 SSH 密钥,这时候就该把旧的授权密钥清出去。
- 做最后一次归档 — 把一份加密的 restic 或 Borg 快照存到一个两台机器都不是的地方。你大概只会需要用到它一次,六周之后,为了一个没人还记得的文件。
- 覆写数据。在 VPS 上你没法核实物理介质本身,所以能做多少做多少:用
shred或者直接覆写应用和数据库目录,然后让服务商的重装或销毁流程去跑。从一开始就做静态加密,才能让这一步变得便宜;没有它,你就只能依赖别人家的删除政策。 - 核实,再取消。确认新机器已经完整地提供了一整周服务,包括那个没人会想起来的每月定时任务,再确认旧机器上已经没有任何东西还能被解析到。然后再取消 — 先看一眼续费日期,因为多花一个月钱买回滚保险,往往比省下那八美元更明智。
把旧账户本身留到最后再删,而且要在你确认无误之后才删。工单记录、发票,还有那个偶尔被遗忘的子服务,往往就活在账户里,而一个你已经登不进去的账户,会是发现一条自己忘了搬走的 DNS 记录的一个尴尬地方。
一份行得通的时间表
拆开摊到一整周里做,这些事一件都不紧张。压缩到一个晚上做,每一件都紧张。
- 第 −7 天。把每条 TTL 都调到 300。给旧服务器做一次清点。下单订新机器,并检查它的 IP、rDNS 和路由。
- 第 −5 天。加固新机器,装好整套技术栈。第一次完整
rsync,这是最慢的一次 — 之后每一轮都只搬增量。 - 第 −3 天。把数据库转储恢复到新机器上,把应用跑起来。通过 hosts 文件覆盖,把整个流程完整测一遍。趁现在还没什么风险,把坏掉的地方修好,因为到时候一定会有什么地方是坏的。
- 第 −1 天。在新服务器上签发 TLS。确认证书链没问题,如果你用了 HSTS,也确认它不会把一个小失误变成一个连点都点不进去的死局。把新 IP 加进 SPF。再检查一遍,确认每条 TTL 是不是真的已经降下来了。
- 第 0 天,早上。旧机器进入维护模式。最后一次增量同步。最后一次转储加恢复。核对行数。翻转 A 记录 — 还有 AAAA。同时盯着两边的访问日志。
- 第 +1 到 +7 天。旧服务器保持开着,不去动它。盯着日志、邮件,以及新 IP 的口碑。如果条件允许,至少让每月任务成功跑一次。
- 第 +7 天。轮换密钥,做最后一次归档,擦除,核实,取消。
能不能预判一次迁移会不会平淡无奇,最好的单一指标就是:第一步是不是在第五步的一周之前就做完了。服务器搬家里几乎所有出岔子的地方,追根究底都是晚上十一点了,TTL 却还停在 86400。
常见问题
VPS 迁移真的能做到零停机吗?
字面意义上的零停机,需要两台机器同时持续接受写入,这意味着要用到复制和一套共享或集群化的数据库 — 这是一个真正棘手的问题,也不该拿来为单台服务器求解。真正靠谱、能够实现的是没有访客看到错误,也没有一次写入丢失:在最后同步期间留一小段只读窗口,配合早就设成 300 秒的 TTL,切换本身只需要几分钟。实际操作中,这大约是一到五分钟的维护页面,对几乎所有网站来说,这和零停机没什么两样,而且比另一种做法安全得多。
DNS 传播到底需要多长时间?
根本不存在“传播”这回事。什么都没有被推送到任何地方 — 解析器只是按照你 TTL 指定的秒数缓存你的记录,过期了才会再问一次。所以老实的答案是“你做修改那一刻,生效的那个 TTL 是多少,就要等多久”。一周前就设成 300 的 TTL,基本上五分钟之内所有人都会切到新地址。而很多注册商仍然默认使用的 86400,会让一些解析器把流量继续送去旧服务器整整一天。这就是为什么调低 TTL 是第一步,而不是第九步。
换主机时,我能保留自己的 IP 地址吗?
除非你自己拥有这段地址空间,并且能让新服务商替你宣告它的路由,这意味着你得是 RIPE 或 ARIN 的成员,拥有自己的地址分配 — 对一家公司来说是现实的,对一台单独的服务器来说就不是了。对其他所有人来说,换主机就意味着换 IP,这正是为什么你要在切换之前而不是之后,去检查它的口碑和反向 DNS。在我们这边,地址在签发前都会对照 Spamhaus 和上百个其他名单做筛查,而且是从按风险分段的地址池里发放,而不是从别人用剩下的里面回收利用,但自己动手核实一遍只要五分钟,而且永远值得做。
我应该改用磁盘镜像或服务商自带的迁移工具吗?
如果两端是同一家服务商、同一种 hypervisor,镜像恢复没问题,而且快得多。跨服务商的话,这通常是个陷阱:镜像里带着旧机器的网络配置、内核模块、驱动和硬件假设,你会花上一整晚,去调试一个引导进一台已经不存在的机器里的系统。一次干净安装,配合一份经过取舍的数据和配置拷贝,能给你一台你自己理解的服务器,还能甩掉旧机器上积累的杂物。迁移是你能拿到的最便宜的一次机会,用来把不需要的东西留在身后。
如果数据库一直在被持续写入,该怎么办?
两个选项,按投入的精力从少到多排列。简单的那个,就是第九步里的只读窗口:对一个繁忙但规模正常的数据库做一次 --single-transaction 转储,只需要几秒钟到几分钟,为此挂这么久的维护页面是划算的。彻底的那个是复制 — 提前好几天,把新服务器配置成一个副本,让它保持同步,再在窗口期内把它提升为主库。这能把冻结时间压缩到几秒钟,代价是配置本身要复杂得多。只有在第一种方案行不通时,才选第二种,而且绝不要在动手当晚临时现学现做。
换主机会伤害我的搜索排名吗?
换主机本身不会。Google 索引的是主机名,不是 IP 地址,只要 URL 和内容保持不变,这次搬家实际上是不可见的。真正会让你付出代价的,是那些往往伴随一次糟糕迁移一起出现的东西:爬虫来访时页面返回 5xx、证书报错、一份从 staging 副本里带过来、里面写着 Disallow: / 的 robots.txt,或者悄悄变了样的重定向。翻转之后立刻检查一下 robots.txt、你的 canonical 标签,以及几个真实的 URL。只要网站在新 IP 上给出的响应和在旧 IP 上一样,那就没什么需要挽回的。
我的 TLS 证书在新服务器上还能用吗?
能 — 证书是签发给域名的,不是签发给 IP 地址的,所以把 /etc/letsencrypt 整个拷贝过去,新机器上立刻就有一张有效证书。真正麻烦的是续期,而不是有效性:HTTP-01 挑战需要域名指向正在执行续期的那台机器,所以在你翻转 DNS 之前,续期在新机器上都会失败。在切换前拷贝好证书,翻转 DNS,然后确认续期定时器在新服务器上能成功跑起来。如果你需要在翻转之前就拿到一张独立的证书 — 或者需要通配符证书 — 那就改用 DNS-01 挑战。

