浏览器输入一个域名之后,第一个可能阻塞请求的环节不是 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。

二、手算 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
五、动画讲解:查询与缓存窗口
六、工程检查清单
- 记录查询域名、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
- RFC 1034: Domain Names – Concepts and Facilities
- RFC 1035: Domain Names – Implementation and Specification
下一篇从域名得到地址之后继续向下走,计算 CIDR 路由选择和 MTU 边界。