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

135 次浏览8 条回复

适用于使用 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 顺序、容器内配置以及应用是否使用自己的解析器。应保留应用原始错误:超时、SERVFAIL、NXDOMAIN 和临时解析失败指向的分支不同。

若系统未运行 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

抓包时同时触发一次受控查询。只有请求没有响应,继续沿目标上游和回程排查;收到 SERVFAIL 或 NXDOMAIN,应保留响应方地址并检查权威记录、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 中的 search 与 options 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 或网络管理器提供的搜索域与路由域;不要只调高重试次数。验证时应从应用原入口复测,并确认查询名称被送到预期链路。

再补一个 NSS 层的细节:hosts: 后面的方括号动作会改变继续查询还是提前返回,不能只看模块排列顺序。典型规则中的 [NOTFOUND=return] 或 [!UNAVAIL=return],可能让前一个模块返回未找到、临时失败后直接结束,不再进入后面的 dns。这类情况常表现为 resolvectl query 能得到结果,而走 glibc NSS 的应用仍失败。

先保留完整规则和原始返回状态:

date -u
getent --version | head -n 1
sed -n '/^[[:space:]]*hosts:/p' /etc/nsswitch.conf
getent ahosts api.example.com
getent ahostsv4 api.example.com
getent ahostsv6 api.example.com
resolvectl query api.example.com.

分别执行 ahostsv4 与 ahostsv6,可以避免把某一地址族的失败误当成整个域名失败。若系统的 glibc getent 提供 --service 选项,还可先用 getent --help 确认语法,再临时指定单个 NSS 服务作只读对照;不同发行版可能没有相同选项,不应把它写入自动化脚本而不做能力检测。

判断方括号规则时需要保留状态语义:SUCCESS、NOTFOUND、UNAVAIL、TRYAGAIN 并不等价。例如 [!UNAVAIL=return] 表示除 UNAVAIL 外都立即返回,其中也包括 TRYAGAIN,所以短暂超时可能不会回退到后续模块。只有 resolvectl 持续成功、同环境 getent 持续失败,并且调整相应 NSS 模块或动作后原应用入口恢复,才足以把问题定位到 NSS 控制流;修改前应备份配置来源,并确认该文件是否由系统配置管理生成。

隔壁打个盹呢Lv1#3

再补一个 NSS 层的细节:hosts: 后面的方括号动作会改变继续查询还是提前返回,不能只看模块排列顺序。典型规则中的 [NOTFOUND=return] 或 [!UNAVAIL=return],可能让前一个模块返回未找到、临时失败后直接结束,不再进入后面的 dns。这类情况常表现为 resolvectl query 能得到结果,而走 glibc NSS 的应用仍失败。

先保留完整规则和原始返回状态:

date -u
getent --version | head -n 1
sed -n '/^[[:space:]]*hosts:/p' /etc/nsswitch.conf
getent ahosts api.example.com
getent ahostsv4 api.example.com
getent ahostsv6 api.example.com
resolvectl query api.example.com.

分别执行 ahostsv4 与 ahostsv6,可以避免把某一地址族的失败误当成整个域名失败。若系统的 glibc getent 提供 --service 选项,还可先用 getent --help 确认语法,再临时指定单个 NSS 服务作只读对照;不同发行版可能没有相同选项,不应把它写入自动化脚本而不做能力检测。

判断方括号规则时需要保留状态语义:SUCCESS、NOTFOUND、UNAVAIL、TRYAGAIN 并不等价。例如 [!UNAVAIL=return] 表示除 UNAVAIL 外都立即返回,其中也包括 TRYAGAIN,所以短暂超时可能不会回退到后续模块。只有 resolvectl 持续成功、同环境 getent 持续失败,并且调整相应 NSS 模块或动作后原应用入口恢复,才足以把问题定位到 NSS 控制流;修改前应备份配置来源,并确认该文件是否由系统配置管理生成。

这个判断还需要先确认一个前提:故障应用确实走 glibc NSS。getent 是 NSS 路径的对照,但静态链接程序、musl 容器、部分语言运行时的内置解析器以及应用自身 DNS 缓存,可能绕过 nsswitch.conf;因此 getent 成功而应用失败,不应直接归因于 NSS。

在 Linux 5.4+ 上,可先用 procfs 固定目标进程视角;读取其他用户进程通常需要 root,readelf 来自 binutils 2.34+:

PID=<实际PID>
date -u
readlink -f /proc/$PID/exe
readlink /proc/$PID/ns/net /proc/$PID/ns/mnt
sed -n '/^[[:space:]]*\(search\|domain\|options\|nameserver\)/p' \
  /proc/$PID/root/etc/resolv.conf
readelf -l /proc/$PID/exe | grep 'Requesting program interpreter'
grep -E '/(libc\.so|ld-musl-|libnss_)' /proc/$PID/maps | sort -u

解释时要保守:看到 ld-musl 可确认不是 glibc NSS;看不到 libnss_* 不能证明应用没有使用 NSS,动态模块可能尚未加载或已经卸载;存在 glibc 也不能证明语言运行时没有选择自己的解析器。若目标根文件系统包含 getent,可用 util-linux 2.36+ 在同一挂载和网络命名空间做对照:

sudo nsenter -t "$PID" -m -n -r -- getent ahosts api.example.com

若该命令不存在,不要把宿主机的 getent 结果当成容器结果。此时应使用运行时自身的诊断开关或应用提供的解析日志,并在受控窗口触发一次原应用请求,同时记录目标网络命名空间的 DNS 流量:

sudo nsenter -t "$PID" -n -- \
  tcpdump -ni any '(udp port 53 or tcp port 53)' -c 50

抓包可能包含域名与地址,应限制包数并脱敏。验证闭环应同时满足:应用原入口恢复、应用实际使用的地址符合预期、同一故障窗口不再新增解析错误。若 resolvectl 与同环境 getent 都持续正确,但应用仍使用旧地址,优先检查运行时或连接池缓存及其 TTL;只改 nsswitch.conf 不能验证这条分支。

还可以补一条与“间歇恢复”很相关的分支:负缓存。域名刚创建、记录短暂删除或分流链路一度返回 NXDOMAIN/NODATA 后,本机 stub、递归上游与应用可能在一段时间内继续缓存否定结果;这时反复执行同一路径查询只能看到缓存,不足以判断权威数据当前是否正确。getent 也不会显示 TTL。

先在故障窗口保留本机路径的响应与缓存统计;dig 来自 BIND 工具包,示例地址必须替换:

date -u
resolvectl statistics
resolvectl query api.example.com.
readlink -f /etc/resolv.conf
ss -lunp '( sport = :53 )'

# 仅在确认 127.0.0.53 是当前 stub 时执行
dig @127.0.0.53 api.example.com. A \
  +time=2 +tries=1 +noall +comments +answer +authority

重点保留状态码、AUTHORITY 段的 SOA 和剩余 TTL。否定响应的缓存时长受 SOA 字段与解析器实现影响,不能把固定等待时间写死。随后,只有在已知且获准直连该域权威 DNS 时,才绕过递归缓存作只读对照:

dig @AUTH_DNS_IP api.example.com. A +norecurse \
  +time=2 +tries=1 +noall +comments +answer +authority

若权威端已返回当前记录,而本机路径仍返回带递减 TTL 的旧否定结果,才有依据继续定位本机或递归上游缓存;分区视图、VPN 内部域或不可直连权威服务器时,这个对照不成立,应改为比较同一分流链路上的递归上游。

resolvectl flush-caches 会改变现场,不应放在取证之前。确需把它作为诊断变量时,先记录上述输出,再执行一次并立即从原应用入口复测;若只恢复一次但在负 TTL 或应用缓存周期后再次失败,仍需回到记录发布顺序、分流链路和应用自身缓存策略,而不能把清缓存当作修复。

novatraiLv1#5

还可以补一条与“间歇恢复”很相关的分支:负缓存。域名刚创建、记录短暂删除或分流链路一度返回 NXDOMAIN/NODATA 后,本机 stub、递归上游与应用可能在一段时间内继续缓存否定结果;这时反复执行同一路径查询只能看到缓存,不足以判断权威数据当前是否正确。getent 也不会显示 TTL。

先在故障窗口保留本机路径的响应与缓存统计;dig 来自 BIND 工具包,示例地址必须替换:

date -u
resolvectl statistics
resolvectl query api.example.com.
readlink -f /etc/resolv.conf
ss -lunp '( sport = :53 )'

# 仅在确认 127.0.0.53 是当前 stub 时执行
dig @127.0.0.53 api.example.com. A \
  +time=2 +tries=1 +noall +comments +answer +authority

重点保留状态码、AUTHORITY 段的 SOA 和剩余 TTL。否定响应的缓存时长受 SOA 字段与解析器实现影响,不能把固定等待时间写死。随后,只有在已知且获准直连该域权威 DNS 时,才绕过递归缓存作只读对照:

dig @AUTH_DNS_IP api.example.com. A +norecurse \
  +time=2 +tries=1 +noall +comments +answer +authority

若权威端已返回当前记录,而本机路径仍返回带递减 TTL 的旧否定结果,才有依据继续定位本机或递归上游缓存;分区视图、VPN 内部域或不可直连权威服务器时,这个对照不成立,应改为比较同一分流链路上的递归上游。

resolvectl flush-caches 会改变现场,不应放在取证之前。确需把它作为诊断变量时,先记录上述输出,再执行一次并立即从原应用入口复测;若只恢复一次但在负 TTL 或应用缓存周期后再次失败,仍需回到记录发布顺序、分流链路和应用自身缓存策略,而不能把清缓存当作修复。

补充一个判读边界:权威直查与递归路径的结果不一致,不一定都是负缓存。+norecurse 查询权威服务器通常只观察该服务器当前提供的数据,并不替客户端完成 DNSSEC 验证;因此“权威有记录、递归返回 SERVFAIL”还要排除签名或委派链验证失败。

若实际递归上游允许直接查询,可在同一网络命名空间、同一时间窗口比较正常验证与设置 CD 位后的结果;dig 来自 BIND 工具包,地址需替换为真实上游:

date -u
dig @RECURSOR_IP api.example.com. A +dnssec \
  +time=2 +tries=1 +noall +comments +answer +authority
dig @RECURSOR_IP api.example.com. A +dnssec +cdflag \
  +time=2 +tries=1 +noall +comments +answer +authority

若正常查询稳定返回 SERVFAIL,而带 +cdflag 的查询返回数据,说明故障更接近该递归器的 DNSSEC 验证路径,不能按普通否定缓存处理。应继续检查 DS 与 DNSKEY 是否匹配、RRSIG 是否过期,以及验证器系统时间;只有明确看到 NXDOMAIN 或 NODATA、SOA 与递减 TTL,才进入负缓存分支。

标志也要保守解释:权威响应中的 aa 只表示权威回答,不表示已验证;递归响应有 ad 才表示该解析器声称数据已验证,但没有 ad 也可能是解析器未启用验证或未向客户端提供该标志。部分递归器不接受客户端的 CD 位,此时两次结果相同不能排除 DNSSEC,应结合上游日志或使用获准的独立验证器复核。最终验证仍应走原应用实际使用的递归链路。

再补一个进程级取证方法:当 getent、resolvectl 与应用结果不一致时,可对一次短生命周期、无副作用的原应用请求做系统调用跟踪,确认它实际读取了哪些配置、连接了哪个解析端点。适用于 Linux 5.4+、strace 5.5+;跟踪其他用户进程可能受 ptrace 策略限制并需要 root。输出可能含域名、地址和文件路径,应先设为仅当前用户可读。

umask 077
date -u
strace -ff -tt -T -yy -s 256 \
  -e trace=%network,openat,readlink \
  -o /tmp/app-dns.trace -- <实际无副作用命令>

grep -Eh 'nsswitch|resolv\.conf|/etc/hosts|:53|127\.0\.0\.53|io\.systemd\.Resolve' \
  /tmp/app-dns.trace*

-ff 保留派生进程,-tt -T 便于与应用日志、抓包按时间对齐,-yy 可显示套接字端点。看到进程读取 nsswitch.conf、hosts 或 resolv.conf,并向 127.0.0.53:53 发包,说明这次请求至少进入了相应本机解析路径;直接连接其他 53 端口则提示绕过 stub。若使用 nss-resolve,也可能看到到 systemd-resolved Unix socket 的连接,而不是 53 端口报文。

判读时要保留三个边界:没有观察到 53 端口调用,不能证明应用没有解析,结果可能来自进程缓存、父进程传入的地址或加密 DNS;短生命周期命令的行为也不一定等同于已运行很久的守护进程;strace 会改变时序,不宜把一次“跟踪后恢复”当作修复证据。若必须附加到线上 PID,应先评估时延影响,只覆盖一个受控请求窗口。

闭环时可同时在目标网络命名空间限制包数抓取 DNS 流量,并核对三项:系统调用中的目标端点、报文中的查询名称与响应码、原应用日志中的 UTC 时间和错误。三者对齐后再决定检查 NSS 规则、本地 stub、递归上游或应用缓存,避免仅凭某个独立诊断工具成功就改变配置。

再补一个容易让“同一命名空间里的 getent”仍与应用不一致的变量:glibc 解析器允许进程启动环境中的 LOCALDOMAIN 覆盖搜索域,并用 RES_OPTIONS 补充 ndots、timeout、attempts 等选项。进入目标网络或挂载命名空间不会自动复制目标进程的环境,因此另起的诊断命令可能仍未复现应用实际参数。

在 Linux 5.4+ 上可先只筛选这两个变量;读取其他用户进程的环境通常需要 root,且不要公开完整 environ,其中可能含凭据:

PID=<实际PID>
date -u
sudo cat "/proc/$PID/environ" \
  | tr '\0' '\n' \
  | sed -n -e '/^LOCALDOMAIN=/p' -e '/^RES_OPTIONS=/p'

sed -n '/^[[:space:]]*\(search\|domain\|options\|nameserver\)/p' \
  "/proc/$PID/root/etc/resolv.conf"

LOCALDOMAIN 存在时,不能再仅按文件中的 search 行推断短名展开;RES_OPTIONS 中较高的 ndots 会改变查询顺序,较小的 timeout 或 attempts 则可能让应用比交互式诊断更早失败。只有筛选后的变量为空,才可排除这两个显式覆盖项。/proc/PID/environ 主要反映进程初始环境;进程若在运行中自行修改环境,输出不一定代表解析调用当时的值,此时仍应以运行时日志或系统调用跟踪为准。

复现时应在隔离的诊断 shell 中仅带入已确认的这两个值,再执行与应用相同的短名或完整名查询;不要导入目标进程的全部环境。对 systemd 服务还应追溯变量来自单元、drop-in 还是启动包装脚本,修改前先确认配置来源。闭环需要从原应用入口验证查询名称、目标解析端点与错误时间线,而不能把一个未复制进程环境的 nsenter ... getent 成功当作恢复证据。