Linux 基础体系 · 第 65/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。

Linux DNS 解析链路:glibc、nsswitch、systemd-resolved、缓存和排障

DNS 故障经常被误判为“网络不通”。实际上,应用执行一次域名解析时,可能经过应用自身缓存、glibc、NSS 模块、/etc/hostssystemd-resolved、本机 DNS 缓存、上游递归 DNS,最后才到权威 DNS。任何一层都可能返回成功、失败、旧数据或与预期不同的数据。

要准确排障,首先要区分三个问题:

  1. 应用是否调用了系统名称解析接口?
  2. 系统名称解析链路选择了哪个 NSS 模块和 DNS 服务?
  3. DNS 查询是否成功,以及成功后到目标地址的路由和连接是否正常?

DNS 解析成功并不代表 TCP、TLS 或应用协议一定成功;反过来,TCP 连接失败也不一定是 DNS 问题。


一次域名解析到底发生了什么

以应用调用 POSIX 风格的 getaddrinfo("api.example.com", "443", ...) 为例,典型链路如下:

flowchart TD
    A[应用调用 getaddrinfo] --> B[应用自身缓存或连接池]
    B -->|未命中| C[glibc NSS 框架]
    C --> D[/etc/nsswitch.conf]
    D --> E[files: /etc/hosts]
    D --> F[resolve: systemd-resolved]
    D --> G[dns: libnss_dns]
    F --> H[systemd-resolved 缓存]
    F --> I[按接口和域名选择上游 DNS]
    G --> J[/etc/resolv.conf 中的 nameserver]
    I --> K[上游递归 DNS]
    J --> K
    K --> L[权威 DNS 或其缓存]
    E --> M[返回地址或错误]
    H --> M
    G --> M
    M --> N[应用建立 TCP/TLS/QUIC 连接]

这张图不是所有发行版的固定实现,而是现代 Linux 中最常见的几种组件关系。关键点是:glibc 本身主要负责名称服务接口和 NSS 调度,不等于它总是直接向 DNS 服务器发包。

例如:

  • hosts: files dns:先查 /etc/hosts,再由 libnss_dns.so.2/etc/resolv.conf 查询 DNS。
  • hosts: files resolve [!UNAVAIL=return] dns:先查 /etc/hosts,再通过 libnss_resolve 查询 systemd-resolved;只有该模块不可用时,才可能继续使用 dns
  • 应用自己实现 DoH:可能完全绕过 glibc、NSS 和 systemd-resolved
  • Java、Go、容器运行时和某些代理软件可能使用自己的解析策略,不能仅凭系统命令推断它们的行为。

glibc:系统名称解析接口,不是单一 DNS 客户端

getaddrinfo 返回的是什么

现代应用通常调用 getaddrinfo,而不是只查询 IPv4 的旧接口。它可以根据参数返回:

  • IPv4 地址,即 A 记录;
  • IPv6 地址,即 AAAA 记录;
  • 套接字类型和协议建议;
  • 地址族、端口和规范名称等信息。

例如,应用要连接 https://api.example.com,可能得到:

AF_INET6  2001:db8::10  port 443
AF_INET   192.0.2.10    port 443

应用随后如何选择地址,取决于应用和系统实现。许多程序会并行或交错尝试 IPv6 与 IPv4,这属于连接策略,不是 DNS 协议本身保证的顺序。

getaddrinfo 的失败也不只有一种含义:

  • EAI_NONAME:名称不存在、没有可用地址,或 NSS 把结果归类为名称不存在;
  • EAI_AGAIN:临时失败,例如超时或临时 DNS 错误;
  • EAI_SYSTEM:底层系统调用失败,此时还应检查 errno
  • 返回成功但地址为空或地址不可达:通常说明后续网络阶段失败,而不是解析阶段失败。

因此,程序日志中只写“getaddrinfo failed”是不够的,至少应记录错误码、查询名称、地址族和调用上下文。

glibc 通过 NSS 选择名称服务

NSS,即 Name Service Switch,是 glibc 用来选择不同名称数据库的机制。配置文件是:

/etc/nsswitch.conf

一个常见配置可能是:

passwd:         files systemd
group:          files systemd
shadow:         files

hosts:          files mdns4_minimal [NOTFOUND=return] dns

其中 hosts 行只影响主机名解析。常见模块含义如下:

模块 典型作用
files 读取 /etc/hosts
dns 使用 glibc 的 DNS NSS 模块,读取 /etc/resolv.conf
resolve 通过 systemd-resolved 的 NSS 模块查询
mdns4_minimal 查询 IPv4 链路本地多播 DNS,通常用于 .local
myhostname 由 systemd 处理本机主机名
systemd 由 systemd 管理的用户、组或主机名相关模块,具体用途依模块而定

nsswitch.conf 的顺序具有因果意义。例如:

hosts: files dns

解析 db.example.com 时,glibc 先查询 /etc/hosts。如果存在:

192.0.2.55 db.example.com

那么 DNS 可能根本不会被访问。

反过来,如果配置是:

hosts: dns files

则 DNS 返回的地址可能优先于本地静态映射。修改 /etc/hosts 后看到“配置明明改了但程序仍然访问旧地址”,第一项检查就应是 NSS 顺序和其他缓存,而不是立即重启网络服务。

NSS 返回状态与控制规则

NSS 模块不是简单返回“有”或“没有”。常见状态包括:

  • SUCCESS:成功找到结果;
  • NOTFOUND:没有找到该名称;
  • UNAVAIL:模块不可用,例如套接字不存在或共享库缺失;
  • TRYAGAIN:临时错误,例如超时。

配置中的控制项会改变后续模块是否继续执行。例如:

hosts: files mdns4_minimal [NOTFOUND=return] dns

[NOTFOUND=return] 表示当 mdns4_minimal 明确判定“名称不存在”时直接返回,不再执行后面的 dns。这通常用于避免 .local 名称继续泄漏到普通 DNS,但如果配置范围写错,也可能导致普通名称被提前截断。

诊断时不要只看 nsswitch.conf 是否“看起来合理”,还要确认模块文件实际存在:

ldconfig -p | grep -E 'libnss_(dns|resolve|mdns)'

常见库文件包括:

libnss_dns.so.2
libnss_resolve.so.2
libnss_mdns4_minimal.so.2

不同发行版的软件包名称不同,不能根据一个发行版的包名直接套用到另一个发行版。


/etc/resolv.conf:配置入口,不一定代表真正的上游 DNS

/etc/resolv.conf 通常包含:

search corp.example
options ndots:5 timeout:2 attempts:3
nameserver 192.0.2.53
nameserver 2001:db8::53

nameserver 的含义

nameserver 表示调用该解析路径的组件可以尝试访问的 DNS 服务器地址。它并不保证:

  • 该服务器一定可达;
  • 该服务器一定是当前接口获得的 DNS;
  • 所有应用都使用它;
  • systemd-resolved 一定会把它当作最终上游。

如果 /etc/resolv.conf 中是:

nameserver 127.0.0.53

这通常表示应用正在访问 systemd-resolved 的本地 stub 监听器,而不是把 DNS 查询直接发给互联网中的 127.0.0.53。如果 systemd-resolved 没有运行,该配置就会导致典型的“本机 DNS 端口无人监听”故障。

检查文件真实来源:

ls -l /etc/resolv.conf
cat /etc/resolv.conf

常见模式包括:

  1. 普通静态文件;
  2. 指向 systemd-resolved stub 文件的符号链接;
  3. 指向 systemd-resolved uplink 文件的符号链接;
  4. 由 NetworkManager、DHCP 客户端、云初始化工具或容器运行时生成。

不要在这些管理者仍然运行时直接手工覆盖文件。文件可能很快被恢复,或者重启后丢失;更严重的是,管理员以为修改已生效,但实际组件继续使用自己的内部状态。

searchdomainndots

给定短名称:

db

解析器可能把它扩展为:

db.corp.example
db.example
db

search 指定候选搜索域,ndots 指定名称中至少有多少个点时才被视为“足够像绝对名称”。实际候选顺序受解析器实现和配置影响,但 ndots 较大时,一个看似完整的名称可能先尝试搜索域扩展。

例如:

search corp.example
options ndots:5

查询:

api.example.com

因为名称只有两个点,可能先尝试:

api.example.com.corp.example

再尝试:

api.example.com

这会带来两个后果:

  • 增加解析延迟;
  • 把内部名称候选发送给不应接收它的 DNS 服务器。

如果名称明确是 FQDN,可以使用末尾点:

dig api.example.com.
getent hosts api.example.com.

末尾点表示 DNS 根下的绝对名称,减少搜索域扩展的歧义。但并非所有应用都允许用户输入带末尾点的 URL 主机名,因此不能简单要求所有业务都这样修改。

超时与重试

timeoutattempts 影响 glibc dns 模块在等待 DNS 响应时的行为。它们不是连接超时,也不是整个应用请求的总超时。多个 nameserver、IPv4/IPv6、查询类型和重试会共同影响最终等待时间。

因此,看到一个 DNS 请求“几十秒后才失败”时,不能只检查单个 DNS 服务器的 RTT,还要计算:

  • 尝试了多少个服务器;
  • 是否对 UDP、TCP 分别尝试;
  • 是否存在搜索域扩展;
  • 应用是否分别请求了 A 和 AAAA;
  • 上层是否又进行了重试。

systemd-resolved:本机解析管理器和缓存层

systemd-resolved 是一个独立的系统服务,不是 glibc 的一部分。它可以提供:

  • 本地 stub DNS 监听;
  • 每接口 DNS 配置;
  • 搜索域和路由域;
  • DNS 缓存;
  • UDP/TCP DNS 查询;
  • 某些场景下的 DNSSEC 验证;
  • 本机主机名和链路本地名称处理。

它不是所有发行版的默认方案。Ubuntu、Fedora、RHEL 系列的具体版本和安装方式可能不同;有些系统使用 NetworkManager 直接生成 resolv.conf,有些系统启用 systemd-resolved,容器中则常常只有一个由运行时注入的 resolv.conf

stub 模式和 uplink 模式

最容易理解的模式是:

应用
  |
  | DNS 请求到 127.0.0.53:53
  v
systemd-resolved
  |
  | 按接口、路由域和缓存决定
  v
真实 DNS 服务器,例如 192.0.2.53:53

此时 /etc/resolv.conf 常见内容是:

nameserver 127.0.0.53
options edns0 trust-ad
search corp.example

127.0.0.53 是 resolved 的 stub 地址。它只代表本机服务入口,不是上游 DNS。

另一个模式是让 /etc/resolv.conf 指向 resolved 生成的 uplink 配置,例如其中直接列出 DHCP 或静态配置得到的 DNS 服务器。这种情况下,使用 libnss_dns 的程序可能绕过 stub,直接向这些服务器发起查询;而使用 libnss_resolve 的程序则通过 resolved 的接口查询。两条路径可能具有不同缓存、搜索域和失败表现。

查看 resolved 状态:

systemctl is-active systemd-resolved
resolvectl status
resolvectl dns
resolvectl domain
resolvectl statistics

典型输出需要重点观察:

  • 哪些网络接口处于已配置状态;
  • 每个接口有哪些 DNS 服务器;
  • 哪些域名是搜索域;
  • 哪些域名是路由域;
  • 全局 DNS 是否存在;
  • 缓存命中和失败计数是否变化。

按接口和域名选择 DNS

现代主机可能同时存在:

  • 物理网卡;
  • Wi-Fi;
  • VPN;
  • Docker 或 Kubernetes 虚拟接口;
  • WireGuard;
  • 云厂商提供的专用网络接口。

systemd-resolved 可以根据接口关联的 DNS 和路由域选择查询路径。比如:

公司 VPN 接口:
DNS: 10.10.0.53
路由域: ~corp.example

公网接口:
DNS: 192.0.2.53
默认 DNS 路由

查询 git.corp.example 时,应优先走 VPN DNS;查询 www.example.com 时,可能走公网 DNS。

这里的 ~corp.example 是路由域语义,不是普通搜索域。普通搜索域负责补全短名称,路由域负责决定某个完整域名应该送往哪个链路。若多个路由域同时匹配,通常选择匹配范围更具体的域;配置冲突时应以 resolvectl status 和抓包结果为准,而不是凭接口名称猜测。

NSS 与 systemd-resolved 的两种接入方式

一种是 nss-resolve

hosts: files resolve [!UNAVAIL=return] dns

glibc 通过 libnss_resolve 与本机 resolved 通信。另一种是:

hosts: files dns

应用经过 libnss_dns,读取 /etc/resolv.conf。如果该文件指向 127.0.0.53,最终仍可能到达 resolved 的 stub,但调用路径不同。

这解释了一个常见现象:

resolvectl query api.example.com
getent hosts api.example.com
dig api.example.com

三个命令可能得到不同结果:

  • resolvectl query 直接询问 systemd-resolved;
  • getent hosts 走 glibc NSS;
  • dig 是独立 DNS 客户端,通常按 /etc/resolv.conf 发 DNS 查询,不使用 NSS 的 filesmdns 等模块。

DNS 缓存:没有一个“系统 DNS 缓存”

DNS 结果的有效时间由 TTL 控制,但实际系统中可能存在多层缓存:

浏览器或应用缓存
    ↓
连接池、代理或语言运行时缓存
    ↓
nscd、systemd-resolved 等本机缓存
    ↓
企业 DNS、路由器或云 DNS 缓存
    ↓
权威 DNS

TTL 的基本规则

对一个资源记录,缓存有效期可以粗略表示为:

Tremain=max(0,Tstored+TTLTnow)T_{\text{remain}} = \max(0, T_{\text{stored}} + TTL - T_{\text{now}})

其中:

  • TstoredT_{\text{stored}} 是缓存记录被保存的时间;
  • TTL 是权威 DNS 返回的生存时间;
  • TnowT_{\text{now}} 是当前时间;
  • TremainT_{\text{remain}} 是缓存还能使用多久。

递归 DNS 返回给客户端的 TTL 通常会随着缓存时间减少。假设权威服务器返回:

api.example.com. 60 IN A 192.0.2.10

递归 DNS 缓存 20 秒后再回答客户端,客户端可能看到 TTL 接近 40,而不是 60。

但实际行为还受到以下因素影响:

  • 负缓存 TTL;
  • 本地缓存实现的上限或下限;
  • DNSSEC 验证失败缓存;
  • CNAME 链上不同记录的 TTL;
  • 应用自己设置的缓存策略;
  • 服务重启导致缓存丢失;
  • 多个解析器使用不同的上游。

正缓存与负缓存

正缓存保存地址结果,例如:

api.example.com -> 192.0.2.10

负缓存保存“名称不存在”或“该类型不存在”的结果。负缓存时间通常由 SOA 记录相关字段决定,而不是简单等于某条 A 记录的 TTL。

这会造成一个容易误判的故障:

  1. 管理员查询到 NXDOMAIN
  2. 修正了权威 DNS;
  3. 权威服务器已经返回新地址;
  4. 客户端仍然得到 NXDOMAIN

原因可能是递归 DNS 或本机仍持有负缓存。此时“清空客户端缓存”不一定立即解决,因为中间递归 DNS 仍可能缓存旧的否定结果。

查看和清理 systemd-resolved 缓存:

resolvectl statistics
sudo resolvectl flush-caches
resolvectl statistics

flush-caches 只影响当前 resolved 实例,不会清理浏览器、应用、nscd、上游递归 DNS 或其他主机的缓存。生产环境执行前应确认服务影响;清空缓存会导致后续查询瞬时增加,并不能修复错误的权威记录或路由配置。


DNS 查询与 TCP/TLS 连接是两个阶段

完整链路可以写成:

名称解析
  -> 得到 IP 地址集合
  -> 根据路由表选择出口
  -> ARP/邻居解析
  -> 建立 TCP 或 QUIC
  -> TLS 握手
  -> 应用协议请求

因此下面几个命令验证的是不同层:

getent ahosts api.example.com
ip route get 192.0.2.10
ss -tanp
curl -v https://api.example.com/
  • getent ahosts:验证 NSS 返回的地址;
  • ip route get:验证内核将选择哪条路由、哪个源地址和接口;
  • ss:观察 TCP 是否建立、是否重传或卡在某状态;
  • curl -v:同时观察解析、连接、TLS 和 HTTP 阶段。

例如 DNS 得到:

2001:db8::10
192.0.2.10

ip -6 route get 2001:db8::10 失败,应用可能先尝试 IPv6,等待失败后才退回 IPv4。这种故障表面上像“DNS 慢”,实际是 IPv6 路由或连接路径问题。


独立验证:getentresolvectldig 的边界

getent 验证真实 NSS 路径

getent hosts api.example.com
getent ahosts api.example.com

getent 使用 glibc NSS,因此最接近大多数使用系统解析接口的动态链接程序。

hostsahosts 并不完全等价:

  • getent hosts 通常偏向主机名到地址的主机数据库查询;
  • getent ahosts 使用 getaddrinfo 风格,更适合观察 IPv4/IPv6 地址族结果。

如果:

getent hosts internal.example.com

返回了 /etc/hosts 中的地址,而 dig internal.example.com 返回了公共 DNS 地址,这不是命令矛盾,而是两者走了不同链路。

resolvectl 验证 resolved

resolvectl query api.example.com
resolvectl query --legend=yes api.example.com

要验证某个接口的配置:

resolvectl status
resolvectl dns
resolvectl domain

如果 resolvectl query 成功,而 getent hosts 失败,应检查:

  • hosts: 行是否包含 resolve
  • libnss_resolve.so.2 是否安装;
  • NSS 控制规则是否提前返回;
  • 应用是否运行在不同的网络命名空间;
  • /etc/nsswitch.conf 是否被容器或运行时覆盖。

dig 区分递归 DNS和权威 DNS

查询系统配置的 DNS:

dig api.example.com A
dig api.example.com AAAA

指定服务器:

dig @192.0.2.53 api.example.com A

从结果中重点观察:

status: NOERROR
ANSWER: 1
AUTHORITY: 0
SERVER: 192.0.2.53#53
Query time: 3 msec

常见 status

  • NOERROR:查询成功;不代表一定有答案;
  • NXDOMAIN:名称不存在;
  • SERVFAIL:服务器无法完成查询,可能是上游失败、DNSSEC 验证失败或配置错误;
  • REFUSED:服务器拒绝该客户端或该查询;
  • TIMEOUT:客户端等待不到响应。

NOERRORANSWER: 0 可能表示该名称存在,却没有请求类型的记录,或返回了其他记录用于进一步处理。不能仅凭 NOERROR 断言“有可连接地址”。

查询权威链路:

dig +trace api.example.com

+trace 会从根开始逐级查询,帮助区分:

  • 根或顶级域委派是否存在;
  • 父区是否委派到正确的权威服务器;
  • 权威服务器是否能回答;
  • 本地递归 DNS 是否只是缓存了旧结果。

它要求当前主机能够直接访问各级 DNS 服务器,并不适合所有受限内网、代理网络或防火墙环境。


从系统调用和数据包确认“到底走了哪条路”

strace 观察 NSS 加载和连接

在具备权限的测试环境中:

strace -f -e trace=connect,sendto,recvfrom,openat \
  getent hosts api.example.com

可能看到:

  • 打开 /etc/nsswitch.conf
  • 打开 /etc/hosts
  • 加载 libnss_dns.so.2libnss_resolve.so.2
  • 连接 127.0.0.53:53
  • 向某个 DNS 地址发送 UDP 数据。

strace 适合确认程序路径,但不应在高流量生产服务上无控制地附加跟踪。附加到进程可能造成额外开销,也可能暴露查询名称和业务信息。

tcpdump 观察 DNS 包

sudo tcpdump -ni any 'port 53'

更精确地观察 UDP 和 TCP DNS:

sudo tcpdump -ni any \
  '((udp or tcp) and port 53)'

如果想只看本机 stub:

sudo tcpdump -ni lo 'port 53'

几个典型判断:

只有到 127.0.0.53 的本地请求,没有外发包

可能原因:

  • systemd-resolved 有缓存;
  • 查询在 NSS 的 /etc/hosts 或其他模块中结束;
  • resolved 没有可用上游;
  • 防火墙、命名空间或接口状态阻止了外发。

有外发请求,没有响应

重点检查:

  • 目标 DNS 地址是否可达;
  • ip route get <dns-ip>
  • UDP 53 是否被防火墙丢弃;
  • 是否需要 TCP 53;
  • VPN 或策略路由是否把请求送错接口;
  • 上游 DNS 是否只允许特定来源地址。

有响应,但 getent 仍失败

重点检查:

  • 响应是否为 NXDOMAINSERVFAIL
  • NSS 模块是否把结果映射为临时错误;
  • 查询的是 A、AAAA 还是其他类型;
  • 应用是否使用不同的网络命名空间或独立解析器;
  • /etc/hostsnsswitch.conf 是否改变了结果。

DNS 查询可能先用 UDP。响应过大、设置了截断位 TC 或某些实现要求时,解析器会改用 TCP 53。只抓 UDP 会错误地得出“DNS 没有 TCP 流量”的结论。


一套可重复的排障流程

假设业务报告:

curl: Could not resolve host: api.example.com

不要立即修改 /etc/resolv.conf。可以按以下顺序缩小范围。

第一步:确认主机和接口状态

ip link
ip addr
ip route
ip route get 192.0.2.53

如果 DNS 服务器本身没有可用路由,继续分析 DNS 配置没有意义。ip route get 显示的是内核对某个目的地址的实际路由选择,包括出口接口、下一跳和可能的源地址。它比只看 ip route 更适合验证某一次通信。

第二步:分别验证 NSS、resolved 和指定 DNS

getent ahosts api.example.com
resolvectl query api.example.com
dig api.example.com A
dig api.example.com AAAA

然后绕过系统配置,指定一个已知 DNS:

dig @192.0.2.53 api.example.com A

可按结果组合判断:

现象 更可能的范围
dig @server 失败 到该 DNS 的网络、服务器 ACL 或 DNS 服务本身
dig @server 成功,普通 dig 失败 /etc/resolv.conf、默认 DNS 或搜索域
resolvectl 成功,getent 失败 NSS 配置、libnss_resolve 或模块加载
getent 成功,应用失败 应用自有解析器、代理、容器网络或应用缓存
A 成功,AAAA 失败 IPv6 DNS 记录、上游策略或 AAAA 查询路径
解析成功,curl 连接失败 路由、防火墙、端口、TLS 或应用层

第三步:检查响应语义,而不只是命令退出码

dig +noall +answer +comments api.example.com

必须区分:

NXDOMAIN

和:

NOERROR
ANSWER: 0

前者表示名称不存在;后者不一定表示整个域名不存在,可能只是没有请求类型的记录。还要查看 CNAMESOAADTC 等信息是否符合环境预期。

第四步:确认请求是否发到了正确链路

sudo tcpdump -ni any 'port 53'

同时在另一个终端执行:

getent ahosts api.example.com

如果机器有 VPN、容器或多张网卡,继续检查:

resolvectl status
ip rule
ip route show table all

DNS 服务器地址正确但走错接口,仍然会超时。尤其是策略路由和 VPN 场景,不能只检查主路由表。

第五步:验证后续连接和 TLS

解析得到地址后:

curl -v https://api.example.com/

如果需要固定 DNS 结果来隔离解析阶段:

curl -v --resolve api.example.com:443:192.0.2.10 \
  https://api.example.com/

--resolve 只改变该次 curl 的主机名到地址映射,同时仍使用 URL 中的主机名进行 HTTP Host 和 TLS SNI。它适合验证“某个 IP 上的服务、证书和路由是否正常”,但不能证明系统 DNS 已修复。

进一步观察 TLS:

openssl s_client \
  -connect 192.0.2.10:443 \
  -servername api.example.com \
  -showcerts

DNS 得到正确 IP,但 SNI 错误、证书链不完整、证书过期或服务端虚拟主机配置错误,都会在 TLS 阶段失败。此类问题应归入 PKI、SNI 和证书运维,而不是继续清 DNS 缓存。


常见故障路径与反例

反例一:只修改 /etc/resolv.conf

管理员执行:

sudo sh -c 'printf "nameserver 192.0.2.53\n" > /etc/resolv.conf'

随后测试暂时成功,但重启网络后又恢复旧内容。原因是该文件由 NetworkManager、systemd-resolved 或 DHCP 客户端管理,手工修改只是改变了当前文件,不改变配置源。

正确做法是先找出管理者:

ps -ef | grep -E 'systemd-resolved|NetworkManager|dhclient'
systemctl is-active systemd-resolved
ls -l /etc/resolv.conf

然后在对应管理器中修改 DNS,并用 resolvectl statusgetentdig 验证最终状态。生产环境直接停用解析服务可能影响所有使用 stub 的进程,应先确认 nsswitch.confresolv.conf 的替代路径。

反例二:dig 成功,所以应用应该成功

dig api.example.com

成功只说明 dig 使用的 DNS 路径得到了响应。应用可能:

  • 先命中错误的 /etc/hosts
  • 使用 nsswitch.conf 中的 mDNS 或 LDAP 模块;
  • 运行在容器中;
  • 使用自己的 DoH;
  • 使用不同的代理或网络命名空间。

因此,系统应用的第一项对照测试通常应是:

getent ahosts api.example.com

反例三:清缓存后旧地址仍然存在

可能的缓存位置包括:

  • 浏览器;
  • 应用运行时;
  • systemd-resolved
  • nscd
  • 企业递归 DNS;
  • 云厂商 DNS;
  • 权威 DNS 前的 CDN 或 DNS 代理。

清除某一层只会改变该层。更重要的是,DNS 记录发布具有传播和缓存时间;如果旧记录 TTL 尚未过期,递归服务器继续返回旧地址是协议允许的行为。

反例四:DNS 返回两个地址,但一个地址不可达

getent ahosts api.example.com

可能返回 IPv6 和 IPv4。DNS 服务器认为两者都是有效记录,但它不负责保证客户端到每个地址的路由和服务端口都可用。

分别验证:

ip route get 192.0.2.10
ip -6 route get 2001:db8::10
nc -vz 192.0.2.10 443
nc -6 -vz 2001:db8::10 443

如果 IPv6 路由存在但实际被防火墙丢弃,问题应在 IPv6 路径、策略或服务监听地址中修复,而不是删除 AAAA 记录作为长期替代方案。

反例五:把 .local 当作普通 DNS 域名

.local 常用于 mDNS。若 NSS 配置包含:

hosts: files mdns4_minimal [NOTFOUND=return] dns

查询 printer.local 可能被送往链路本地多播,而不是企业 DNS。若企业确实把 .local 作为普通单播 DNS 后缀,mDNS 配置可能造成延迟或提前返回。

这不是单纯的 DNS 服务器故障,而是名称空间和 NSS 模块冲突。应避免把 mDNS 专用后缀和企业单播 DNS 后缀混用。


缓存与配置变更的生产取舍

修改 DNS 记录前先看 TTL 和回滚路径

如果要迁移服务地址,至少要知道:

  • 当前 A、AAAA、CNAME 的 TTL;
  • 中间是否有 CDN、负载均衡或企业递归 DNS;
  • 客户端是否有长期缓存;
  • 新旧地址是否能并行服务;
  • 回滚时旧地址是否仍然可用。

提前降低 TTL 只能影响降低之后被重新获取的记录,不能让已经缓存的旧记录立即失效。TTL 降低得过早还可能增加递归 DNS 查询量。

清缓存不是无风险操作

清缓存可能导致:

  • 大量请求同时访问上游 DNS;
  • 依赖 DNS 的服务出现瞬时延迟;
  • DNS 服务器 ACL、限流或容量问题被放大;
  • 旧缓存被清掉后暴露出权威 DNS 当前的错误。

因此清缓存应当配合:

resolvectl statistics
sudo resolvectl flush-caches
resolvectl statistics

并观察外部 DNS 查询量、服务错误率和解析延迟,而不是把它当成默认修复动作。

DNSSEC、加密 DNS 与传统 DNS 的边界

传统 DNS 通常使用 UDP 53,必要时回退 TCP 53。DoT 使用 TLS,通常为 TCP 853;DoH 使用 HTTPS,通常为 TCP 443 或 HTTP/3。应用若直接使用 DoH,抓包中可能只看到 HTTPS,而看不到明文 DNS 包。

DNSSEC 验证失败时,递归解析器或本机验证器可能返回 SERVFAIL,即使域名在权威服务器上确实存在。排查时应比较:

dig @指定递归DNS api.example.com
dig +dnssec @指定递归DNS api.example.com
dig +trace api.example.com

但不能仅凭是否出现 RRSIG 判断整个链路已经完成验证;还要确认验证器是否启用、信任链是否成立,以及上游是否已经替客户端完成验证。

加密 DNS 解决的是传输观察和篡改风险的一部分,不会自动修复:

  • 错误的 search 域;
  • 错误的 NSS 顺序;
  • 失效的路由;
  • 过期证书;
  • 服务端 SNI 配置;
  • 应用自己的错误缓存。

最小化诊断脚本示例

下面的命令序列适合在测试主机或低风险维护窗口执行:

#!/usr/bin/env bash
set -u

name="${1:-api.example.com}"

echo "== resolver configuration =="
ls -l /etc/resolv.conf
cat /etc/resolv.conf

echo
echo "== nsswitch hosts =="
getent ahosts "$name" || true

echo
echo "== systemd-resolved =="
if command -v resolvectl >/dev/null 2>&1; then
    resolvectl query "$name" || true
    resolvectl status || true
fi

echo
echo "== DNS wire query =="
if command -v dig >/dev/null 2>&1; then
    dig +time=2 +tries=1 "$name" A || true
    dig +time=2 +tries=1 "$name" AAAA || true
fi

echo
echo "== route candidates =="
if command -v getent >/dev/null 2>&1; then
    getent ahostsv4 "$name" || true
    getent ahostsv6 "$name" || true
fi

这个脚本故意不自动执行 flush-caches、重启 systemd-resolved 或修改配置,因为这些操作会改变系统状态。它只采集四类证据:

  1. /etc/resolv.conf 的入口配置;
  2. glibc NSS 的结果;
  3. systemd-resolved 的视图;
  4. 独立 DNS 客户端的 A/AAAA 结果。

如果四者不一致,差异本身就是定位线索。


如何建立可靠的判断链

一个可验证的判断应当像下面这样逐步展开:

应用报“无法解析”
    ↓
getent 是否成功?
    ├─ 否:检查 NSS、hosts、resolved、resolv.conf 和 DNS 响应
    └─ 是:应用可能绕过系统解析,或问题已进入连接阶段
         ↓
dig @指定服务器是否成功?
    ├─ 否:检查到 DNS 的路由、ACL、UDP/TCP 53 和服务器状态
    └─ 是:检查本机选择的 nameserver、缓存和搜索域
         ↓
IP 路由和端口是否成功?
    ├─ 否:检查 ip route、策略路由、防火墙和服务监听
    └─ 是:检查 TLS、SNI、证书链和应用协议

这条链路的核心不是命令数量,而是每一步都验证一个不同的假设:

  • getent 验证 NSS;
  • resolvectl 验证 resolved 的配置、路由域和缓存视图;
  • dig @server 验证指定 DNS 的协议交互;
  • tcpdump 验证实际数据流向;
  • ip route get 验证内核路由;
  • curlopenssl s_client 验证连接及 TLS。

Linux DNS 排障的关键结论是:“系统 DNS”不是单一组件,而是一条可分叉的解析链路。 glibc 提供调用入口,NSS 决定查询模块,systemd-resolved 可能提供本地代理、按接口选择和缓存,/etc/resolv.conf 可能只是入口指针,递归 DNS 负责缓存和迭代查询,而应用还可能完全绕开上述路径。只有把每一层的输入、输出、缓存和网络流量分别验证,才能把“解析失败”“解析到旧地址”“解析成功但连接失败”区分开。


系列导航与关联阅读

官方资料

本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。