同域名换服务器:零停机的六个阶段与回滚预案

把一台跑了三年的服务器换掉,听起来是件简单事:买新机器、传文件、改 DNS、完事。真做过的人都知道,最容易出事的不是迁移过程,而是切换 DNS 之后的头两个小时——旧机器上还有人在写数据,新机器上数据库已经跑起来了,两边各写一份,等 DNS 完全生效时,你已经分不清哪边是最新的。

还有一种更常见的翻车方式:改完 A 记录,发现新机器上某个扩展没装、某个 cron 没配、Nginx 配置文件抄漏了一段,于是赶紧把记录改回去。可惜 DNS 有缓存,你的浏览器刷新一下就能回滚,但已经拿到新 IP 的那部分用户,会在缓存过期之前一直访问一台有问题的机器。

所以"同域名换服务器"这件事,关键不在于搬家的技术难度,而在于 DNS 的传播特性决定了你必须提前做规划。域名不变、URL 不变、内容不变,唯一的变量是 IP。这篇文章把整个流程拆成六个阶段,每个阶段给出可执行的命令和检查项,目标是一次切换零停机、可回滚。

阶段零:切换前 48 小时,先把 TTL 降下来

所有准备工作里,这一步最容易被跳过,但它决定了你切换时能有多灵活。DNS 记录的 TTL 决定了下游解析器把旧 IP 缓存多久。如果你的 A 记录 TTL 是 3600 秒,那么改完之后最长会有 1 小时以上的窗口,用户仍然在访问旧机器;如果你想把旧机器关掉、或者想回滚,就必须等这个窗口过去。

正确做法是提前两天把 TTL 降到 60 秒:

# 先看当前的 TTL 和记录
dig +noall +answer www.example.com A
# www.example.com.  3600  IN  A  203.0.113.10

# 改成 60(Cloudflare / DNSPod / 阿里云控制台操作,或 API)
# 改完等待旧的 3600 秒缓存过期,所以务必提前 24~48 小时操作

这里有个细节:把 TTL 改成 60 之后,还得等原来 3600 秒的缓存自然过期,新的 60 秒 TTL 才会生效。也就是说,改 TTL 和真正切换之间至少要隔一个旧 TTL 的时长。很多人当天改 TTL 当天切,等于白改。

降完 TTL 用几个公共解析器验证一下,确认权威服务器返回的已经是新 TTL:

dig @8.8.8.8 www.example.com A +noall +answer
dig @1.1.1.1 www.example.com A +noall +answer
dig @223.5.5.5 www.example.com A +noall +answer

如果站点套了 CDN,情况会更简单一些:只要 CDN 的回源地址能改,用户侧的 DNS 完全不用动。但要注意,CDN 回源到新机器时,源站必须能正确处理 X-Forwarded-For 和 Host 头,否则日志里的 IP 全是 CDN 节点,限流和风控会失效。这一点在第 1756 篇讲限速的文章里提过一次,换服务器时尤其容易踩。

阶段一:新机器的基础环境对齐

第二件容易翻车的事是"新机器看起来一样,其实不一样"。手工装系统和三年前装系统的人可能不是同一个人,中间改过什么配置早就忘了。所以不要靠记忆,直接对比。

在旧机器上把所有需要对齐的东西导出成清单:

# 已安装的软件包(Debian/Ubuntu)
dpkg --get-selections | grep -v deinstall > /root/pkgs.txt
# 系统版本、内核
. /etc/os-release; echo "$PRETTY_NAME $(uname -r)"

# 监听端口和对应用途
ss -lntup

# 定时任务(root 和各站点用户都要查)
crontab -l > /root/cron_root.txt 2>&1
ls -l /etc/cron.d/ /etc/cron.daily/
for u in www-data mysql; do crontab -u $u -l; done

# systemd 里手工加过的单元
ls -l /etc/systemd/system/*.service

# Nginx 站点配置和 PHP 扩展
nginx -T > /root/nginx_full.conf 2>&1
php -m > /root/php_modules.txt

新机器装好系统之后,逐项对照这份清单。特别注意三样东西:时区(timedatectl)、PHP 版本与扩展(版本差一个小版本,某些老代码就会出问题)、数据库版本与字符集(collation 不一致会导致排序和唯一索引行为不同)。这三项不会报错,只会悄悄让数据出错。

timedatectl                         # 应为 Asia/Shanghai,NTP 已同步
php -v; php -m | sort > /tmp/new_php.txt
diff /tmp/new_php.txt /root/php_modules.txt    # 差集就是漏装的扩展
mysql --version
mysql -e "SHOW VARIABLES LIKE 'character_set_server'; SHOW VARIABLES LIKE 'collation_server';"

阶段二:数据迁移,先全量再增量

数据分两类:数据库和文件。先做全量,这一步可以在业务正常运行的时候做,慢一点没关系。

# 数据库:一次性导出,用 --single-transaction 保证一致性快照
mysqldump --single-transaction --quick --default-character-set=utf8mb4 \
  --routines --triggers --events bbs | gzip > /root/bbs_full.sql.gz

# 文件:用 rsync 走 SSH,压缩传输
rsync -aHAX --numeric-ids --info=progress2 \
  -e "ssh -p 22 -c aes128-gcm@openssh.com" \
  /var/www/ root@203.0.113.20:/var/www/

-aHAX 里 H 是硬链接、A 是 ACL、X 是扩展属性。个人站点如果用了 setfacl 给运维账号授权(比如让非 root 账号只能读日志),漏掉 A 会导致切换后权限全不对。上传目录如果有很多小文件,第一遍 rsync 可能很慢,可以加 --exclude 排除缓存和日志目录,切换后再单独处理。

全量传完之后,到真正切换前的那一刻,再做一次增量同步。第二次 rsync 只传差异,几秒钟就完。同时把数据库的增量补上——最简单的办法是在切换瞬间重新 dump 一次数据库,因为个人站点数据量通常不大(几百 MB 以内),压缩传输几分钟就能完成,比搭主从复制要省事得多。

如果数据量大到无法在切换窗口内重新 dump,才值得考虑主从:新机器作为从库连旧机器,同步追上之后停写、确认 Seconds_Behind_Master 为 0、提升为主库。但这条路会引入 binlog 格式、server-id、GTID 等一系列配置,对一次性的迁移来说是过度设计。判断标准很简单:如果一次 dump + 导入能在 10 分钟内完成,就不要搞主从。

阶段三:新机器上线前的预检清单

数据和代码都到位了,但这时候新机器还不能对外服务。用一个只监听本机的方式把它跑起来,用 hosts 绑定来测:

# 在本机用 curl 直接指定 Host 和 IP,绕过 DNS
curl -s -o /dev/null -w "%{http_code} %{time_total}\n" \
  --resolve www.example.com:443:203.0.113.20 \
  https://www.example.com/

--resolve 这个参数是换服务器时最实用的工具,它让你在不改 DNS、不改 hosts 的情况下,把请求打到你指定 IP 的证书上。测一遍下面这些:

# 1. 首页、一个文章页、一个搜索页、一个 404
for u in / /1234.html /?s=test /nonexistent-page; do
  echo -n "$u  "
  curl -s -o /dev/null -w "%{http_code} %{time_total}s\n" \
    --resolve www.example.com:443:203.0.113.20 "https://www.example.com$u"
done

# 2. 证书链是否完整(缺中间证书会让部分安卓机报错)
openssl s_client -connect 203.0.113.20:443 -servername www.example.com \
  < /dev/null 2>&1 | grep -E "Verify return code|s:"

# 3. HTTP 跳 HTTPS、www 与非 www 的规范化是否一致
curl -sI --resolve www.example.com:80:203.0.113.20 http://www.example.com/ | head -3

然后是最容易被忘的一步:把新机器的出站行为也对齐。检查邮件发送(mailq 是否堆积、SPF/DKIM 是否绑定了新 IP——很多站点的 SPF 记录里写死了 IP,换机器后邮件直接进垃圾箱)、外部 API 的白名单(支付回调、第三方接口往往按 IP 白名单授权)、以及备份任务的目标地址。

# SPF 记录里如果写了 ip4:,换 IP 后必须同步改 TXT 记录
dig +short TXT example.com | grep spf

# 出站邮件测试
echo "test from new server $(date)" | mail -s "mail test" you@example.com
tail -20 /var/log/mail.log

阶段四:切换,二十分钟内完成

到了这一步,域名解析是最容易的一步,难的是"切换瞬间的写入怎么处理"。有三种策略,按复杂度递增:

策略 A(推荐给个人站点):短暂维护窗口 + 停写。 提前在页面挂一个 5 分钟后的维护公告,到点后旧机器 Nginx 直接返回 503 维护页(保留静态资源,让监控能看到明确状态),然后做最后一次数据库 dump、增量 rsync、导入新库、改 DNS、新机解除维护。整个窗口控制在 10 到 20 分钟。对博客和论坛来说,深夜做一次 15 分钟维护,比冒数据不一致的风险划算得多。

# 旧机器:进入维护模式
cat > /etc/nginx/conf.d/maintenance.conf <<'EOF'
location / {
    return 503;
}
error_page 503 /maintenance.html;
location = /maintenance.html { root /var/www/static; internal; }
nginx -t && systemctl reload nginx

策略 B:双写。 切换期间让应用同时写旧库和新库。听起来美好,实际很难做对:主键冲突、自增 ID 不同步、事务无法跨库、失败回滚怎么办,每一个都是坑。除非你的应用已经抽象了数据层,否则不要为了一次迁移引入双写。

策略 C:只读窗口。 切换前把旧站点设为只读(论坛关掉发帖、评论关闭),用户可以继续浏览,等新机接管后再开放写。这适合内容型站点,体验比 503 好,实现也简单——比双写安全得多。

改 DNS 的时候,如果是 A 记录直接改值,客户端只会看到"旧 IP 或新 IP",这是最安全的。如果你用 CNAME 指向一个中间名字,那就可以玩灰度:把中间那条记录拆成两条按权重分配(DNS 加权轮询),先 10% 流量进新机,观察一小时,再 50%,再全量。这需要你的 DNS 服务商支持权重记录(Cloudflare 的 Load Balancer、DNSPod 的线路分流都可以)。没有这个能力也不用勉强,直接切更简单,反正 TTL 只有 60 秒。

阶段五:切换后 48 小时,旧机器不能关

改完 A 记录不等于切换完成。这时候必须做三件事:

1)验证新 IP 已经生效。 用多个公共解析器确认,而不是只在自己的电脑上刷新:

for r in 8.8.8.8 1.1.1.1 223.5.5.5 114.114.114.114; do
  echo -n "$r  "; dig @$r www.example.com A +short | tr '\n' ' '; echo
done

2)盯 48 小时的错误日志和访问日志。 重点看新增的 5xx、上传失败、登录失败(session 目录如果没同步会导致所有人被登出——这个现象很常见,用户反馈"换服务器之后要重新登录")。同时对比两边的访问量曲线,如果新机流量只到旧机的 70% 就停了,说明有一部分 DNS 还在指向旧 IP(通常是运营商缓存),继续等。

# 新机上的 5xx 统计(按小时)
awk '{print $4, $9}' /var/log/nginx/access.log \
  | grep ' 5' | cut -c2-15 | sort | uniq -c | sort -rn | head -20

# 旧机还有多少流量(判断缓存是否清空)
tail -1000 /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head

3)旧机器保持原样,直到确认无误。 不要急着卸载、不要格式化、不要把旧机上的 cron 关掉(如果有些任务只在旧机上跑,你会同时失去它和报警)。48 小时内旧机只做一件事:如果发现严重问题,把 A 记录改回去,60 秒后流量就回来了。这才是"可回滚"的真正含义。

回滚预案要提前写清楚,而不是出事时临时想:

## 回滚检查单(切换后 48 小时内有效)
触发条件:新机 5xx 比例 > 1%、下单/发帖持续失败、数据库报错无法定位
步骤:
1. 把 A 记录改回 203.0.113.10(原 IP),TTL 保持 60
2. 确认 dig @8.8.8.8 已返回旧 IP
3. 旧机若在维护模式:删除 maintenance.conf,nginx -t && reload
4. 检查 48 小时内在新机上产生的数据:
   - 新帖、新评论、新注册用户(按 created_at > 切换时间 查)
   - 上传的文件(find /var/www/uploads -newer /tmp/cutover_marker)
5. 把上述数据补回旧库(导出 INSERT 语句,注意主键冲突用 ON DUPLICATE KEY UPDATE)
6. 确认旧机功能正常后再考虑第二次切换

第 4、5 步是回滚真正的难点。切回去之后,新机上那段时间产生的数据不会自动回到旧库,忘了这一步就会丢评论、丢用户。所以切换时先在新机上打一个时间戳文件 touch /tmp/cutover_marker,回滚时用它精确定位这段时间的增量数据。这个习惯看起来很小,出事后能救命。

最后:这件事真正的成本在哪

整篇文章的流程里,搬文件、导数据库、改 DNS 加起来不到两小时,真正耗时的是准备工作:对齐环境、检查 SPF、预检证书、写回滚清单。经验上来说,准备时间和执行时间的比例大概是 10:1,而所有的线上事故都出在那 10 里面没做到的地方。

所以给一个务实的建议:如果你的站点只是个人博客、每天几百访问、没有用户注册和评论,那就不必搞这么复杂,挂个维护页十分钟搬完就行。但只要有用户数据、有写入、有邮件通知,这六步一步都不能省——尤其是把 TTL 提前降下来,以及把旧机器留够 48 小时。这两个动作几乎零成本,却把"一次搬家的风险"从"可能丢数据"降到了"大不了切回去"。

另外提醒一句:换服务器之后,Google 和百度会重新评估你的站点。IP 变更本身不影响收录,但如果新机器的响应变慢、或者切换期间长时间 503,抓取频率会下降。所以切换后一周内盯一下 Search Console 的抓取统计和服务器错误报告,有问题早发现。这件事我在讲收录的文章里写过,换 IP 是最容易触发"抓取错误突然上升"的场景之一。

Last modification:October 20th, 2026 at 08:24 pm

Leave a Comment