大多数迁移,不是在复制阶段失败的。它们失败在 DNS 变更之后的那二十分钟里——半个互联网还在跟旧服务器打交道,另一半已经转向了新服务器,而这两台机器同时都在接受写入。文件原封不动地到齐了,网站也能打开,可订单却分别落进了两个再也无法核对一致的数据库里。要避开这一点,几乎和传输速度没什么关系,却几乎完全取决于顺序:你先改什么,你冻结什么,以及你要让什么一直跑着,直到你真正确定为止。
“不停机”到底意味着什么
值得把话说精确,因为这个说法底下,其实盖住了两个截然不同的承诺,而它们所需要付出的努力也天差地别。搞清楚自己到底想要哪一个,是这场迁移里第一个真正要紧的决定。
如果两者必须牺牲一个,牺牲可用性。四分钟的维护页面,是你能解释清楚的事;分裂开的数据,却是你没法修补的事——因为已经没有谁能说清楚,哪一份才是对的。
动手之前,先写下你能忍受的最长写入冻结时间:四分钟、三十秒,还是零。这一个数字,决定了后面的一切——一次转储加恢复是否够用,要不要上复制,要不要在最前面架一层代理。还没定下这个数字就先选好工具,正是迁移里那些意外的来源。
复制数据之前,先做盘点
一台服务器,会悄悄积累下一堆没人记录过的东西:排查故障时加的一条 cron 任务、为合作方地址开的一条防火墙规则、粘贴进某个服务文件里的一个 API 密钥。复制并不会把这些也带过去,你只能在接下来的两周里,一个一个地把它们找回来。现在花半个小时做一次盘点,几乎能把这些麻烦都提前排除掉。
什么在监听
运行 ss -tulpn,把每一个正在监听的套接字都过一遍。每一个,都对应着一个必须在新机器上也存在的服务;每一个解释不清楚的端口,都值得先搞明白再去复制它——迁移正好是个好时机,能让你注意到哪些东西从 2023 年就一直在跑,却没人管过。
什么在定时运行
对包括 root 在内的每一个用户都跑一遍 crontab -l,再看看 systemctl list-timers,以及应用自己内置的任何调度器。定时任务是迁移过程中最常见的、会被重复保留下来的东西——而重复,比缺失还要糟糕。
什么不在磁盘上
DNS 记录、你这个地址的反向 DNS、防火墙规则、第三方手里握着的 API 密钥、webhook 的目标地址,以及别处任何写着你当前 IP 的白名单。这些东西,没有一个存在于你即将复制的那个文件系统里。
应用做了哪些假设
写死的绝对路径、配置文件里的主机名、数据库套接字的位置、bind 指令里的某个地址。这些东西坏起来都是悄无声息的——服务照常启动,却根本不能用。
把它写进一个纳入版本控制的文件里,而不是留在终端的滚屏记录里。切换过程中你会翻看它三次,其中一次,一定是在你脑子转不过来的那一刻。
选目的地,看准那些以后没法再改的东西
配置是可以随时调整的;但有那么几项属性不是,值得在你还能自由选择的时候,认真拿定主意。反正都要搬家了,趁现在修正那个你一直将就着的限制条件,是成本最低的一次机会。
- 地区,因为它同时决定了你和用户之间的延迟,还有适用的法律。/locations 列出了各个地区,以及它们的往返延迟特征;挑一个和你现在要离开的那个地区,故障互不牵连的。
- 法律立足点——如果你搬家的原因,是你现在的主机商转发投诉的速度比转发数据包还快。/offshore-hosting 讲清楚了司法管辖区真正能管到什么,又管不到什么。
- 账单身份——因为一家在收款前就要求你提交证件的主机商,从一开始就等于替你建了一份档案。/no-kyc-vps 和 /pay-with 讲的是另一种做法:一个用加密货币充值的余额,没有银行卡,也没有任何名字和这台机器绑在一起。
- 余量——因为搬两次家,是没有人会主动计划的结局。/vps 列出了各档配置,如果你从没真正测算过当前这台机器的负载,/guides 里也有对应的估算方法。
- IPv6,再加一个干净的 IPv4——因为一个回收再利用的地址,到手时可能已经背着别人留下的坏名声。趁它还没变成邮件问题之前,先查一查。
先把 DNS TTL 调低,提前好几天动手
这是唯一一步没法留到最后再匆忙补上的准备工作,因为它起效的快慢,取决于一个你控制不了的时钟。记录上的 TTL,告诉解析器该把一个答案缓存多久。如果它被设成一天,那么一个小时前刚查询过的解析器,接下来二十三个小时里,不管你后来发布了什么,都会继续把用户送到旧服务器那里。
- 1
先查清楚当前的 TTL
dig +noall +answer yourdomain.com 能看到剩余的缓存时间;用 dig @ns1.example.net yourdomain.com 直接查询你的权威域名服务器,看到的则是配置的原始值。把它记下来——它决定了你要等待多长时间。
- 2
把它调到 300 秒
把每一条会变动的记录的 TTL 都调低:A、AAAA,以及任何指向这台主机的 MX 或 CNAME 记录。五分钟,短到足以让切换过程从容不迫,又长到不会把你的域名服务器压垮。
- 3
等旧的 TTL 过期,然后再多等一会儿
新的短 TTL,只有在旧的长 TTL 过期之后,才会真正传到解析器那里。在把这个更低的 TTL 当真之前,至少要等满一整个旧 TTL 的周期——如果原来是一天,就等一天。提前动手不花什么成本,却能给整场切换换来余地。
有些解析器根本不理会你设的 TTL,始终按自己的最短周期缓存;还有一部分客户端,会把结果缓存一整个进程的生命周期。要提前料到,哪怕切换做得再教科书,也会有长尾流量在接下来好几个小时里持续打到旧地址上。这不是跳过 TTL 这一步的理由——恰恰相反,这正是旧服务器之后必须继续留着的理由。
构建新服务器,而不是克隆旧的那台
本能的做法,是对当前磁盘做一次镜像,再到别处还原。这是错误的一步,原因和它在恢复场景里错误的原因一模一样:块级克隆,会原原本本地把配置漂移、孤立的软件包、被人半途放弃的半成品服务,以及之前入侵者留下的任何后门,都忠实地复制过去。它还会把你死死锁在旧的发行版上。
用一个最新的基础镜像构建新机器,靠脚本安装各项服务,只复制数据本身。脚本才是真正的产出——正是它,能把下一次迁移变成一个下午的事,而不是两周;也正是它,能让你在出事之后重建机器,而不必像考古一样去挖当年做了什么。
- 在任何东西碰它之前,先完成开通、更新和加固:只认密钥登录的 SSH、默认拒绝一切的防火墙、无人值守的安全更新。/guides 里有这份清单完整的一小时版本。
- 安装和生产环境相同大版本的运行时与数据库。迁移的同时还想着把 PostgreSQL 从 14 升级到 17,可不是什么好时机——一次只改一件事,这样万一出问题,原因也只可能有一个。
- 复制文件之前,先用相同的数字 ID 重新建好用户和用户组,否则就得在事后手动修正属主。用 --numeric-ids 传输能保住这些数字不变;把它们重新对应回正确的名字,是你自己的活儿。
- 趁两个地址都还有效的时候,把新地址加进每一个目前写着旧地址的白名单——合作方的 API、托管数据库的防火墙、支付网关,还有你自己的监控系统。
把服务都装好,然后停掉并禁用它们。一个开机自启、并且已经在新地址上响应的 Web 服务器,会被扫描器发现,会被收录到错误的主机名下面——更糟的是,谁只要提前解析到了这个地址,它就会乐呵呵地把你网站那份还没填满的半成品送给对方。在你自己决定好之前,新机器上不该有任何东西公开响应。
分两轮复制文件
对一个活跃文件系统只做一次传输,得到的不过是一个移动靶子的某个快照。分两轮传输能干净地解决这个问题:第一轮耗时长,针对的是一套还在运行的系统;第二轮很短,在冻结期间进行——也只有第二轮,才真的需要快。
第一轮什么时候跑都行——提前好几天也没问题。它负责搬运大头:上传目录、邮件队列、容器卷,还有堆了好几年的媒体文件。等到真正切换的时候,它已经是过时的数据了,而这正是第二轮存在的意义。
- 通过 SSH 运行 rsync -aAXH --numeric-ids --info=progress2:-a 对应常规属性,-A 对应 ACL,-X 对应扩展属性,-H 对应硬链接——如果这台机器上有什么东西依赖去重,硬链接就很重要。
- 排除掉那些不该被搬过去的东西:/proc、/sys、/dev、/run、各种临时目录、软件包缓存,以及你不需要的日志。复制内核接口,往好里说是浪费时间,往坏里说会直接把传输卡死。
- 只在第二轮加上 --delete。放在第一轮无伤大雅;放在第二轮,它会删掉那些源头上后来也被删掉的文件——而这正是你想要消除的那种偏差。
- 系统配置要有选择地复制,而不是整体照搬。你想要的是 Web 服务器的 vhost 配置、服务单元和应用配置——而不是旧机器的文件系统表、网络配置,或者机器 ID。
- 为这次传输专门生成一个用完即弃的 SSH 密钥,在目标端授权它,迁移完成后再删掉它。一个在系统里赖着不走、一放就是两年的迁移密钥,是一份没人还记得是为什么签发的凭证。
两轮之间,要去验证,而不是想当然。带上 --dry-run 跑一遍第二轮,会把它将要改动的内容原样打印出来:列出几百个最近改动过的文件是健康的;列出四万个,就说明某条排除规则写错了,或者有什么东西在无缘无故地重写时间戳。
迁移数据库,不丢一笔写入
这里,就是你那份冻结时间预算真正被花掉的地方,而该用哪种技术,完全取决于你一开始写下的那个数字。下面这三种方案都是对的——只是分别对应不同的数字。
不管选哪一种,旧数据库都必须先停止接受写入,新数据库才能开始接受写入。不是紧随其后——而是在此之前。两边同时可写的这段重叠期,就是脑裂窗口期,也是这整篇指南里唯一一种没有干净修复方式的故障:两段已经分叉的历史,没有任何合并方式能绕开一行一行手动核对数据。
数据集比较大的时候,把表结构和数据分开转储。先恢复表结构,能让你提前在新主机上验证好结构、索引和权限,这样一来,加载数据就变成了单独一个耗时较长的操作——你在冻结期间启动它的时候,已经能确信它一定能顺利落地。
TLS 证书要在切换之前签发,而不是之后
新服务器上的证书,必须在第一个用户抵达的那一刻就已经有效。事后才发现不是这样,意味着每一个访客都会撞上浏览器的安全警告页——而如果你发送了 HSTS 响应头(你也应该这么做),他们连点击跳过的办法都没有。在 DNS 指向任何新地方之前,有两种办法能先备好一张能用的证书。
- 复制现有的那张证书。整个 ACME 状态目录都复制过去,或者只从你客户端存放的位置取出证书和私钥。它立刻就是有效的,因为有效性和文件放在哪台服务器上毫无关系;等 DNS 切换过去之后,续期也会照常恢复。
- 用 DNS-01 方式签发一张全新的证书:靠一条 TXT 记录证明你对域名的控制权,而不是对当前地址发起 HTTP 请求。这种方式在迁移之前、切换之前就能用,对通配符证书也同样适用——而这一点,HTTP-01 完全做不到。
- 切换之前,不要在新服务器上尝试做 HTTP-01 验证。验证请求是通过 80 端口、冲着这个域名发起的,而这个域名此时仍然解析到旧机器——验证只会一次又一次地失败。
不用动 DNS 也能验证:只在一条命令里把主机名解析到新地址就行:curl -sI --resolve yourdomain.com:443:198.51.100.10 https://yourdomain.com/ ——把里面的地址换成真实的。这一行命令,是整场迁移里价值最高的一次检查。它会真实地跑一遍新机器上的 vhost、证书和整套应用,让你能在发现配置错误还完全不花代价的时候,就把它揪出来。
如果这台服务器要发邮件,就要更早动手
邮件,是迁移里那种要过好几天才会出问题、出问题也悄无声息、而且还会被怪到别的原因头上的那部分。一个新地址没有发送信誉,还可能继承了别人留下的坏名声;而你发布过的每一条认证记录,又都绑在你正要离开的那个地址上。
- 在真正定下这个新地址之前,先拿它去查一遍主流的黑名单。一个带着历史污点的回收 IP,趁着更换还不花钱的时候,是值得换掉的。
- 把新地址的反向 DNS 记录,设置成你的邮件主机名。收信服务器会检查正向解析和反向解析是否一致,光是缺一条 PTR 记录,就足够被当成垃圾邮件处理了。
- 切换之前,把新地址加进 SPF;切换之后,再把旧地址移除——重叠期间两个地址都列在里面,这样不管邮件实际是从哪台服务器发出的,认证都能通过。
- 复制现有的 DKIM 私钥,而不是重新生成一套——这样 DNS 里已经发布好的 selector 才能继续通过验证。重新生成就意味着要重新发布,而重新发布又有它自己的传播延迟。
- 如果你的发信量不小,就要逐步预热这个新地址。一台从诞生起就没发过一封邮件、却突然发出一万封邮件的服务器,在收件方看来,和一台已经被攻陷的主机没有任何区别。
正式切换
前面所有的准备工作,都是为了让这一部分变得短、有序,而且可以撤回。挑你真正的流量低谷来做,而不是迷信凌晨三点——去看看你自己的访问统计。动手之前,先把回滚方案写下来,因为真正需要用到它的那一刻,正是你最不想临时现写的那一刻。
- 1
冻结旧服务器上的写入
维护模式、一个只读的数据库用户,或者在反向代理层面让写入路径直接报错,都可以。整个过程中,读取请求一直由旧机器继续提供服务——这正是数据搬家的同时,网站还能保持在线的原因。
- 2
关掉旧服务器上的每一个定时器
把 crontab 注释掉,停掉定时器,停掉队列工作进程。从这一刻起,旧机器不能再处理任何东西,否则任务就会被跑两遍:两张发票、两封邮件、两次 webhook 投递。
- 3
跑最后一次增量同步
带上 --delete 的第二轮文件传输,接着是最后一次数据库转储,或者让副本追上进度。这一步很短,最多几分钟,因为大部分数据量,第一轮早就已经搬过去了。
- 4
对着真实主机名验证新服务器
用前面那个 --resolve 的技巧,实际跑一遍一个会读数据库的页面、一次登录,还有一条写入路径。确认一下,行数和源头对得上。这一切都要在 DNS 变更之前做完——这时候回滚还完全不花任何代价。
- 5
切换 DNS,启动服务
把 A 和 AAAA 记录更新成新地址,然后在新服务器上启用并启动应用及其定时器。流量会在一个 TTL 周期之内开始抵达,并在接下来的一个小时里持续转移过来。
- 6
让旧服务器继续提供读取服务
让它以只读状态,至少再多留二十四小时。带着过期缓存的客户端,还是会落到它头上;而一台只读的旧服务器,给出的是稍微有点过时的页面,而不是一个被拒绝的连接——这正是一次不被察觉的迁移,和一次被人察觉到的迁移之间的差别。
这段重叠期里,还藏着最干净的一招。与其硬生生地切换 DNS,不如在第五步做完之后,把旧服务器重新配置成指向新服务器的反向代理。每一个仍然解析到旧地址的掉队请求,都会被透明地转发过去,整个切换也就彻底不再依赖 DNS 的传播速度;等到打到旧服务器上的流量归零,再把这层代理拆掉就行。记得带上 X-Forwarded-For,这样你的日志和限流规则,看到的依然是真实的客户端地址。
最初的 48 小时
网站能打开,不代表迁移就结束了。真正结束,是在没有任何东西还依赖着旧机器的那一刻——而找出这些依赖关系,是一件需要主动去做的事,不是坐着等就行的。
- 盯着旧服务器的访问日志。每一个还在往那儿打的请求,都是一个没迁移干净的依赖——可能是某个合作方对接里写死的地址,可能是带着过期缓存的移动端客户端,也可能是一个没人认领的监控探针。这份日志,就是你的待办清单。
- 确认定时任务真的在新主机上跑过了。不是确认它们处于启用状态,而是确认它们在该跑的时间点跑了,而且给出了预期的结果。一个悄无声息什么都不做的 cron 任务,和一个正常工作的 cron 任务,看起来一模一样。
- 用一次真正的 dry run 去测试证书续期,而不是等到六十天后才发现,你的 ACME 客户端这段时间里,一直在对着一台早就收不到验证请求的服务器续期。
- 把邮件流程端到端、两个方向都检查一遍,包括应用自动发出的那些邮件。密码重置邮件是最典型的牺牲品,因为在真有用户需要用到之前,根本没人会去测它。
- 给新服务器做一次备份,再把它恢复到某个地方。一台没有经过验证的备份的全新机器,处境比你离开的那台还要糟糕;/guides 里有这个论点的完整版本。
- 确认无误之后,把 DNS 的 TTL 恢复成正常值。要是一直把它留在 300 秒,就是在没有任何剩余好处的情况下,永久多付出一点查询次数和延迟上的代价。
旧服务器下线
最后这一步,是最容易被跳过的一步,也是唯一一个会牵连到隐私的一步。旧磁盘上存着你的密钥、你的数据库、你的客户数据,还有你的日志;取消服务并不会把这些东西擦掉——它只是把这块存储卷释放回一个资源池,而下一个租户拿到的,就是主机商的清盘策略碰巧留下的东西。
- 在动手之前,先等旧服务器的访问日志彻底安静下来。支付服务商还在往旧地址推送 webhook 的时候就把服务取消掉,正是一次迁移在一周之后变成一场事故的常见原因。
- 要轮换,而不只是删除:旧服务器上存过的每一份凭证——API 密钥、数据库密码、部署密钥,还有那把临时的迁移密钥。只要是存在过一台你已经不再掌控的机器上的东西,就该当它已经泄露来处理,因为迟早,它真的会泄露。
- 释放这块存储卷之前,先把数据覆盖掉。把敏感目录彻底粉碎,或者用一个巨大的随机文件把剩余空间填满,再删掉它。在虚拟化磁盘上,这种做法并不完美,但远比什么都不做要好。
- 把任何你以后可能想参考的东西,做最后一次归档——日志、配置、记录着你实际做了什么的 shell 历史记录——然后把它加密存放在一个既不是这台、也不是那台服务器的地方。
- 只有在新服务器扛过了一整个计费周期,并且完整通过了一次备份与恢复测试之后,才去取消旧服务。这一个月的重叠期,是整个过程里最便宜的一份保险。
一份可以直接套用的时间表
这里面的每一步,单独拿出来都不难。迁移之所以会搞砸,是因为所有步骤被压缩进了一个晚上:TTL 还没过期,证书还没测试过,回滚方案也还没写。把它摊开到一整周里,每天不过是二十分钟的工作量。
迁移真正会在哪里出问题
不是出在复制这一步,而是出在那些从来就不在磁盘上的东西,还有那些被跑了两遍的东西。
- 两台服务器同时在写——也就是脑裂窗口期,是这里面唯一一种没有干净修复办法的故障。防住它靠的是顺序,而不是工具:先停掉旧的,再启动新的。
- 两台机器上的定时器都在跑,这正是客户收到的每样东西都变成两份的原因。要在最后一次同步之前关掉旧的那些,而不是之后。
- 一个从没被调低过的 TTL,会把一次五分钟的切换,拖成一整天没人安排人手去盯的长尾。
- 因为两台机器上的数字 ID 对不上,文件属主落地就错了位——应用照常启动,却连自己的上传目录都写不进去。
- 一张打算等切换完之后再处理的证书,撞上一条 HSTS 响应头,访客连继续往下走的办法都没有。
- 一个写死在你控制不到的某个地方的地址——可能在某个合作方的对接里,可能在一条防火墙规则里,也可能是某个你早就忘了它存在的子域名的 DNS 记录里。
- 在搬家当天就把旧服务器取消掉——恰恰是在统计数据告诉你最可能需要回滚的那个时间点,把回滚这条路给断掉。
VPS 迁移要花多长时间?
实际工作量通常是摊在一整周里的几个小时,而用户能感知到的部分只有几分钟。大批量传输和 TTL 调低,都提前好几天就完成了,期间一切照常运行;真正的切换,不过是最后一次增量同步、一次 DNS 变更,加一轮验证。一个只有几个 GB 数据、数据库规模也不大的网站,三十分钟就能从容完成切换,冻结时长通常只有个位数的几分钟。
能不能做到完全不停机的迁移?
对读取来说,可以——旧服务器会一直响应,直到 DNS 切换过去,之后再把它变成反向代理,连传播期的长尾流量都能一并解决。写入是更难的部分:一段短暂的冻结,是保证不丢数据最简单的办法,而复制能把这段冻结时间压缩到几秒钟。真正做到写入零冻结,需要应用在重叠期间同时写两个数据库——这是可以做到的,但也会引入一种大多数网站根本用不上的故障模式。
应该克隆磁盘,还是重建服务器?
重建。克隆会把配置漂移、孤立的软件包,还有过去某次入侵留下的任何后门都一并带过去,还会把你锁死在旧的发行版上。用脚本安装、只复制数据,换来的是一台干净的机器,加上一份可以重复使用的配方——而这份配方,也正是下一次重建能变快的原因。
我的 IP 地址和搜索排名会怎么样?
地址会变;域名不会,而链接、排名和历史记录,跟的都是域名。保持 URL 完全一致,在重叠期间让旧服务器继续响应,这样爬虫就永远不会遇到连接错误——这样一来,这次变更对搜索引擎来说,基本上是不可见的。在不同国家之间搬家,可能会让一些对延迟敏感的指标略有波动,所以在 /locations 上,挑一个离你真实受众更近的地区。
怎么迁移数据库才能不丢数据?
先让源头停止写入,再让目标端开始写入——这个先后顺序,就是全部的保证所在。对于小数据集,冻结、用 pg_dump 或者 mysqldump --single-transaction 转储、恢复,然后切换。对于更大的数据集,提前搭建好复制,在切换时把副本提升为主库,这样追上进度只需要几秒钟。在放行任何流量之前,通过比对关键表的行数来做验证。
能不能搬到一家离岸主机商那里,又不用暴露自己的身份?
可以。一家从加密货币余额里扣费的主机商,没有银行卡,也没有账单地址会绑定在这台机器上,所以服务器背后的这个账号,压根就不掌握任何和你有关的信息。迁移这件事本身,在技术上完全一样——同样的文件传输、同样的转储、同样的 DNS 切换。/offshore-vps 讲清楚了这套模式是怎么运作的,/pay-with 讲的是支付那一部分。
什么时候取消旧服务器才算安全?
至少要等它的访问日志安静了一整天,它持有过的每一份凭证都已经轮换完毕,而且你已经给新服务器做过一次备份,并且成功在别处恢复过,这时候才算安全。多花一个月租一台小 VPS,是整个过程里最便宜的一份保险——这笔钱,决定的是你面对的究竟是一次简单的回滚,还是一场事故。


