Linux域名解析链路排查:从hosts到递归服务器

很多人对 DNS 的印象停留在「改个 A 记录,等十分钟生效」。直到某天你在服务器上敲 ping www.example.com,返回 Temporary failure in name resolution,而 ping 1.1.1.1 一切正常——这才意识到,DNS 是一条比 HTTP 更脆弱、也更难排查的链路:它同时在 UDP、TCP、TLS 之间跳,还要经过本地缓存、系统解析器、递归服务器、根/顶级/权威四级,任何一层出问题,表现都是一样的「解析不了」。

本文按一条真实解析请求的路径,把 Linux 上的 DNS 解析逐层拆开:每一层在做什么、出问题时的典型症状、以及用什么命令能在三十秒内定位到是哪一层。

一、先分清「谁在解析」:三个完全不同的东西

排查 DNS 之前必须搞清一个前提:Linux 上的「域名解析」并不是一个程序在做,而是至少三套机制在叠加。

  • glibc 的 NSS 链路:几乎所有程序(curl、PHP、nginx、ssh)最终调的都是 getaddrinfo(),它由 /etc/nsswitch.conf 决定顺序,常见的是 hosts: files dns——先查 /etc/hosts,再查 DNS。
  • 本地解析器缓存:systemd-resolved、dnsmasq、nscd 这三者都可能插在中间缓存结果。它们是不是存在,直接决定「改了解析为什么没生效」。
  • 递归解析服务器:/etc/resolv.conf 里 nameserver 指向的那台。它可能是运营商/公共 DNS,也可能是你自建的。

这三者中任何一个配置不一致,就会出现「dig 能解析、curl 不能解析」这种让人抓狂的现象。原因通常是 dig 直接读 resolv.conf 并自己发包,**完全绕过 nsswitch 和 getaddrinfo**;而 curl、PHP 走的是 glibc 那条路。所以第一个动作永远是:

cat /etc/resolv.conf
cat /etc/nsswitch.conf | grep hosts
systemctl status systemd-resolved 2>/dev/null | head -5
ps aux | grep -E '[d]nsmasq|[n]scd'

/etc/resolv.conf 顶部如果写着 # This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8),说明这个文件是个软链接,你手改它会在重启后被覆盖。真正的配置在 /etc/systemd/resolved.conf。

二、第一层:glibc 与 /etc/hosts 的「隐形优先」

这一层的坑非常经典。/etc/hosts 的优先级高于 DNS,而且它不会过期。曾经有人在测试时往 hosts 里加了一行 10.0.0.5 api.example.com 做联调,半年后那台机器下线了,正式环境开始偶发超时——查了半天 DNS,最后发现是那行没人记得的 hosts 记录。

# 确认到底是哪条记录在生效
getent hosts api.example.com

# 对比:绕过 nsswitch 直接问 DNS
dig +short api.example.com

getent hosts 走的是完整的 NSS 链路,**如果它和 dig 的结果不一致,问题一定在 /etc/hosts 或 NSS 层**,不用再往下查了。这个命令值得养成习惯,比 dig 更贴近应用的真实行为。

还有一种情况是 NSS 里配了 mdns 或 wins,解析一个普通公网域名时先发一轮 mDNS 广播,超时几百毫秒后才落到 DNS。表现是「解析很慢但能解析」。用 strace -f -e trace=connect curl -sI https://example.com 能看到它在往 224.0.0.251:5353 发包。

三、第二层:options timeout 与「两个 nameserver」的陷阱

/etc/resolv.conf 里最常见的写法是:

nameserver 8.8.8.8
nameserver 1.1.1.1
options timeout:2 attempts:2

很多人以为这两个 nameserver 是「负载均衡」,实际上 glibc 的行为是**严格按顺序尝试、只有第一台超时或拒绝才用第二台**。这意味着:如果第一台是黑洞(不响应但也不拒绝,比如防火墙 DROP),那么每一次新解析都要先等满 timeout × attempts,默认是 5 × 2 = 10 秒,然后才轮到第二台。

这是「网站总是莫名慢 5 秒或 10 秒」的经典成因之一。判断方法很直接:

# 分别直连两个 nameserver 测响应时间
dig @8.8.8.8 www.example.com +time=2 +tries=1 | grep 'Query time'
dig @1.1.1.1 www.example.com +time=2 +tries=1 | grep 'Query time'

# 如果不响应,用 nc 确认是丢包还是拒绝
nc -zuv 8.8.8.8 53

注意 nc -zuv 对 UDP 的结果不可靠(UDP 没有握手,永远「成功」),只能作为参考。真正可靠的判据是 dig +time=2 +tries=1 是否在 2 秒内返回——超时就说明那一台废了。

建议把 timeout 显式调小,比如 options timeout:1 attempts:2。默认 5 秒的等待在 web 场景里完全无法接受。另一个建议是:**多个 nameserver 只在你能确定它们都健康时才配**,配一个健康的第一台 + 一个已知可用的第二台,比堆四个不确定的强得多。

四、第三层:递归解析与 TTL 缓存,为什么「改了半天还没生效」

权威记录改完之后,从你改的这一刻到全世界都能看到,中间隔着两级缓存:递归服务器的缓存和客户端的缓存。生效时间取决于改之前的 TTL,而不是改之后。

# 看权威服务器上的真实记录(绕过所有缓存)
dig @ns1.your-dns-provider.com www.example.com +norecurse

# 看公共 DNS 的缓存状态
dig @8.8.8.8 www.example.com

# 看当前 TTL 还剩多少
dig www.example.com | grep -E '^www|IN\s+A'

第三个命令输出里的数字(比如 300)就是剩余 TTL 秒数。如果它一直在 300 左右不动,说明你查到的是缓存;如果从 300 递减到 0 然后变回 300,说明缓存刚过期重取了一次。

更麻烦的是负缓存。NXDOMAIN 也会被缓存,时长由权威记录的 SOA 最小 TTL 决定。如果你的 SOA 里 minimum 写的是 86400,那么一条「还没建好」的子域名在全世界会被记住 24 小时的「不存在」。这就是为什么加新子域名时,**先建记录再发链接**,顺序反了会给自己制造一天的黑洞。

dig example.com SOA +short
# 输出格式:ns1.example.com. admin.example.com. 2026103101 7200 3600 1209600 86400
#                                                        ^serial ^refresh ^retry ^expire ^minimum

五、UDP 与 TCP:被截断的响应和「大包」问题

DNS 查询默认走 UDP 53 端口,但 UDP 响应有个历史限制:超过 512 字节时,服务器会设置 TC(truncated)标志,客户端需要改用 TCP 重查。EDNS0 把这个上限提到了 4096 字节,但**中间设备(防火墙、NAT、云安全组)经常不认这个约定**,直接丢弃大包。

症状很有辨识度:小域名能解析,大一点的(很多记录、DNSSEC 签名、或返回多个 IP)就超时。用 dig +bufsize=512 和 +tcp 做对比测试:

dig www.example.com +bufsize=512 | grep -E 'flags|Truncated'
dig www.example.com +tcp | grep -E 'status|Query time'

如果 TCP 成功而 UDP 超时,基本可以确定是中间网络在丢大 UDP 包。云服务器上最常见的成因是安全组只放行了 UDP 53 入站,没有放行 TCP 53——因为出站方向的响应不需要放行,但「UDP 被截断后改走 TCP」的这一步需要出站 TCP 53 可达。自建递归解析器时,务必 UDP/TCP 都放行。

六、systemd-resolved:现代发行版的「中间人」

Debian 12、Ubuntu 22.04 之后,systemd-resolved 默认开启(虽然不一定启用)。它监听 127.0.0.53:53,resolv.conf 指向它,它再向上游转发。这带来三个新问题:

  • 它有自己的缓存。systemd-resolve --flush-caches(新版本是 resolvectl flush-caches)才是清缓存的正确命令,重启 nginx 或者改 resolv.conf 都没用。
  • 它支持 per-link 配置:多个网卡(比如 eth0 + docker0 + tailscale0)各自可以有独立的 DNS 和搜索域。用 resolvectl status 能看到每个 link 的实际配置,这比看 resolv.conf 准确得多。
  • 它默认不做 DNSSEC 验证,但会感知上游。如果上游服务器的响应格式不规范(比如缺少额外的 A 记录追加),resolved 会直接判为无效并丢弃,表现是 dig 能查、应用不能查。
resolvectl status
resolvectl query www.example.com
resolvectl statistics | head -20

resolvectl query 的输出比 dig 更接近应用视角,因为它走的是 resolved 的完整链路。如果 resolvectl query 正常而应用异常,那问题一定在 glibc/NSS/hosts 这一侧,方向立刻清晰了。

如果你确实不需要 systemd-resolved(比如你自己跑了 dnsmasq 或 CoreDNS 在 127.0.0.1:53),正确做法是 systemctl disable --now systemd-resolved,然后把 /etc/resolv.conf 从软链接换回普通文件。**不要在 resolved 运行时硬改 resolv.conf**,它会在网络变化时把文件重写回去。

七、三十秒定位法:按顺序问四个问题

把上面的内容压缩成一个可执行的手册。DNS 出问题时,按顺序问这四个问题,每一步都能把范围砍掉一半:

问题一:是全部域名不能解析,还是只有某个域名?执行 getent hosts www.baidu.com 和 getent hosts 1.1.1.1。全都不行 → 网络或 resolv.conf 的问题,跳到问题二;只有特定域名不行 → 权威记录或负缓存的问题,直接去查 SOA 和 TTL。

问题二:换一个 DNS 服务器能解析吗?执行 dig @8.8.8.8 www.example.com +time=3。能解析 → 你本机配置的 nameserver 有问题,检查 resolv.conf 和 resolved status;不能解析 → 是出站 53 端口被封或上游网络问题,用 dig @8.8.8.8 +tcp 再试一次区分 UDP/TCP。

问题三:getent 和 dig 结果一致吗?不一致 → 一定在 /etc/hosts 或 NSS 层,用 grep -n example.com /etc/hosts 一条就够。一致但应用仍然解析错 → 怀疑应用自己缓存了(PHP 的持久连接、Java 的 JVM DNS 缓存默认永久、Go 的纯 Go 解析器不走 glibc)。

问题四:慢还是不通?如果是慢,看三个数:dig 的 Query time、resolv.conf 的 timeout 设置、以及 nameserver 的数量。十有八九是第一个 nameserver 是黑洞。

八、小结:DNS 问题的判据是「层次」,不是「命令」

DNS 排查之所以让人痛苦,是因为它的失败模式高度相似:不管哪一层坏了,用户看到的都是「解析不了」。真正的解法不是记住更多命令,而是记住这条链路的层次结构——应用 → glibc/NSS/hosts → 本地缓存(resolved/dnsmasq/nscd)→ 递归服务器 → 权威服务器。

每一层都有一个「能跳过下层直接验证上层」的命令:getent hosts 验证到 NSS 为止,resolvectl query 验证到本地缓存为止,dig 直接验证递归服务器,dig @权威NS +norecurse 直接验证权威数据。**先确认哪一层是对的,再往下查**,比盲目重启服务快十倍。

最后一条经验:给站点做任何 DNS 变更(换 IP、加子域名、改 TTL)之前,先在 /etc/hosts 里做本地预演,确认新服务器真的能服务这个域名;变更之后,用 dig @8.8.8.8 和 dig @1.1.1.1 两个公共 DNS 分别验证,别只用一台。DNS 是唯一一个「你自己看是对的、别人看是错的」的环节。

Last modification:October 31st, 2026 at 08:30 pm

Leave a Comment