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 故障都在教同样的经验,而我们总是一次次重新学习。
- 监控 DNS 解析,而不只是 DNS 服务器。 DNS 服务器健康,并不代表解析就能正常工作。要监控完整路径:你的应用真的能解析它所需要的主机名吗?每 30 秒解析一次关键主机名的合成检查,比单纯的服务器监控能更快发现问题。
- 为内部 DNS 设置较短的 TTL。 当你需要修改内部 DNS 记录时,较长的 TTL 意味着缓存里是过期数据。内部服务使用 60 秒的 TTL,可以在几乎不增加解析器负载的前提下实现快速故障切换。
- 在操作系统更新后测试你的 DNS 配置。
.internal的故障只影响了 macOS 用户。每次大版本系统更新后都要测试你的开发环境——DNS 解析的变化很少出现在发布说明里。 - 记录你的 DNS 架构。 哪些 DNS 服务器负责哪些域名?拆分点在哪里?VPN 断开时会发生什么?这份文档是故障发生时你最先需要的东西,也是最没人愿意写的东西。
- 准备好备用方案。 如果内部服务的 DNS 解析失败,你的应用能否改用硬编码的 IP?这做法不优雅,但一个能用的硬编码 IP,总比一个解析不出来的域名强。
DNS 是那种基础服务:一切正常时完全隐形,一旦出问题就是灾难性的。.internal 的故障提醒我们,DNS 配置比它看上去脆弱得多——一次操作系统更新就可能改变数百万用户的解析行为。构建系统时就假设 DNS 会出问题,这样当它真的出问题时,你就不会那么措手不及了。


