拷贝是最简单的部分——真正造成停机的是什么
问别人他们的迁移要花多久,得到的答案通常是传输时间的估算:两百 GB 数据走千兆链路,算它四十分钟,再加一小时余量。这个数字通常没错,却几乎从来都不是真正要紧的那个数字,因为传输发生的时候,旧服务器依然在应答请求。拷贝期间没有人会掉线。真正掉线的区间,是新机器已经就绪和互联网也认同这一点之间的那段空档,而这段空档的长短,由你根本控制不了的缓存决定。
真实迁移事故里,绝大多数都出在四种失效模式上,没有一种和带宽有关。第一种:改动 DNS 记录时,它的存活时间依然是一整天,于是相当一部分用户还在连着那台你已经关掉的机器。第二种:数据库在你完成转储之后又接受了写入,新主机上线时带着一份悄悄过时的数据,而你要等到客户投诉,才发现丢的正是那二十分钟。第三种:TLS 证书连同它的 ACME 账户密钥,只存在于旧磁盘上,于是新主机给每个浏览器都返回一个名称不匹配的证书。第四种:某个出站集成——支付处理商、银行的 SFTP 投递、合作伙伴的 API、第三方托管的数据库——是按 IP 地址加入白名单的,它会在离真正病因最远的地方悄无声息地失败。
有句人人都在用的话,值得说得直白一点。“零停机迁移”几乎总是指那几秒钟没人注意到的连接被拒,而把它当作目标其实完全合理。真正意义上的零停机,需要两台机器同时正确地对外服务,这又要求应用本身能接受“自己同时是两台机器”这件事:没有本地会话状态,没有只存在于一侧的本地上传目录,数据库要么能复制,要么干脆放在别处。大多数自建应用栈都够不到这个标准,诚实的方案是一次短暂、可控、经过演练的冻结,而不是一个你原本没打算启动的架构项目。
所以一次好的迁移,形状并不取巧:提前把新机器建好,趁旧机器还扛着负载时就证明它能用;把不可逆的部分压缩成几分钟、写成脚本的命令;一次只改一件事;并且始终留一条能退回去的路,同时给这条退路定一个不再为它买单的日期。
盘点:那些不在代码仓库里的服务器组成部分
你的部署仓库描述的是应用,不是这台机器。从第一次 apt install 到今天之间的某个时刻起,服务器已经积起了一层任何版本控制都从未见过的东西:为了解决某个问题而手动装上的软件包、有人赶时间写的 systemd 单元、某个服务账户下的 cron 任务、防火墙规则、sysctl 调优、locale、时区、交换文件、TLS 证书、SSH 主机密钥、数据库角色与授权,还有一个不在仓库里的媒体目录——之所以不在仓库里,恰恰是因为它是用户自己放进去的。迁移意味着要刻意把这一层复现出来,而做到“刻意”的唯一办法,就是先把它写下来。
五条命令就能捕捉到其中的大部分。Debian 和 Ubuntu 上的 dpkg --get-selections,或者 Rocky 和 Alma 上的 dnf repoquery --userinstalled,给你软件包清单。systemctl list-unit-files --state=enabled 给你所有配置为开机启动的项目,这比“什么在运行”更值得问,因为它也包括那个目前已经崩溃的东西。用 crontab -l -u 对 /etc/passwd 里的每个账户执行一遍,再加上 /etc/cron.d 的内容和每一个 .timer 单元,给你计划任务。nft list ruleset 或 ufw status numbered 给你防火墙规则。而 ss -tulpn 给你实际正在监听的内容,这正是你发现那个自己都忘了存在的服务的办法。把这五项都写进文件,拷到机器之外,而不是拷到机器上。
还有一类清单,没人会主动写下来,却是几周后真正咬人的那一种:所有信任你当前 IP 地址的东西,不管在哪里。带 IP 白名单的支付网关;另一家服务商那边的托管数据库,其访问规则写死了旧地址;按网络来源认证的 SMTP 中继;一个监控服务、一个只往已知地址推送的 webhook 发送方、同事的防火墙。把自己的配置和文档都翻一遍,搜一下旧地址,再逐一检查你付费用的每个第三方服务的控制面板。这些东西会在切换之后才出问题,而且是异步地、在集成下一次运行的那个不确定时刻出问题——这正是它们经常被误诊为“同时发生了别的什么问题”的原因。
最后,把 DNS 区域本身也记录下来。导出它,或者至少把每条记录的类型、值和 TTL 都记下来,因为迁移恰恰就是那种时刻——总有人会突然注意到一条已经错了两年的记录,然后好心地在一堆事情正忙的时候顺手把它改掉。
| 要盘点什么 | 它存在哪里 | 忘了会出什么问题 |
|---|---|---|
| 手动安装的软件包 | 软件包数据库 | 新主机上某个服务因为缺少一个没人记得装过的库而拒绝启动。 |
| 已启用的 unit 和 timer | systemd | 后台工作悄悄停止:没有备份、没有证书续期、没有队列 worker。 |
| 每个用户的 cron 任务 | 各账户的 crontab | 夜间任务悄然消失,直到月底才被发现。 |
| 防火墙规则集 | nftables 或 ufw | 要么某个端口一直关着、应用看起来像坏了,要么干脆全部敞开。 |
| TLS 证书与 ACME 账户密钥 | 证书目录 | 浏览器显示名称不匹配,续期还会开出一个全新账户。 |
| 数据库角色与授权 | 服务器全局配置,不在转储里 | 数据恢复得很完整,应用却登录不进去。 |
| 第三方 IP 白名单 | 别人家的控制面板里 | 支付、webhook 和合作伙伴 API 会在数小时甚至数天后失败。 |
| 带 TTL 的 DNS 区域 | 你的 DNS 服务商 | 你说不清哪些记录还需要迁移,也说不清它们被缓存了多久。 |
先降低 DNS TTL——并且要弄清楚为什么这一步必须提前好几天
递归解析器会把一个应答缓存到它拿到的存活时间为止,在到期之前,它没有任何义务再次询问。这一点带来一个几乎人人第一次都会栽进去的后果:把一条记录的 TTL 从 86400 降到 300,对十分钟前刚缓存过旧答案的解析器来说,毫无意义。那个解析器还会继续持有这个“一天有效”的答案二十三小时五十分钟,直到那时才会得知你新设的五分钟 TTL。所以,低 TTL 必须在你打算迁移任何东西之前、至少提前一整个旧 TTL发布出去。如果你的记录用的是一天的 TTL,迁移的第一个动作就要在正式切换前两天完成,而且只是一行改动。
对每一条指向这台机器的记录都要这么做,而不只是你脑子里想到的那一条。A 记录和 AAAA 记录——一个还留着指向旧主机的 IPv6 地址,是那种会把人绕晕的经典 bug,因为双栈客户端会悄悄优先选用它,而且只有部分用户会受影响。MX 记录。它们前面挂着的任何 CNAME 也不例外,记住:客户端沿着一条链走下去,要受链上每一环各自 TTL 的约束,所以一条五分钟的 A 记录,如果背后挂着一条一天有效期的 CNAME,实际效果依然是一整天。而且,如果你同时还要更换 DNS 服务商,千万别把这两件事放在同一周做:域名服务器委派和粘合记录,是由上级区域按它自己的节奏缓存的,两个改动叠在一起,出问题时你根本分不清是哪一个改动引起的。
验证,而不是假设。dig +noall +answer example.com A 会告诉你解析器目前持有的答案,以及它剩余的倒计时;直接用 dig @ns1.example.net example.com A 查询你的权威服务器,则会告诉你实际发布的内容。如果两者对不上,你看到的就是缓存,而记录旁边的那个数字,恰好就是你还得等多久。
有两个细节值得预留余地。第一,部分解析器会做钳制:一部分 ISP 和企业级转发器,不管你发布的是什么,都会强制执行一个最低 TTL,这在技术规范上是不被推荐的做法,但确确实实存在。第二,根据 RFC 8767,解析器在联系不到你的权威服务器时,可能会故意返回一个过期答案——这对韧性来说是优点,但在你希望改动尽快传播开的那天,就成了麻烦。综合这两点,在大部分流量已经转移之后,要为一条以小时计、偶尔长达一天的长尾流量预留计划。只要旧机器依然能正确应答,这条长尾就不是问题——这也正是让它继续开着的全部理由。RFC 2181 是权威参考,规定了各实现意见不一致时 TTL 应有的行为。
| 时间点 | 动作 | 为什么在这个时间点 |
|---|---|---|
| T 减 7 天 | 盘点旧服务器;写好操作手册 | 这一步发现的一切都会影响计划,所以必须在计划定稿之前完成。 |
| T 减 3 天 | 开通新 VPS;首次恢复;演练 | 留出时间,好在演练发现漏洞时把机器第二次重建一遍。 |
| T 减 2 天 | 把 A、AAAA 和 MX 的 TTL 降到 300 | 必须让一整个旧 TTL 完全过期,低数值才能进入每一处缓存。 |
| T 减 1 天 | 第一次 rsync;启动数据库复制 | 耗时长且无需值守,旧服务器依然承载着生产流量。 |
| T 零点 | 冻结写入、最终同步、验证、切换 DNS | 唯一不可逆的窗口;之所以短,是因为上面的一切都已经做完了。 |
| T 加 1 小时 | 同时观察两边的访问日志 | 旧主机上流量的衰减曲线,才是缓存到期的真实度量。 |
| T 加 7 天 | 下线旧主机;把 TTL 调回原值 | 回滚已经过期;永久保持 300 秒的 TTL 只会增加延迟,没有任何好处。 |
趁旧机器还在服务,先在新机器上演练
整个流程里价值最高的一个习惯,就是让新机器提前好几天就存在并且真正能用。实际上,VPS 按小时计费:在我们的套餐上,付款确认后大约六十秒服务器就会上线,没有开通费,也没有合同,所以在 8.50 美元档 上做三天演练,成本大约只是一美元。这是一种极其便宜的办法,能在时钟真正开始跑之前,把迁移里的每一个未知数都变成已知数。
要用主机名测试,而不是用 IP。直接用浏览器访问新服务器的地址,你得到的只是默认虚拟主机、没有 SNI 匹配、一个证书警告,以及一套可能完全不像生产环境的路由——而且它还会让你误以为某样东西是正常的,实际上并不是。正确的工具是解析器覆盖。curl --resolve 能在单条命令里把一个主机名钉死到一个地址上,所以 curl --resolve example.com:443:203.0.113.10 https://example.com/ 会针对新机器,走真实的域名、真实的 TLS 握手和真实的虚拟主机,同时全世界其他人依然连到旧机器。要做交互式测试,在自己工作站的 hosts 文件里加一行,对浏览器来说效果是一样的。
演练能稳定抓出的,正是上一节里说的那层积累下来、没被记录过的东西。一个自上次重启前就一直在跑、其实并未真正启用的守护进程。一个依赖本地邮件传输来报告失败的 cron 任务。文件属主在拷贝过程中以数字形式被保留了下来,可在一台 /etc/passwd 不同的机器上,这些数字的含义完全变了。配置文件里写死的地址。某个数据库是在字符集默认值不同的版本上,用某一种字符集建的。某个 Python 或 PHP 的次版本,弃用了你正在用的某个特性。这些问题,在演练阶段修复只要十分钟,等到了正式切换阶段再去诊断,就要花一小时。
有一项测试比其余所有都更有价值:把新机器建两遍。只凭你的盘点清单和备份,从零重建一次,完全不去看旧服务器。如果第二次也能正确启动,说明你的盘点是真实的,操作手册是可执行的。如果不能,你恰好是在一个发现问题不花任何代价的场景里,找到了那个漏洞。做对了之后打一份快照——每个套餐都自带每小时快照、保留七天,恢复大约只要三十秒——这样一来,之后在新机器上做的任何实验,也都能撤销。
rsync 跑两遍:服务运行时跑一次长的,窗口期内跑一次短的
两遍模式正是把维护窗口压短的关键。第一遍在服务完全在线的状态下运行,想跑多久就跑多久——几个小时、跑一整夜,甚至横跨一个周末都可以。第二遍在冻结期间运行,只传输自第一遍以来发生变化的部分,对一般的工作负载来说,这只是数据中很小的一部分,耗时也就是可预测的几分钟。你是在用一次长时间无人值守的拷贝,换一次简短的、有人盯着的拷贝,而这笔交易几乎总是划算的。
参数比大多数人以为的更重要。rsync -aHAX --numeric-ids 是基准配置:-a 表示归档模式,-H 保留硬链接——这一点对 Maildir 存储和去重备份树至关重要,丢掉它会让你的磁盘占用成倍增加——-A 保留 POSIX ACL,-X 保留扩展属性,比如 SELinux 标签和文件能力(capabilities),--numeric-ids 则让用户 ID 以数字形式直接拷贝,而不是先在源端的用户数据库里解析一遍,再在目标端用另一份用户数据库重新解析一遍。加上 --info=progress2 能得到一行清爽的进度信息,加上 --partial 能让被中断的传输续传,而不是从头重来。除非链路确实很慢,否则不要加 -z:对已经压缩过的媒体文件和加密归档再做压缩,只会把瓶颈转移到 CPU 上。
--delete 只在第二遍里使用。在第一遍里用它纯属自伤——没有任何好处,因为目标端本来就没有需要删除的东西。到了第二遍,它就变得必不可少:不用它的话,你在两遍之间从旧服务器上删掉的文件,会在新服务器上永远留存下去,迁移最后变成了继续提供一份你本来存心删掉的内容。
你不应该做的一件事,是把整个根文件系统 rsync 过去。这是一种很流行的捷径,结果却是一台谁都说不清道理的机器。哪怕排除掉 /proc、/sys、/dev、/run 和 /tmp,你得到的系统,内核和 initramfs 来自一台旧主机,网络接口命名源自完全不同的硬件,/etc/machine-id 现在在两台在线机器上重复了,还有那积攒了五年的配置漂移,被你原封不动地花力气保留了下来。应该做的是:迁移你盘点过的数据和配置,操作系统则用模板重新装一份干净的。这多花一个小时,换来的是一个干净、现代的基础,也意味着新服务器是一台你真正理解的机器,而不是一台你从未理解过的机器的复制品。
验证拷贝结果,而不是相信退出码。两遍都跑完之后,再用 -n --checksum --itemize-changes 把同一条命令跑一遍:这是一次比较内容而非时间戳的空跑(dry run),每一处差异都会打印一行。你想要的结果是沉默——这比两边 du -s 数字对上,是强得多的证明。
数据库是 rsync 帮不上忙的那部分
在文件层面拷贝一个正在运行的数据库目录,得到的是一份撕裂的副本:有些页面写进去了,有些没有,也不保证其中任何部分构成一个一致的事务边界。InnoDB 很可能会从中恢复过来,看起来一切正常,而这比直接失败更糟糕,因为你只会在某天恢复失败的时候才发现问题。真正诚实的策略只有两种——做一次事务一致的转储,或者做复制——而在两者之间做选择,其实就是在选择你愿意为多少停机时间买单。
在 MySQL 和 MariaDB 上走转储这条路,mysqldump --single-transaction --routines --triggers --events 能在不锁住整个服务器的情况下给出一份一致的快照,但有两点需要留意:它只对事务性表保证一致性,哪怕只有一张老旧的 MyISAM 表,也会悄悄破坏这个保证;转储过程中执行的任何 DDL 语句同样会破坏它。在 PostgreSQL 上,pg_dump -Fc 负责处理每一个数据库,而 pg_dumpall --globals-only 负责处理人们常常忘记的那一半——角色、密码、授权和表空间存在于集群这一层,而不在任何单个数据库内部,恢复时如果漏掉它们,数据本身会完美无缺,应用却没法通过身份验证登进去。
真正决定你窗口长度的数字,不是转储文件的大小,而是转储加传输加恢复加索引重建的总和,而其中往往是索引重建耗时最长。用真实数据集在演练阶段实测它,而不是靠估算。几个 GB 的量级是几分钟;上百 GB、又带着大量索引的话,可能要几个小时——到了这个量级,转储这条路对在线服务来说已经不再可行,复制就不再是一种优化,而是唯一的答案。
复制这条路把成本反过来了:你提前把工作做掉,切换本身就变得微不足道。提前好几天在新主机上建好一个副本——MySQL 和 MariaDB 用基于 GTID 的复制,PostgreSQL 用 pg_basebackup 加流式热备,如果同时还要跨大版本升级,就用逻辑复制。让它追上进度,并且一直保持追平。这样一来,切换就变成:停掉主库的写入,等副本报告延迟为零,把它提升为主库,再把应用指向它。这只需要几秒钟,也是唯一真正配得上“接近零停机”这个说法的方式。
别忘了那些不在数据库里的状态。Redis、Valkey 和 Memcached 里存着会话、速率限制计数器和队列;Elasticsearch、OpenSearch 或 Meilisearch 里的搜索索引,存着你数据的一份派生副本。针对每一种,都要明确决定是迁移它还是重建它。如果 Redis 实例里存的是会话,值得用一次 BGSAVE 加上拷贝生成的文件来迁移它,除非你不介意让所有人都被登出。搜索索引几乎总是在新主机上重新索引比传输更快——但重新索引需要时间,所以要在演练阶段就开始做,而不是留到冻结期间。
| 策略 | 切换停机时间 | 窗口之前要做的工作 | 什么时候该选它 |
|---|---|---|---|
| 直接文件拷贝一个正在运行的数据目录 | 不停机,但数据可能已损坏 | 无 | 永远不该选。之所以列在这里,是因为这是人们最先会尝试的做法。 |
| 一致性转储加恢复 | 转储加传输加恢复加重建索引 | 在演练阶段实测一次 | 数据集小到实测总耗时在你能接受的范围内。 |
| 转储、恢复,再回放增量 | 几分钟 | 提前配置好 binlog 或 WAL 保留策略 | 中等规模的数据集,建完整副本的成本超出你的预期。 |
| 切换时提升副本 | 几秒钟 | 复制已运行且连续多天零延迟 | 任何大规模场景,以及任何无法接受长时间冻结的场景。 |
| 停掉数据库,冷拷贝文件 | 拷贝耗时的长短 | 无 | 小型内部服务,下线一小时确实毫无代价。 |
TLS 证书、SSH 主机密钥,以及那些不能说重签就重签的身份
证书签发在最不凑巧的时刻,恰好存在一个先有鸡还是先有蛋的问题。HTTP-01 challenge 通过这个域名去抓取一个文件来证明你控制着它,这要求这个域名已经解析到了正在被验证的机器——而在切换之前,它并没有。这里有三条出路,你应该刻意选定其中一条。把证书目录直接拷过去,这样立刻就能用,而且完全正当,因为那本来就是你自己的密钥。改用 DNS-01 challenge,它通过发布一条 TXT 记录来验证,因此在一台目前还没有任何东西指向它的机器上也能用,而且它也是通配符证书的唯一选择。或者,接受 DNS 切换之后有一小段窗口期,让新主机自己去申请证书——这对个人网站没问题,但对任何有真实用户的东西都不行。
如果要拷贝,就把整个目录都拷过去。证书和私钥是显而易见要拷的部分;ACME账户密钥才是那个容易被落下的部分,没有它,新主机上的客户端会在第一次续期时注册一个全新账户。表面上一切正常,但你已经悄悄丢掉了这个账户的历史记录和一切账户级状态。属主和权限也要一并保留——私钥保持 600,归档目录保持 700——因为一次拷贝如果放宽了密钥的权限,造成的后果,会比你原本担心的那次迁移本身还要糟糕。
不要指望反复重新签发、看看会发生什么。Let's Encrypt 的速率限制对完全相同的一组主机名,每周能签发的重复证书数量设了上限,而两次失败的演练加上一次真正的签发,就足以在那个你完全等不起的日子里,轻易撞上这个上限。演练要指向 staging 环境;生产端点留给真正要用的那一次。
SSH 主机密钥是另一个在场的身份,这里的正确答案,其实取决于你为什么要搬家。拷贝 /etc/ssh/ssh_host_* 能让迁移变得不可见:每个客户端的 known_hosts 依然对得上,不会有任何自动化流程因为指纹警告而卡住。这样很方便,对于在你信任的服务商之间做的常规搬迁来说,也是正确的选择。但如果你离开的原因,是不再信任旧主机或旧服务商,那这就是错误的选择——任何能接触到那块磁盘的人,都已经拿到了你的主机私钥,把它带去新服务器,只会把这个问题一并带过去。这种情况下,应该生成全新的密钥,通过服务器之外的渠道发布新指纹,并让客户端对旧的记录执行 ssh-keygen -R。密钥存在哪里、又是怎么被选中的,参见 OpenSSH sshd 手册。
不管选哪一种,有一条规则是绝对的:绝不能让两台机器同时使用同一个主机密钥,并且同一个主机名又同时解析到两边。这种配置下,客户端根本没法分辨自己连到的到底是哪一台服务器——而这恰恰就是主机密钥存在的意义所要阻止的事情。在两者重叠的窗口期里,要么旧主机保留自己的身份、新主机用它自己的身份,要么在新主机接过密钥的那一刻,就立刻收回旧主机的名字。
正式切换:那十分钟需要一份写下来的执行顺序
到目前为止的一切,都是可逆的,也不急于一时。正式切换两者都不是,这正是要事先把它写下来的全部理由:在时间压力下临场发挥,正是把一个五分钟的窗口,变成一场两小时事故的原因。操作手册应该是按顺序排好的、逐字照抄就能执行的命令,每一条旁边都写上预期输出,这样执行它靠的是阅读,而不是思考。
行之有效的顺序是这样的。第一步,停掉旧主机上的写入——一个维护页面、一个只读标志位,或者干脆停掉应用本身,同时让 Web 服务器继续用一个占位页面应答。第二步,用 --delete 跑最后一遍 rsync。第三步,完成数据库这一步:要么执行你已经实测过的转储—传输—恢复流程,要么停掉主库的写入,确认复制延迟归零,再把副本提升为主库。第四步,启动新主机上的各项服务,给它们一点安顿的时间——连接池、队列 worker 和证书加载,冷启动都比你记忆中的要慢。第五步,验证。第六步,也是只有在验证通过之后,才去修改 DNS 记录。第七步,让旧主机继续开着。
验证必须真正说明问题。抓一下首页,只能证明 Web 服务器活着,别的什么都证明不了。要针对真实主机名使用 curl --resolve,访问一个会经过数据库、并返回可断言内容的路径——单单为了这个理由,就值得专门写一个检查数据库连接、缓存连接和剩余磁盘空间的健康检查接口。然后去读新主机的访问日志,确认你的请求确实出现在里面。这最后一步听起来有点吹毛求疵,但正是这一步,能抓住“解析器覆盖其实没生效、你辛辛苦苦验证的其实是旧服务器”这种情况。
DNS 切换之后,把两边的访问日志放在一起看。新主机上的流量在爬升;旧主机上的流量随着缓存过期而衰减,而这条衰减曲线,才是长尾究竟有多长的唯一真实度量。当旧主机的日志衰减到接近于零,迁移才算真正完成。在那之前,旧机器必须继续为还连着它的用户正确地工作——这正是它要保持开机、只读,而不是在一时兴起之下就被关掉的原因。如果应用没法做成只读,旧主机继续提供数小时的过期数据,通常还是比旧主机直接拒绝连接的危害要小——但这是一个应该提前做出的判断,而不是临场决定。
一次只改一件事。不要同时换操作系统大版本又换主机。不要同时换 DNS 服务商又换记录。不要同时换 PHP 版本又换机器。每多一个同时发生的改动,凌晨三点出问题时你要排查的假设就会成倍增加,而那个你曾经很想顺手一起做掉的升级,下周依然在那儿等着你——而且是在一台你现在已经可以回滚回去的机器上。
邮件、rDNS,以及不会跟着你一起搬走的信誉
如果这台服务器要发邮件,迁移就还有另外一半,而这一半其实跟迁移本身没什么关系。一个 IP 地址带着一份发信信誉,这份信誉是花几周时间攒出来的,而且不会跟着你一起搬过去。这是一次技术上完美无缺的迁移,最终却换来一整周用户投诉的最常见原因,而所有的缓解措施,都必须在切换之前就开始,而不是之后。
最先出问题的记录是 SPF。一条包含 ip4: 机制、写着旧地址的策略,会在你从新地址发信的那一刻起就开始失败,而且由于 RFC 7208 策略本身就是 DNS 记录,它们也有自己的 TTL 需要提前规划。要在切换之前把新地址加进 SPF,重叠期间两个地址都保留,等下线时再把旧地址移除。只要把私钥连同其余配置一起拷过去,DKIM 就能顺利迁移,因为公钥那一半早就已经发布出去了。DMARC 不需要改动,但值得确认一下:报告地址,是不是还依赖着一台即将被销毁的机器。
对一台要发信的主机来说,反向 DNS 不是可选项。新地址上的 PTR 记录必须存在,而且必须和服务器在 HELO 或 EHLO 里报出的名字一致,否则相当一部分收信方会仅凭这一点就拒收。在我们的套餐上,PTR 可以直接在面板里编辑,是打个勾就能搞定的事,不用开工单——在演练阶段就把它设好,验证它能正确解析,并确认正向记录也能对得上。在真正发出任何邮件之前,先拿新地址去对照主要黑名单查一遍:回收利用的地址,有时候一到手就已经在名单上了,用自己的测试邮件发现这一点毫无代价,等客户帮你发现,代价就大了。Spamhaus 是其中最要紧的一个。
在收信这一侧,要让两台主机都保持可达。把新服务器加为优先级 10 的 MX,旧的那台保留在优先级 20,留一周时间,而不是直接删掉:发信方会重试,其中一些还缓存着你的 MX 记录,一台在旧主机上默默收下邮件的服务器,远比一次退信要好得多。然后,在销毁旧机器之前,先强制投递并清空它的队列——用 postqueue -f 强制尝试投递,再用 mailq 确认结果为空。留在一台你即将删除的服务器队列里的邮件,不是被延迟了,而是彻底没了。自建邮件服务器指南完整讲述了送达率这一侧的内容。
当旧服务商已经把机器关掉了
并不是每一次迁移都是计划好的。一次滥用投诉之后的账号暂停、一张过期的信用卡、一个没人会解释原因就被锁住的账户——共同点都是:你现在要迁移的这台机器,你已经登录不进去了,上面那套从容的操作顺序也不再适用。提前了解一下通常还能做些什么,是值得的,因为能做这些事的窗口,往往很短。
在实例被暂停、但还没被删除的这段时间里,通常还剩三条路,按有用程度从高到低排列。控制面板可能依然能提供快照或备份下载,这一步就能一次性解决全部问题。救援模式或恢复模式,可能依然能挂载着你的磁盘启动一个可用的镜像,让你挂载文件系统,把要紧的东西通过网络拷出来。而紧急控制台,即便网络已经被禁用,往往依然能连上,这足够你读取配置文件、找回一把密钥,或者记下都装过什么——但它走不通数据库这条路,所以不要指望靠它。按这个顺序依次尝试,而且要立刻就试:暂停之后的数据保留,是一项政策,而不是一项权利,而且常常只以天为单位计算。
有一条界限,是任何工单都解决不了的,值得直白地说清楚,因为它是这个网站所卖的这套模式的直接后果。在一个从未收集过身份信息的主机商这里——包括我们自己——除了持有账户凭据本身,没有任何办法能证明你就是账户持有人。这正是无 KYC 主机的意义所在,也是它要付出的代价:没有人能凭一张护照照片替你恢复访问权限,因为压根就没有任何护照曾经和这个账户绑在一起过。无 KYC 主机指南完整讨论了这笔权衡;操作层面的后果很简单:你的恢复计划,没法指望走任何人的人情。
应急流程,就是把计划内流程里的重叠期去掉之后剩下的那部分。用你保存在别处的备份,把新主机跑起来。老老实实按主机名验证,因为恰恰是在你已经宕机的这个时刻,跳过验证的诱惑才最强烈。然后立刻切换 DNS——TTL 的那套纪律依然适用,只不过现在你是把缓存长尾当成停机在支付,而不是当成重叠期在支付,这也是“把 TTL 长期保持在较低水平”这件事,应该是一项常设策略、而不只是迁移时的临时步骤的最有力理由。要预期邮件服务会有一段时间质量下降,并提前告知用户,而不是事后再解释。
这个教训不止适用于倒霉的这一天。一份放在服务器本身上的备份,根本不算备份;它只是你即将失去的那份东西的第二份拷贝而已。真正要紧的属性是:这份拷贝在别处,离开服务器之前就已加密,只能追加、不能删改,这样一台被攻陷或被暂停的主机就没法删掉自己的历史记录——而且,还有那个人人都会跳过的部分:真正实际恢复过一次,恢复进一个用完即弃的实例里,这样你才知道恢复真的能用。加固清单讲了怎么在一台新机器上把这些都建起来;如果你读到这一节,是因为已经来不及了,那就在你正要迁移过去的那台机器上,今天,在做任何别的事情之前,先把它建起来。
回滚:定义触发条件,并给它设一个到期日
几乎每个人都说自己有回滚方案,但几乎没有人写下过究竟是什么会触发它。没有触发条件,这个决定就会在最糟糕的时刻,由团队里最疲惫的那个人来做,而通常的结果是没有人回滚,因为眼下这个问题看起来总像是再有五分钟就能修好。要提前定好:错误率超过某个阈值并持续了多久,某个具体功能坏了,或者干脆定一个固定时刻,过了这个时刻就停止排查、直接回退。
决定“回滚”到底意味着什么的那个约束条件,是写入。从新主机接受第一次写入开始,回退就意味着要么放弃这些写入,要么把它们重新回放到旧机器上。所以你实际上是在两种完全不同的东西之间做选择。第一次写入之前的回滚,只是一次 DNS 改动,几分钟就能搞定。经过一整天生产流量之后的回滚,则是一次方向相反的迁移,步骤和本指南讲的完全一样,耗时也和本指南描述的一样长。要在任何时刻都清楚自己面对的是哪一种,而且要记住:如果你让旧机器的数据库始终保持能接收恢复的状态,而不是把它挪去做别的用途,第二种回滚会容易得多。
把旧服务器留大约一周:开着机、只读、DNS TTL 依然保持低值、监控依然指着它。在最小档套餐上,这份保险大约只要 2 美元,这个数字根本不该出现在决策考量里。真正该考虑的是:一周的时间,足够让每周跑一次的 cron 任务、差不多按月出的报表,以及那个只在周五触发的集成,都至少在新主机上跑过一次——而这些恰恰是一个两天窗口会完全漏掉的故障。
然后,有意识地去下线它。先确认旧主机的访问日志已经平了一整天。给它做最后一次备份,保留的时间要比你觉得需要的更长一些。撤销它持有的一切:API 密钥、部署密钥、数据库用户、它在别人 IP 白名单里的条目、它的监控检查、它的 SPF 条目、它的 MX 记录。销毁这个实例。最后,把 DNS TTL 调回一个合理的值——永久保持 300 秒的 TTL,意味着解析器在网站往后的整个生命周期里,每小时都要重新验证二十次,一旦迁移结束,这不会给你带来任何好处,只会让每一次冷查询都多一点延迟。一小时是一个合理的常设值;如果是那种你完全不指望会很快变动的记录,一天也可以。
整个迁移流程,按执行顺序排列
提前一周,盘点旧机器,写好操作手册:软件包、已启用的 unit、crontab、防火墙、监听中的套接字、数据库角色,以及每一个把旧地址加入过白名单的第三方。提前三天,开通新 VPS,把数据恢复上去,用主机名加解析器覆盖,把整个流程演练一遍——然后只凭盘点清单再重建一次,证明这份盘点是真实可靠的。提前两天,把 A、AAAA 和 MX 的 TTL 降到 300,让一整个旧 TTL 有时间在你真正需要低值生效之前完全过期。
提前一天,在生产环境照常运行的同时,用 -aHAX --numeric-ids 跑第一遍 rsync,并且把数据库复制建起来,或者用真实数据集实测转储窗口有多长。同一时间段把证书也一并处理好——拷贝包含 ACME 账户密钥在内的整个证书目录,或者改用 DNS-01——并且在新地址真正发出任何邮件之前,先把 PTR 记录和新的 SPF 条目设置好。
到了窗口期:冻结写入,用 --delete 跑最后一遍 rsync,完成数据库这一步,启动服务,针对真实主机名、走一条会经过数据库的路径去验证,确认请求确实落到了新主机的日志里,然后才切换 DNS。让旧主机保持开机、只读。同时盯着两边的访问日志,直到旧主机那边的曲线趋平。
一周之后:确认每周任务已经在新主机上跑过,给旧主机做最后一次备份,撤销它的密钥以及散落各处的白名单条目,销毁它,把 TTL 调回去。如果你想要的新机器不只是更新,而是比你离开的那台更好,加固的头十五分钟是自然而然的下一步,而且在一台还没积累出自己那层未记录状态的服务器上做这件事,要容易得多。如果你还没决定落在哪里,司法管辖区指南讲清楚了冰岛、荷兰、罗马尼亚和瑞士之间真正的差异——而且你可以在大约一分钟内把目标机器跑起来,今天就开始演练。