DNS 解析过程详解:从域名查询到 TTL 缓存的 Python 实验
DNS 解析过程详解:从域名查询到 TTL 缓存的 Python 实验
站内搜索
直接问 AI

DNS 解析过程详解:从域名查询到 TTL 缓存的 Python 实验

浏览器输入一个域名之后,第一个可能阻塞请求的环节不是 HTTP,而是 DNS。DNS 的工程难点不在于记住“域名转 IP”,而在于理解递归解析、报文边界、TTL 缓存和失败时延之间如何相互作用。

本文从 RFC 1034/1035 的模型出发,用固定的四次查询实验,把缓存命中率如何改变用户感知延迟算出来。

一、从 stub resolver 到 authoritative server

应用通常把查询交给递归解析器。缓存缺失时,递归解析器可能依次询问 root、TLD 和 authoritative server;命中缓存时则直接返回仍在 TTL 有效期内的记录。响应报文的 header 至少携带 transaction ID、flags、question count 和 answer count。

DNS TTL 缓存命中和缺失的解析延迟时间线
固定 TTL 为 60 秒:第 1、3 次查询 MISS,第 2、4 次查询 HIT。

二、手算 TTL 对平均延迟的影响

实验设缓存缺失为 42 ms,命中为 1 ms,查询发生在 t=[0, 10, 70, 80] 秒。第一次查询填充缓存并在 60 秒过期;70 秒时必须重新查询。

latencies = [42, 1, 42, 1] ms
average = (42 + 1 + 42 + 1) / 4 = 21.50 ms
hit_ratio = 2 / 4 = 50%

这不是说 TTL 越长越好:更长 TTL 降低解析成本,但也会延长地址迁移或故障切换的陈旧窗口。

三、运行环境与 Python 实验

运行环境:Python 3.10+ 与 Matplotlib;无需 root、网络连接或真实 DNS 服务器。实验读取仓库中的固定场景并重建 CSV 与图。

cd network-fundamentals-lab
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python src/dns_resolution_cache.py
expiry = -1
for at_second in [0, 10, 70, 80]:
    hit = at_second < expiry
    latency = 1 if hit else 42
    if not hit:
        expiry = at_second + 60

生成的 dns-cache-results.csv 明确记录了每次请求、HIT/MISS、延迟和过期时间。

四、报文层:为什么需要读 header

应用故障排查时,仅看 IP 不够。transaction ID 关联查询与响应,QR/RCODE 区分响应和错误,answer count 可以说明响应是否真正返回数据。实验包中的 C 程序解析固定响应 header:

cc -std=c11 -Wall -Wextra -O2 src/dns_packet_parser.c -o /tmp/dns_parser
/tmp/dns_parser
# txid=0x2a10 flags=0x8180 questions=1 answers=1

五、动画讲解:查询与缓存窗口

动画展示缓存 MISS 的递归路径,以及 TTL 有效期内直接返回的 HIT 路径。

六、工程检查清单

  • 记录查询域名、record type、resolver、TTL、RCODE 和完整耗时,而不是只记录最终 IP。
  • 将权威 DNS 变更的 TTL 降低窗口与发布/回滚时间安排在一起。
  • 排查延迟时区分应用 DNS 缓存、系统缓存和递归解析器缓存。
  • 线上观察可使用 dig example.com A +noall +answer +stats,但其结果不可替代固定实验。

负缓存:改对了记录,还是解析不到

上面算的都是成功响应的 TTL。但排障时最容易卡住的其实是另一半:失败的响应同样会被缓存。

当权威服务器返回 NXDOMAIN(域名不存在)时,这个「不存在」的结论也会被递归解析器记下来一段时间,这叫负缓存。关键在于它的时长不由记录的 TTL 决定——记录都不存在,哪来的 TTL——而是由该域 SOA 记录里的 minimum 字段控制。

dig SOA example.com +short
# ns1.example.com. admin.example.com. 2026080801 7200 3600 1209600 3600
#                                                                  ^^^^
#                                        最后这个字段就是负缓存时长(秒)

典型的场景是这样的:你新加了一条 A 记录,加之前手贱先查了一次,那次查询返回 NXDOMAIN 并被缓存。记录加好之后再查,还是解析不到——而且你会以为是记录没生效,于是反复检查 DNS 面板。实际上要等的是负缓存过期,时长由 SOA 的最后一个字段决定,很多默认配置是 3600 秒。

所以有个很实用的习惯:在记录还没配好之前,不要去查它。一次不必要的查询能给自己制造一小时的困惑。

TTL 之外还有好几层缓存

TTL 只约束递归解析器。从应用到权威服务器之间还有好几层,每一层都可能自己存一份,且不一定尊重 TTL。

  • 浏览器自己的 DNS 缓存:Chrome 有独立的解析缓存,时长通常是一分钟量级,与系统缓存无关。
  • 操作系统层:systemd-resolved、nscd、macOS 的 mDNSResponder 各有各的缓存策略。
  • 运行时/语言层:这一层最容易出事。JVM 在启用了安全管理器时,networkaddress.cache.ttl 的默认值是 -1,意思是永久缓存。一个长期运行的 Java 服务解析一次域名之后,即便对方 IP 换了、TTL 早就过期,它也会一直连旧地址,直到进程重启。

这解释了一类很典型的现象:切换服务器 IP 之后,大部分客户端很快就切过去了,少数几个死活连旧地址。不是 DNS 没生效,是那几个客户端在某一层把结果永久留下了。

排查顺序应该从外往内:先用 dig @8.8.8.8 确认权威数据对了,再用 dig(走本机默认解析器)看递归层,最后才怀疑应用自己。跳过前两步直接改应用配置,往往是在修一个不存在的问题。

还有一种情况会让上面所有排查都失效:本机走了透明代理。这类代理会给域名返回一个合成的假地址(常见于 198.18.0.0/15 这类保留段),自己在那个地址上收连接再按域名转发。此时你 dig 出来的 IP 和实际连接的目标毫无关系,任何基于解析结果的判断都不成立。我在我的 SSRF 防护把自己拦了里详细写过这个机制,以及它如何让一整套出网检查变得没有意义。

七、从固定实验映射到线上排障

固定实验的作用不是模拟所有 DNS 行为,而是把几个关键变量拆开。线上排障时,可以把观察结果先归到下面几类,再决定是否需要抓包、换 resolver 或检查权威 DNS 配置。

线上现象 优先检查 可能原因
首次访问慢,刷新后变快 递归解析耗时与本地缓存状态 缓存 MISS 后路径较长,后续 HIT 降低延迟
部分地区仍访问旧地址 记录 TTL、变更时间和 resolver 缓存 TTL 窗口内仍存在陈旧响应
偶发 NXDOMAIN 或 SERVFAIL RCODE、权威服务器健康和 DNSSEC 状态 权威侧异常、验证失败或中间解析器故障
同一域名解析出不同 IP 记录类型、CDN 策略和客户端地理位置 负载均衡、Anycast 或地理调度造成差异

八、读 CSV 时应该关注什么

dns-cache-results.csv 里最有用的不是单个延迟值,而是 HIT/MISS 与时间轴的对应关系。第 2 次查询只有 1 ms,是因为它落在第一次查询写入缓存后的 TTL 有效期内;第 3 次重新变成 42 ms,是因为 70 秒已经超过了 60 秒 TTL。

如果你把查询时间改成 [0, 10, 20, 30],命中率会变成 75%;如果改成 [0, 70, 140, 210],命中率会接近 0。这个简单变化能说明一个重要事实:DNS 缓存收益不只由 TTL 决定,也由用户访问间隔决定。高频访问域名更容易受益于缓存,低频访问域名即使 TTL 不短,也可能经常遇到 MISS。

FAQ

TTL 为零能否消除陈旧响应?

它会显著减少缓存收益且仍无法消除所有中间行为;可控发布通常使用有计划的低 TTL 窗口。

为什么这篇文章不用抓包?

目标是让数值可重复。固定报文字节足以学习 header,而真实抓包会混入操作系统、网络与权限差异。

References

下一篇从域名得到地址之后继续向下走,计算 CIDR 路由选择和 MTU 边界。

发表回复

向下探索