适用场景

线上服务切换负载均衡、迁移 API 网关或故障切流后,权威 DNS 已显示新 IP,但仍有一部分客户端持续访问旧地址。常见表现包括:请求间歇超时、少量用户仍命中已下线机房、发布后错误率在一个较长窗口内缓慢回落。

本文以 api.example.com203.0.113.10 切换到 203.0.113.20 为例,说明如何区分权威记录、递归解析器、本机缓存、应用连接池四层状态,并给出可回滚的切换流程。

现象与风险

运维同学通常先执行:

dig api.example.com A +short

若此命令返回新地址,很容易误以为 DNS 已完全生效。实际上,它默认经过本机配置的递归解析器;不同网络、不同运营商和不同进程可能仍持有旧值。更危险的是,旧 IP 在切换后立即回收,缓存尚未过期的客户端就会连接到无关服务。

排查时先明确两件事:

  1. 权威服务器现在发布什么记录;
  2. 失败请求实际连到了哪个 IP,以及该值来自哪一层缓存。

先确认权威记录与委派链路

不要只看默认 dig 输出。先找到域名的权威 NS,再直接向其中一台查询:

dig example.com NS +short
dig @ns1.dns-provider.example api.example.com A +norecurse
dig @ns1.dns-provider.example api.example.com SOA +norecurse

+norecurse 要求服务器不要代为递归查询。A 记录的 ANSWER SECTION 应是新地址;记录末尾的 TTL 是递归缓存最多可继续保存该答案的秒数。SOA 中的 serial 应随本次变更递增;若 serial 未变,往往是变更提交到错误 zone、CDN DNS 控制面尚未同步,或查看的是从节点旧数据。

还应检查是否存在 CNAME 链:

dig @ns1.dns-provider.example api.example.com CNAME +noall +answer
dig @ns1.dns-provider.example api-origin.example.net A +noall +answer

最终连接地址的有效期受整条链中 TTL 影响。只降低最外层 CNAME 的 TTL,不能让下游 A 记录缓存立即失效。

按层定位旧地址从哪里来

1. 比较多个递归解析器

选择本公司 DNS、公共 DNS 和故障客户端所在网络的 DNS 分别查询:

for resolver in 10.0.0.53 1.1.1.1 8.8.8.8; do
  echo "== $resolver =="
  dig @"$resolver" api.example.com A +noall +answer
done

如果某个解析器答复旧 IP,且 TTL 小于变更前 TTL,说明它正在自然过期;不要反复刷新查询来替代等待。若 TTL 每次都回到完整值,则可能有上游仍在发布旧记录,需沿该解析器的转发链继续检查。

2. 检查 Linux 本机缓存与 hosts 覆盖

/etc/hosts 的优先级通常高于 DNS。使用下面命令查看系统解析结果和覆盖项:

getent ahostsv4 api.example.com
grep -nE '(^|[[:space:]])api\.example\.com([[:space:]]|$)' /etc/hosts
resolvectl query api.example.com 2>/dev/null || true

getent 走的是 Name Service Switch,最接近大部分进程的实际解析路径。若 dig 是新 IP 而 getent 是旧 IP,优先检查 hosts、nscdsystemd-resolved 或企业安全客户端注入的 DNS 策略。

在确认问题仅存在于本机缓存后,才执行对应的刷新动作:

sudo resolvectl flush-caches
sudo systemctl restart nscd  # 仅在 nscd 已部署时执行

不要把重启 DNS 服务当作全网修复手段;它只影响当前节点。

3. 识别应用进程自己的缓存和长连接

即使 DNS 已更新,进程也未必立刻重新解析。Java 的 networkaddress.cache.ttl、Go 的自定义 resolver、服务网格的 DNS 代理,以及 HTTP 客户端的 keep-alive 连接都会延迟切换。

在故障 Pod 或主机上同时记录解析和真实建连目标:

getent hosts api.example.com
curl -sv --connect-timeout 3 https://api.example.com/healthz -o /dev/null
ss -ntp | grep ':443'

curl -v 中的 Trying x.x.x.x 是本次连接使用的地址;ss 可发现进程仍复用到旧后端的已建立连接。若仅旧连接失败,应缩短客户端空闲连接时间、主动 drain 连接或滚动重启工作负载,而不是盲目改 DNS。

一次可观测的定位示例

某次切换后,权威服务器返回 203.0.113.20,公司 DNS 也返回新地址,但两台应用主机的 getent 仍为 203.0.113.10。检查发现 /etc/hosts 中遗留了一条应急映射。移除该行并刷新 systemd-resolved 后,新建连接恢复正常。

为了避免凭感觉判断,应在切换窗口记录下列字段:

时间、探针节点、查询服务器、A 记录、TTL、HTTP 建连 IP、状态码、错误类型

将这些字段写入探针日志或监控标签,可以快速判断问题是 DNS 传播、节点配置,还是后端服务自身失败。

安全切换方案

DNS 不适合做秒级、无损的强制流量迁移。建议按以下顺序操作:

  1. 提前至少一个旧 TTL 周期降低 TTL,并确认权威和主要递归解析器均已显示较小 TTL;
  2. 新旧后端同时提供服务,新后端先通过独立域名或探针验证健康;
  3. 修改记录后保留旧后端的监听和证书,覆盖一个“旧 TTL + 最大客户端缓存 TTL + 连接空闲超时”的窗口;
  4. 分地域、分运营商检查解析结果和实际 HTTP 建连地址;
  5. 指标稳定后再下线旧地址,并把 TTL 恢复到正常值。

若使用四层/七层负载均衡,优先在负载均衡层做 drain 和权重切换;DNS 只负责将客户端引导到稳定入口。这样即使少量客户端保留旧解析结果,也仍能由旧入口转发或返回可控响应。

预防措施

  • 建立权威 DNS、多个递归 DNS、应用侧 getent 和真实 HTTP 建连的四级合成探针。
  • 将 hosts 临时映射纳入配置管理,并设置到期检查,禁止手工遗留。
  • 在发布手册中写明 TTL 降低和恢复的时间点,禁止“改记录后立即下线旧机”。
  • 为客户端设置合理的 DNS 缓存上限和连接空闲超时;长连接协议要支持连接排空。
  • 将 DNS 变更、后端扩缩容和证书更新纳入同一回滚预案,确保旧入口在窗口内可恢复。

总结

DNS 变更后仍访问旧地址,并不必然是 DNS 服务商“没生效”。先用直接查询确认权威答案,再逐层检查递归缓存、本机覆盖和应用连接复用,最后通过保留旧入口与分层探针完成平滑切换。把 DNS 当作最终一致性的流量引导机制,才能避免一次记录变更演变为线上故障。