深度解析塑造未来的技术文章。

DNS 配置仍在让生产环境出问题

DNS 看似简单,一出问题就麻烦。解析 OS 更新、resolv.conf、mDNS 和 .internal 顶级域如何引发开发者意想不到的生产事故。

信件被错误地分送到气动管道的分拣室

苹果最新的 macOS 版本让数千名开发者的自定义 DNS 设置失效了。具体来说,它开始拦截 .internal 顶级域的查询——很多公司用这个域名来标识内网资源——并把这些请求转给 mDNS(组播 DNS),而不是交给已配置的 DNS 解析器。原本用了多年好好的服务突然连不上了。问题的修复并不直观,错误信息也没什么帮助。而且和大多数 DNS 问题一样,它的症状看起来像是除了 DNS 以外的任何东西。

这是一个关于 DNS 配置的故事。这个话题每个开发者都会遇到,真正理解的人不多,而在凌晨两点把生产环境搞挂之前,大家通常都低估了它。

.internal 顶级域事件

很多组织把 .internal 当作内部服务的私有顶级域来用,比如 api.internal、db.staging.internal、grafana.internal。这看起来是个安全的选择:它不是公共顶级域,一看就知道是“内部的”,而且这么用已经好几十年了。

然后苹果的新系统决定把 .internal 交给 mDNS 处理——也就是那个在家庭网络里解析 myprinter.local 的协议。当你的 Mac 尝试解析 api.internal 时,它不会去问配置好的 DNS 服务器,而是在本地网络上广播一个 mDNS 查询。你的 DNS 服务器根本看不到这个查询。结果要么是没有回应(超时),要么是拿到错误的地址(如果本地网络里恰好有某台机器响应了这个名字)。

症状是这样的:curl api.internal 卡住 5 秒然后失败;SSH 连接内部主机超时;VPN 用户访问不了内网资源;Docker 容器无法解析服务名。由于失败表现为一个笼统的连接超时,大多数开发者会去排查 VPN、防火墙、应用程序——唯独不会先怀疑 DNS。

现代系统上 DNS 解析实际是怎么工作的

“DNS 很简单”这种心智模型——电脑向 DNS 服务器请求 IP,服务器返回结果——已经好多年不准确了。现代系统的解析链路包含多个阶段,每个阶段都可能拦截、修改或重定向查询。

DNS resolution pipeline on a typical macOS/Linux system:
Application calls getaddrinfo("api.internal")
↓
1. /etc/hosts — checked first. Static overrides.
↓
2. NSSwitch / resolver configuration
- Linux: /etc/nsswitch.conf controls lookup order
- macOS: /etc/resolver/* for per-domain DNS servers
↓
3. System resolver (systemd-resolved, mDNSResponder)
- May intercept .local, .internal, or other special TLDs
- May apply DNSSEC validation
- May cache responses
↓
4. DNS server (from DHCP, VPN, or manual config)
- Queries sent to configured upstream resolver
- May go through a corporate DNS proxy
↓
5. Recursive resolver (8.8.8.8, 1.1.1.1, corporate)
- Walks the DNS hierarchy: root → TLD → authoritative
↓
Response flows back up the chain

每个阶段都可能出问题。系统解析器可能在查询到达你配置的 DNS 服务器之前就把它截走;VPN 可能覆盖 DNS 设置;DHCP 下发的 DNS 服务器可能优先于你手动的配置;而每一层的缓存意味着修复可能不会立即生效。

关于 /etc/resolv.conf 的谎言

在 Linux 上,/etc/resolv.conf 过去一直是 DNS 配置唯一的权威来源。在那里设置 nameserver,系统就用它,简单明了。

现代 Linux 发行版让这件事复杂了很多。如果你用的是 systemd-resolved(Ubuntu、Fedora 以及大多数基于 systemd 的发行版默认都用它),那么 /etc/resolv.conf 只是一个符号链接,指向 127.0.0.53 上的存根解析器。真正的 DNS 配置保存在 systemd-resolved 的状态里,通过 resolvectl 管理。直接编辑 /etc/resolv.conf,要么在下次网络变化时被覆盖,要么把存根解析器搞坏。

# What you THINK your DNS config is:
$ cat /etc/resolv.conf
nameserver 127.0.0.53  # This is systemd-resolved's stub
# What your DNS config ACTUALLY is:
$ resolvectl status
Global:
DNS Servers: 8.8.8.8 8.8.4.4
DNS Domain: ~.
Link 2 (eth0):
DNS Servers: 10.0.0.1  # From DHCP — this takes precedence
DNS Domain: corp.internal
Link 5 (wg0):  # WireGuard VPN
DNS Servers: 10.100.0.1
DNS Domain: ~internal  # VPN claims the .internal domain
# Three different DNS servers, each handling different domains.
# /etc/resolv.conf shows none of this.

Docker 又增加了一层。Docker 容器有自己的 /etc/resolv.conf,通常是容器启动时从宿主机复制过来的。如果宿主机的 DNS 发生变化(VPN 连上、切换网络),正在运行的容器会一直沿用旧的 DNS 配置,直到重启。这是“重启后正常,过一阵子又坏了”这类问题的常见根源。

Split DNS 与 VPN 冲突

Split DNS——对不同域名使用不同的 DNS 服务器——在企业环境里很常见。你的 VPN 把 *.corp.internal 的查询路由到公司的 DNS 服务器,而公网查询仍走你平时的解析器。配置正确时这套方案运转良好;配置不对时,它出问题的方式会非常让人摸不着头脑。

常见的故障模式:

  • VPN 把 DNS 服务器设为全局默认。 有些 VPN 客户端会把 VPN 的 DNS 服务器设为所有查询的默认解析器,而不只是内网查询。这样一来,你所有的 DNS 请求——包括个人上网——都会经过公司的 DNS 服务器。这既慢(公司 DNS 可能在另一个数据中心),也涉及隐私问题。
  • DNS 服务器顺序引发的冲突。 配置了多个 DNS 服务器时,系统会按顺序尝试。如果第一个服务器很慢或不可达(VPN 已断开,但 DNS 配置还留着),每次 DNS 查询都要等超时之后才会尝试第二个服务器,这会让每次连接都多出 5 秒的延迟。
  • 搜索域会悄悄追加。 如果搜索域设为 corp.internal,那么对 api 的查询也会尝试 api.corp.internal。这很方便(可以直接 ssh api,不用写 ssh api.corp.internal),直到某个短主机名和内网服务名撞车,查询就会解析到错误的 IP 上。
  • mDNS 拦截内部域名。 正如 .internal 事件所示,系统解析器可能会在某些顶级域的查询到达任何已配置的 DNS 服务器之前就将其拦截。这种情况特别阴险,因为拦截是悄无声息发生的——没有错误信息,只有超时或错误的响应。

DNS 排障:工具箱

DNS 出问题时,你需要能看清解析链路每个阶段实际发生了什么的工具。

# 1. Check what your system actually resolves:
$ dig api.internal
# Shows: query sent to which server, response received
# The 'SERVER' line tells you which DNS server answered
# 2. Query a specific DNS server directly:
$ dig @10.100.0.1 api.internal
# Bypasses the system resolver — goes directly to the specified server
# If this works but 'dig api.internal' doesn't, the problem is
# in the system resolver, not the DNS server
# 3. Check systemd-resolved state (Linux):
$ resolvectl query api.internal
# Shows which interface/server handled the query
# 4. Check macOS DNS routing:
$ scutil --dns
# Shows per-domain DNS configuration on macOS
# Look for your .internal domain — is it being routed correctly?
# 5. Monitor DNS queries in real time:
$ sudo tcpdump -i any port 53
# See actual DNS packets. If no packets go out for your query,
# the system resolver is intercepting it locally.
# 6. Flush DNS cache (when fixing config):
$ sudo dscacheutil -flushcache && sudo killall -HUP mDNSResponder  # macOS
$ resolvectl flush-caches  # Linux with systemd-resolved

最重要的诊断步骤是:用 dig @server hostname 直接查询你的 DNS 服务器。如果这样能通、但正常解析不通,问题就出在应用程序和 DNS 服务器之间——可能是系统解析器拦截、缓存或路由的问题。如果直接查询也失败,那问题就在 DNS 服务器本身,或者到它的网络连接上。

选择你的私有顶级域

.internal 事件引出了一个现实问题:内部服务应该用什么顶级域?没有放之四海而皆准的安全答案,但有些选项明显比其他的好。

  • .internal —— RFC 6762 为内部用途保留了这个域名,但苹果的 mDNS 处理方式让它在 macOS 上成了问题。在 Linux 上可能没问题。
  • .local —— 保留给 mDNS(RFC 6762)。如果用它来承载通过 DNS 解析的服务,会和 Bonjour/Avahi 冲突。不要用。
  • .corp、.home、.mail —— 并未保留。ICANN 随时可能把它们开放为公共顶级域,届时你的内部解析就会失效。不要用。
  • 你自己拥有的域名的子域。 比如 internal.yourcompany.com——这是最安全的选择。父域由你掌控,不存在冲突风险。缺点是输入起来比较长。
  • .test、.example、.invalid、.localhost —— 由 RFC 6761 保留,并保证永远不会被分配为公共顶级域。对于非生产环境的内部服务,.test 是最干净的选择。

那个朴素但正确的答案:使用你自己拥有的域名的子域。api.internal.yourcompany.com 没有歧义,不会和任何东西撞车,并且在所有操作系统和 DNS 解析器上都能正常工作。它确实更长,但凌晨两点的 DNS 故障,比多敲几个字符要糟糕得多。

生产系统的经验教训

每一次 DNS 故障都在教同样的经验,而我们总是一次次重新学习。

  1. 监控 DNS 解析,而不只是 DNS 服务器。 DNS 服务器健康,并不代表解析就能正常工作。要监控完整路径:你的应用真的能解析它所需要的主机名吗?每 30 秒解析一次关键主机名的合成检查,比单纯的服务器监控能更快发现问题。
  2. 为内部 DNS 设置较短的 TTL。 当你需要修改内部 DNS 记录时,较长的 TTL 意味着缓存里是过期数据。内部服务使用 60 秒的 TTL,可以在几乎不增加解析器负载的前提下实现快速故障切换。
  3. 在操作系统更新后测试你的 DNS 配置。 .internal 的故障只影响了 macOS 用户。每次大版本系统更新后都要测试你的开发环境——DNS 解析的变化很少出现在发布说明里。
  4. 记录你的 DNS 架构。 哪些 DNS 服务器负责哪些域名?拆分点在哪里?VPN 断开时会发生什么?这份文档是故障发生时你最先需要的东西,也是最没人愿意写的东西。
  5. 准备好备用方案。 如果内部服务的 DNS 解析失败,你的应用能否改用硬编码的 IP?这做法不优雅,但一个能用的硬编码 IP,总比一个解析不出来的域名强。

DNS 是那种基础服务:一切正常时完全隐形,一旦出问题就是灾难性的。.internal 的故障提醒我们,DNS 配置比它看上去脆弱得多——一次操作系统更新就可能改变数百万用户的解析行为。构建系统时就假设 DNS 会出问题,这样当它真的出问题时,你就不会那么措手不及了。