bigywater.tech
网络协议 · 68

DNS 缓存与 TTL:负缓存与过期时的行为

DNS 的快慢,一半靠缓存,一半靠 TTL 管着缓存什么时候失效。这篇在本机上看缓存的痕迹:公网解析器给出的 TTL 到底怎么变,负缓存(不存在的名字也会缓存)的时间由谁决定,命中缓存省下多少时间,本机操作系统自己又缓存了什么。有一半的实验结果和教科书上的整齐倒数不一样,我如实写。

101 ~ 300
权威 TTL 300 秒时,1.1.1.1 实测返回的 TTL 范围(不是整齐的倒数)
86400 秒
.invalid 的 NXDOMAIN 响应里 SOA 给出的负缓存时间
80.9 → 0.3 ms
本机 getaddrinfo 解析 example.net 第一次与之后几次的耗时

TTL 的倒数

实测 · 本机执行输出
    轮次(s)   1.1.1.1 返回的 TTL   8.8.8.8 返回的 TTL
        0      [300]            [300]
        2      [110]            [135]
        4      [296]            [300]
        6      [274]            [133]
        8      [101]            [135]
       10      [286]            [290]
       12      [300]            [123]
       14      [298]            [282]
  相邻两轮的差(应约等于实际间隔 2 秒加上查询耗时):1.1.1.1 [190, -186, 22, 173, -185, -14, 2];8.8.8.8 [165, -165, 167, -2, -155, 167, -159]

缓存命中返回的是剩余寿命,理论上逐渐减小。但公网解析器地址背后有很多缓存节点,每次问到的节点不同,TTL 来回跳。

过期与重取

实测 · 本机执行输出
    0s:60 5s:60 10s:60 15s:60 20s:60 25s:55 30s:60 35s:45 40s:60 45s:60 50s:60 55s:55 60s:8 65s:9 70s:39 75s:60 80s:60
  TTL 往上跳(= 缓存过期、重新向权威服务器取)的位置:[(25, 55, 60), (35, 45, 60), (60, 8, 9), (65, 9, 39), (70, 39, 60)];最大值 60
  每次往返时间(ms):[246, 188, 207, 190, 186, 194, 184, 182, 183, 180, 184, 190, 184, 193, 182, 193, 185]

60 秒 TTL 的 github.com,8.8.8.8 返回 60 / 55 / 45 / 8 / 9 / 39 …,没有清晰的倒数曲线。

命中与未命中

实测 · 本机执行输出
  这些随机名字的响应:[('NOERROR', [1800]), ('NOERROR', [1800]), ('NOERROR', [1800])] …(RCODE 和授权区 SOA 的 TTL)
  第一次(未命中):[305, 263, 270, 300, 292, 293, 275, 299];中位 292 ms
  第二次(命中):[286, 297, 300, 284, 217, 304, 284, 296];中位 291 ms;差值中位 1 ms

对 1.1.1.1 命中与否只差 1 ms,被 290 ms 的网络往返淹没。

负缓存

实测 · 本机执行输出
  ier….invalid → RCODE NXDOMAIN;授权区 SOA 记录的 TTL = 86400 秒,SOA 里的 minimum 字段 = 86400 秒;负缓存时间 = min(86400, 86400) = 86400 秒
  之后每 3 秒再问同一个名字,授权区 SOA 的 TTL 倒数:[86398, 86400, 86389, 86400](命中负缓存,剩余寿命在减少)
  对照 NODATA:example.net 的 SRV 记录 → RCODE NOERROR、回答 0 条、授权区有 SOA(TTL 1800,minimum 1800);隔 2 秒再问 3 次,SOA 的 TTL:[[1800], [1800], [1800]]——“名字存在但没有这种类型”也会被缓存

不存在的名字(NXDOMAIN)和名字存在但没有这种记录(NODATA)都带 SOA,缓存时间取 SOA TTL 与 minimum 的较小者。

本机缓存

实测 · 本机执行输出
  example.net(存在)连续 6 次:80.9,0.7,0.3,0.3,0.3,0.3 ms
  随机的不存在子名字 xzo….example.net 连续 4 次(NODATA / NXDOMAIN):2017.2,1.0,0.4,0.3 ms
  随机的 .invalid 名字连续 4 次:127.8,0.7,0.3,0.3 ms

操作系统缓存正向和否定答案,命中时 0.3~1 ms。

分步演示

缓存与 TTL
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。向 1.1.1.1 和 8.8.8.8 发了几百个普通 DNS 查询;这两个地址背后是多个缓存节点,TTL 观察值每次运行不同;第三节的“命中”只是第二次查询,不保证落在同一个节点
    • 没有验证:单个缓存节点内部的 TTL 倒数(需要能固定到同一个节点);解析器对 TTL 的上下限钳制(很多解析器会把过短或过长的 TTL 改掉);EDNS 客户端子网(ECS)对缓存的影响;预取(prefetch)和过期服务(serve-stale)的行为;getaddrinfo 第一次调用 2017.2 ms 的原因;本机缓存的 TTL 是否与 DNS 响应的 TTL 一致
    京ICP备2026067502号-1京公网安备11010502063487号