Linux 基础体系 · 第 65/85 篇。示例面向现代主流 Linux 发行版;发行版差异、权限和生产风险会明确说明。
Linux DNS 解析链路:glibc、nsswitch、systemd-resolved、缓存和排障
DNS 故障经常被误判为“网络不通”。实际上,应用执行一次域名解析时,可能经过应用自身缓存、glibc、NSS 模块、/etc/hosts、systemd-resolved、本机 DNS 缓存、上游递归 DNS,最后才到权威 DNS。任何一层都可能返回成功、失败、旧数据或与预期不同的数据。
要准确排障,首先要区分三个问题:
- 应用是否调用了系统名称解析接口?
- 系统名称解析链路选择了哪个 NSS 模块和 DNS 服务?
- 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
常见模式包括:
- 普通静态文件;
- 指向 systemd-resolved stub 文件的符号链接;
- 指向 systemd-resolved uplink 文件的符号链接;
- 由 NetworkManager、DHCP 客户端、云初始化工具或容器运行时生成。
不要在这些管理者仍然运行时直接手工覆盖文件。文件可能很快被恢复,或者重启后丢失;更严重的是,管理员以为修改已生效,但实际组件继续使用自己的内部状态。
search、domain 与 ndots
给定短名称:
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 主机名,因此不能简单要求所有业务都这样修改。
超时与重试
timeout 和 attempts 影响 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 的files、mdns等模块。
DNS 缓存:没有一个“系统 DNS 缓存”
DNS 结果的有效时间由 TTL 控制,但实际系统中可能存在多层缓存:
浏览器或应用缓存
↓
连接池、代理或语言运行时缓存
↓
nscd、systemd-resolved 等本机缓存
↓
企业 DNS、路由器或云 DNS 缓存
↓
权威 DNS
TTL 的基本规则
对一个资源记录,缓存有效期可以粗略表示为:
其中:
- 是缓存记录被保存的时间;
TTL是权威 DNS 返回的生存时间;- 是当前时间;
- 是缓存还能使用多久。
递归 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。
这会造成一个容易误判的故障:
- 管理员查询到
NXDOMAIN; - 修正了权威 DNS;
- 权威服务器已经返回新地址;
- 客户端仍然得到
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 路由或连接路径问题。
独立验证:getent、resolvectl 和 dig 的边界
用 getent 验证真实 NSS 路径
getent hosts api.example.com
getent ahosts api.example.com
getent 使用 glibc NSS,因此最接近大多数使用系统解析接口的动态链接程序。
hosts 和 ahosts 并不完全等价:
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:客户端等待不到响应。
NOERROR 但 ANSWER: 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.2或libnss_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 仍失败
重点检查:
- 响应是否为
NXDOMAIN或SERVFAIL; - NSS 模块是否把结果映射为临时错误;
- 查询的是 A、AAAA 还是其他类型;
- 应用是否使用不同的网络命名空间或独立解析器;
/etc/hosts或nsswitch.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
前者表示名称不存在;后者不一定表示整个域名不存在,可能只是没有请求类型的记录。还要查看 CNAME、SOA、AD、TC 等信息是否符合环境预期。
第四步:确认请求是否发到了正确链路
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 status、getent、dig 验证最终状态。生产环境直接停用解析服务可能影响所有使用 stub 的进程,应先确认 nsswitch.conf 和 resolv.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 或修改配置,因为这些操作会改变系统状态。它只采集四类证据:
/etc/resolv.conf的入口配置;- glibc NSS 的结果;
- systemd-resolved 的视图;
- 独立 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验证内核路由;curl和openssl s_client验证连接及 TLS。
Linux DNS 排障的关键结论是:“系统 DNS”不是单一组件,而是一条可分叉的解析链路。 glibc 提供调用入口,NSS 决定查询模块,systemd-resolved 可能提供本地代理、按接口选择和缓存,/etc/resolv.conf 可能只是入口指针,递归 DNS 负责缓存和迭代查询,而应用还可能完全绕开上述路径。只有把每一层的输入、输出、缓存和网络流量分别验证,才能把“解析失败”“解析到旧地址”“解析成功但连接失败”区分开。
系列导航与关联阅读
- 系列入口:Linux 完整学习路线:从内核与文件系统到网络、性能和生产运维
- 上一篇:Linux TCP 诊断:ss、tcpdump、重传、SYN 队列、TIME_WAIT 和延迟
- 下一篇:Linux 路由深入:多表、策略路由、ECMP、VRF 和反向路径检查
- 延伸:Linux 路由、DNS 与网络诊断:ip、ss、dig、tcpdump 和抓包
- 延伸:Linux TLS 与证书运维:PKI、链、SNI、OCSP、续期和排障
官方资料
本文依据 Linux 内核、systemd 与主流发行版官方文档重新梳理;正文与实验由 WR BLOG 编写。

评论
0 条讨论