Linux DNS 间歇失败:区分 NSS、缓存、上游与分流解析

13 次浏览2 条回复

适用于使用 glibc 与 systemd-resolved 的 Linux 主机,命令基线为 systemd 245+、iproute2 5.5+;dig 来自 Debian/Ubuntu 的 dnsutils 或 RHEL 系的 bind-utils,属于可选工具。示例域名、接口和 DNS 地址都必须替换为实际值。先采集失败窗口,不要一开始就改写 /etc/resolv.conf 或清空缓存。

1. 从原始失败环境固定证据

在出现问题的同一主机、容器或网络命名空间内执行:

date -u
getent ahosts api.example.com
resolvectl query api.example.com
readlink -f /etc/resolv.conf
sed -n '/^[[:space:]]*hosts:/p' /etc/nsswitch.conf

getent 走应用常用的 NSS 路径,结果会受 nsswitch.conf、本地文件和解析模块影响;resolvectl query 观察 systemd-resolved 的决策。两者结果不同,优先检查 NSS 顺序、容器内配置以及应用是否使用自己的解析器。应保留应用原始错误:超时、SERVFAILNXDOMAIN 和临时解析失败指向的分支不同。

若系统未运行 systemd-resolved,resolvectl 失败不等于 DNS 故障,应按实际使用的 NetworkManager、容器运行时或本地缓存程序继续检查。

2. 确认配置来源与每链路 DNS

systemctl is-active systemd-resolved
resolvectl status
ip -br link
ip route
ss -lunp '( sport = :53 )'

重点记录目标接口的 DNS Servers、DNS Domain、DefaultRoute 与当前默认路由。/etc/resolv.conf 指向 127.0.0.53 时,它只是本机 stub,真实上游应从 resolvectl status 查看。若同一域名只在 VPN 接入后失败,检查路由域是否配置为 ~corp.example 一类的分流域,以及查询是否被送到预期接口;不要只把公共 DNS 写入文件,这可能破坏内部域名解析。

3. 分别验证上游、UDP 与 TCP

dig 时,对实际上游逐个执行:

dig @DNS_IP api.example.com A +time=2 +tries=1
dig @DNS_IP api.example.com AAAA +time=2 +tries=1
dig @DNS_IP api.example.com A +tcp +time=2 +tries=1

直接查询上游成功而 getent 失败,问题更靠近本机 NSS、stub 或缓存;所有上游都超时,再查路由、防火墙和上游可达性。UDP 超时但 TCP 成功,或较大响应才失败时,应检查 53/UDP 策略、响应截断、EDNS 与路径 MTU。一次成功不足以排除间歇故障,应在故障窗口对同一上游做有限次数采样,并记录每次 UTC 时间和返回码。

4. 对齐日志、统计与报文

resolvectl statistics
journalctl -u systemd-resolved --since '-15 min' --no-pager -o short-iso
sudo tcpdump -ni any '(udp port 53 or tcp port 53)' -c 100

抓包时同时触发一次受控查询。只有请求没有响应,继续沿目标上游和回程排查;收到 SERVFAILNXDOMAIN,应保留响应方地址并检查权威记录、DNSSEC 或分流配置;有响应但应用仍报错,则回到 NSS、应用缓存和网络命名空间。抓包可能包含查询名称和地址,应限制包数并对公开输出做必要脱敏。统计项是累计值,应比较同一服务启动周期内的前后增量,不要把单次总数当作当前故障率。

5. 排除容器与宿主机视角差异

宿主机解析成功不能证明容器内成功。应进入实际工作负载执行 getent ahosts api.example.com,并检查其 /etc/resolv.conf;若只能从宿主机定位,可先确认目标进程 PID,再在其网络命名空间只读验证:

readlink /proc/1/ns/net /proc/PID/ns/net
sudo nsenter -t PID -n getent ahosts api.example.com

nsenter 来自 util-linux,执行前必须确认 PID 属于目标进程。容器中的 127.0.0.53 若没有对应本地 stub,通常是配置传递错误,不能用宿主机上的监听状态代替判断。

6. 闭环验证

每次只修改一个有证据支持的配置项。完成后从最初失败的应用入口重复无副作用请求,并同时确认:

getent ahosts api.example.com
resolvectl query api.example.com
resolvectl statistics

验证窗口应覆盖原先的失败周期;结果需显示查询被送到预期链路和上游,应用错误不再新增,超时或失败计数不再随受控请求增长。仅执行一次 dig 成功,不能证明应用实际解析路径已经恢复。

可以补一个容易误判的细节:BIND 的 dig 收到带 TC 标志的 UDP 响应时,默认可能继续用 TCP 重试,因此普通查询成功不一定代表 UDP 已拿到完整响应。排查截断或 EDNS 路径时,可把原始 UDP 响应与显式 TCP 分开观察(先用 dig -v 记录实际版本):

dig @DNS_IP api.example.com A +time=2 +tries=1 +ignore
dig @DNS_IP api.example.com A +time=2 +tries=1 +tcp

+ignore 会保留截断的 UDP 结果,重点查看响应头是否包含 tc;若 UDP 有 tc 而 TCP 返回完整答案,这是正常回退条件,本身不能直接归因为上游故障。若应用仍失败,应确认其是否支持 TCP 回退,以及客户端到上游的 53/TCP 是否可达。

怀疑 EDNS 或分片路径时,再保持域名、记录类型和上游不变,分别比较:

dig @DNS_IP api.example.com A +time=2 +tries=1 +ignore +bufsize=1232
dig @DNS_IP api.example.com A +time=2 +tries=1 +ignore +noedns

这两条应分开执行并同步抓包;只有 +noedns 稳定成功时,才有依据继续检查 EDNS 处理、中间设备和 UDP 大包路径。最终仍需从原应用入口复测,避免把 dig 的回退行为当成应用解析已经恢复。

再补一个容易被完整域名测试掩盖的分支:应用若实际查询的是短名,glibc 会按 /etc/resolv.conf 中的 searchoptions ndots:n 展开查询;VPN 或 DHCP 更新搜索域后,实际 QNAME 和选中的分流链路都可能变化。此时用 dig api.example.com 成功,不能覆盖应用对 api 的失败。

先在应用所在的同一网络命名空间记录配置,并分别复现短名、完整名和带根点的绝对名:

date -u
sed -n '/^[[:space:]]*\(search\|domain\|options\|nameserver\)/p' /etc/resolv.conf
resolvectl status
getent ahosts api
getent ahosts api.example.com
resolvectl query api
resolvectl query api.example.com.

api.example.com. 末尾的点表示绝对域名,可避免再次附加搜索后缀。若只有短名失败,应同步抓包确认客户端依次发出的 QNAME、响应码和目标 DNS,而不是只比较最终返回值:

sudo tcpdump -ni any -vv '(udp port 53 or tcp port 53)' -c 100

抓包后立即在同一环境触发一次短名查询。若短名被展开到意外后缀,修复对象应是 DHCP、VPN 或网络管理器提供的搜索域与路由域;不要只调高重试次数。验证时应从应用原入口复测,并确认查询名称被送到预期链路。