2983 字
约 9 分钟
20
S02-X09 · DNS 解析链路与缓存层级

"解析改了,为什么还有用户访问旧地址"——答案不在 DNS 配置面板,而在从浏览器到权威服务器的六层缓存上,以及每层遵守(或不遵守)TTL 的方式。

先拆开看一次解析:谁替谁跑腿、缓存的过期时间从哪来、哪一层会骗你。


1. 一次解析的完整链路

层级 位置 典型 TTL 行为 观察方式
浏览器缓存 浏览器进程内存 / 磁盘 短,且受页面连接复用影响 清缓存、开无痕
系统缓存 systemd-resolved / nscd / mDNSResponder 按响应 TTL,部分实现有上限 resolvectl statistics
hosts 文件 /etc/hosts 永不过期,优先级高于 DNS getent hosts DOMAIN
本地 DNS(递归器) 运营商、8.8.8.8、127.0.0.53 按 TTL,可能被强制抬高或压低 dig @LOCAL_DNS
根 / 顶级域(迭代) 13 组根、各 TLD 委派记录的 TTL 较长 dig +trace
权威服务器 你的 DNS 服务商 由记录 TTL 决定 dig @AUTH_NS
$ dig +trace DOMAIN +short
$ dig +noall +answer DOMAIN A

+trace 会从根开始逐级查询,把每一步的委派与最终答案都打出来——这是判断"卡在哪一层"最直接的手段。


2. 递归查询与迭代查询:谁替谁跑腿

查询方式 发起者 谁承担逐级查找 结果形态
递归查询 客户端 → 本地 DNS 本地 DNS 独自跑完全程 要么给最终答案,要么给错误
迭代查询 本地 DNS → 根/TLD/权威 每一跳只给"下一跳去哪" 逐级委派(NS + glue 记录)

客户端(stub resolver)总是发递归查询,报文里置 RD=1;递归器对上游发迭代查询并自己维护缓存。所以"权威服务器上没有请求日志"是正常现象——请求都在递归器那层被缓存拦住了。

# 关闭递归,模拟"只问不跑腿",观察是否只拿到委派
$ dig @8.8.8.8 DOMAIN A +norecurse +noall +authority

3. 记录类型各自干什么

类型 用途 容易踩的点
A 名字 → IPv4 只改 A 不改 AAAA,双栈客户端仍走旧地址
AAAA 名字 → IPv6 没有 IPv6 服务却配了 AAAA,会拖慢连接建立
CNAME 别名 → 另一个名字 不能与 MX/TXT 共存于同一名字;链路过长会断
MX 邮件路由,带优先级 指向 CNAME 是违规的
TXT 任意文本:SPF、DKIM、域名验证 多条 TXT 是并列关系,不是覆盖关系
NS 委派:该区由谁权威 改 NS 后旧 NS 仍可能应答一段时间
SRV 服务发现:主机 + 端口 + 权重 客户端支持度不一,_service._proto.name 格式易写错
CAA 限制哪些 CA 能签发本域证书 不配等于不限制;配错会导致续签失败
$ dig DOMAIN MX +short
$ dig DOMAIN TXT +short
$ dig _https._tcp.DOMAIN SRV +short

4. TTL 与"改了解析为什么不生效"

TTL 是缓存方遵守的建议值,不是强制命令。改记录后,旧值要等各级缓存逐个过期,链路才收敛到新值。

现象 真实原因 处理
自己刷新就生效,别人不生效 你命中的是另一台递归器 换递归器对比 dig @8.8.8.8 与 @1.1.1.1
部分城市仍旧地址 运营商递归器不严格遵守 TTL,或做了透明缓存 提前把 TTL 调低再切,切完再调回
改了 A 记录但 IPv6 用户异常 AAAA 没同步改 两个类型一起改,或先删 AAAA
浏览器清缓存仍不生效 连接复用 / HTTP/2 会话保持 / DNS 预取 换浏览器或无痕窗口验证
# 拿到权威值,绕开所有中间缓存
$ dig @AUTH_NS DOMAIN A +noall +answer
# 看当前递归器缓存里剩多少
$ dig @LOCAL_DNS DOMAIN A +noall +answer

切流量的正确顺序:提前一个 TTL 周期把 TTL 降到 60 秒量级 → 观察一个周期 → 改记录 → 验证 → 再调回长 TTL。反过来做,故障时就没有快速回退的余地。


5. CNAME 的两条硬约束

  1. 不能与 MX、TXT 等共存于同一名字(RFC 1034/2181 的语义约束)。example.com 上放了 CNAME,就不能再放 MX。所以裸域(apex)通常用 ALIAS/ANAME 这类厂商扩展,而不是标准 CNAME。
  2. 链路过长会失败。每多一跳就多一次查询,部分递归器对 CNAME 链深度有限制,超了直接返回 SERVFAIL。
$ dig www.DOMAIN CNAME +short
$ dig www.DOMAIN A +noall +answer     # 看解析器是否已把链走完

另一种故障:CNAME 指向内部域名(如 svc.internal),外部递归器解析不到,外部用户拿到 NXDOMAIN 或 SERVFAIL,内网一切正常。


6. 负缓存:NXDOMAIN 也会被缓存

查询一个不存在的名字,权威服务器会在 SOA 记录里给出一个 negative TTL(通常取 SOA 的 minimum 字段),递归器据此缓存"不存在"这一事实。

返回码 含义 是否被缓存
NOERROR + 空 answer 名字存在但没有该类型记录 是(按 SOA minimum)
NXDOMAIN 名字不存在 是(按 SOA minimum)
SERVFAIL 解析器自身出错或上游超时 部分实现会短暂缓存
REFUSED 上游拒绝递归 通常不缓存
$ dig DOMAIN SOA +noall +answer
$ dig not-exist.DOMAIN A +noall +comments | grep -i "status"

这解释了"新域名配好了但部分用户打不开":他们命中的递归器在记录尚不存在时查过一次,负缓存没过期前会一直返回 NXDOMAIN。


7. resolv.conf、ndots 与搜索域

$ cat /etc/resolv.conf
nameserver 127.0.0.53
search svc.cluster.local internal
options ndots:5 timeout:2 attempts:3
参数 作用 典型影响
nameserver 递归器地址,可多个 只有第一个超时才会试下一个
search 短名补全的搜索域列表 短名会按顺序依次尝试拼接
ndots:N 名字里点数 ≥ N 才先当绝对名查 设太大 → 先拼搜索域,多出若干次无效查询
timeout / attempts 单次超时与重试 叠加后总等待可达数秒

ndots 是 Kubernetes 环境最典型的隐形延迟源:默认值偏高时,访问 api.example.com(2 个点)会先被拼成 api.example.com.svc.cluster.local 查一遍,失败再查原名——外部请求因此平白多出一个往返。


8. dig 与 getent hosts 结果不同

$ dig DOMAIN +short
$ getent hosts DOMAIN

原因很直接:dig 直连 DNS 服务器,只走 DNS 协议;getent hosts 走 NSS(/etc/nsswitch.conf 的 hosts: 行),会按顺序查 files(/etc/hosts)、dns,可能还有 mdns4_minimal、myhostname。

工具 走什么 适用判断
dig / nslookup 纯 DNS 协议 判断 DNS 记录本身对不对
getent hosts NSS 全链路 判断应用实际会拿到什么
ping / curl 通常走 NSS 与真实行为最接近
resolvectl query systemd-resolved 看本地缓存与 DNSSEC 结果

所以"dig 能解析但程序连不上",第一件事是 getent hosts,而不是继续查 DNS。


9. CDN 与 DNS 的关系

CDN 的核心机制就是在 DNS 层按来源返回不同地址:递归器的出口 IP 决定它拿到哪个边缘节点。

概念 作用
调度粒度 按递归器出口 IP 归属地/运营商分流,不按真实用户
TTL 与切流速度 TTL 越短切得越快,代价是查询量上升
CNAME 接入 业务域 CNAME 到 CDN 域,由 CDN 决定最终 A 记录
回源段 CDN 到源站的连接与用户侧解析无关,需单独评估
解析异常的表现 部分地区拿到错误节点 → 表现为区域性慢或证书不匹配

常见误判:dig @8.8.8.8 看到的地址与用户实际访问的不同,就断定"解析错了"。其实你把查询源换成了 Google 的出口,本就该拿到另一个节点。


10. DoH / DoT 的取舍

方案 端口 优点 代价
明文 DNS 53/UDP、53/TCP 简单、可抓包、延迟最低 中间人可读可改
DoT 853/TCP 加密且不污染现有解析链路 需要 853 放行,部分网络封
DoH 443/TCP 与 HTTPS 同端口,穿透性最好 混在 Web 流量里,排障与审计变难
内部 DoH 网关 443 统一出口、可审计 单点故障与额外一跳

取舍的关键不是"要不要加密",而是排障能力:上了 DoH,tcpdump port 53 什么都看不到,问题会从"解析错"变成"不知道发生了什么"。至少要在客户端侧留查询日志。


11. 常见坑

坑 现象 正解
只改 A 忘了 AAAA 双栈用户走旧地址 两类一起改,或先摘 AAAA
CNAME 指向内部域名 外部用户 SERVFAIL,内部正常 对外只用公网可达的名字
TTL 设得过长 故障时无法快速切流 切流前先把 TTL 降到 60 秒量级
容器里看到 127.0.0.11 以为上游出错 那是 Docker 内嵌 DNS,真实上游看 docker inspect
用 ping 验证解析 ICMP 被墙导致误判 用 dig + getent hosts 分开验证
忽略负缓存 新记录打不开 等 SOA minimum 过期,或换递归器
ndots 过大 每个外部请求多出无效查询 调小 ndots 或用绝对名(结尾加点)
$ cat /etc/nsswitch.conf | grep '^hosts'
$ docker exec APP cat /etc/resolv.conf
$ resolvectl query DOMAIN

12. 边界

需求 更合适的做法 理由
需要秒级切流 短 TTL + 双写过渡 DNS 缓存不可控,做不到秒级
需要按用户身份分流 应用层调度 DNS 只能看到递归器出口
需要防劫持 DoT/DoH + DNSSEC 明文 DNS 无法保证完整性
内网服务发现 服务注册中心 / SRV 大规模动态实例不适合手工 A 记录
排查"某地打不开" 多地探针 + +trace 单点 dig 无法复现区域差异

一句话:DNS 只负责"名字到地址"的一次性映射,任何要求它承担"实时调度"的设计都会在缓存这一层失效。


13. 自测

  1. 递归查询与迭代查询分别由谁承担逐级查找?为什么权威服务器上看不到用户请求?
  2. 改了 A 记录却没改 AAAA,哪类用户会出问题,表现是什么?
  3. CNAME 为什么不能和 MX 同时存在于同一个名字上?
  4. NXDOMAIN 会被缓存多久,由哪条记录决定?
  5. dig 能解析但应用连不上,下一步该用哪个命令验证?
  6. 容器里 resolv.conf 指向 127.0.0.11,它代表什么?真实上游怎么查?
S02-X09 · DNS 解析链路与缓存层级
https://www.clxhxhhr.top/posts/5901/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。