qinghuan
← 回到知识探索

计算机趣闻 / 动手实验

DNS 改了,为什么有人还在去旧地址?

用两台虚构解析器各自倒计时,看清权威记录更新、缓存剩余时间与下一次查询的区别。

展开本文目录
  1. 先动手看看
  2. 一边管原始记录,一边暂存答案
  3. TTL 是副本的使用期限,不是全球倒计时
  4. 同一秒查询,可以得到两个地址
  5. 还有两种情况,需要另外看规则
  6. 刷新网页,不等于清掉所有 DNS 缓存
  7. 记住这三点
  8. 别把例子用过头
  9. 自己试一小步
  10. 查原始资料
  11. 找一份资料继续练
开始前,会这些就够了
  • 知道域名是便于记忆的名字,连接网站还需要地址
遇到不熟悉的词,先看这 3 个解释
TTL
在缓存语境里,指一条数据允许保留或被当作有效副本的时间长度。到期后怎样失效、清理或重新读取,要看具体系统的过期规则和使用方式。
DNS(域名系统)
按域名查询地址等记录的系统。知道网站的名字后,可以先借它查找连接所需的信息。
递归解析器
替使用者查找 DNS 答案的服务,也常把查到的答案暂存起来供后续复用。
打开完整术语小抄 ↗

假设你把学习网站搬到新机器,已经把域名对应的地址改好。自己访问正常,同学却还被送到旧机器。先别急着认为某个人操作错了:替你们查地址的服务,可能各自保存着一份还没到期的旧答案。

DNS 可以帮助程序按名字查找地址等记录。本篇只看同一个名字的一条地址记录,用十秒钟的虚拟时间演示;这不是实际网站的等待时长,也没有向外部服务器发送查询。

改了地址,两张缓存便条会一起变吗?

A 在第 0 秒、B 在第 4 秒分别直接获取了旧地址,TTL 都是 10 秒。现在从第 4 秒开始;可以先推进 2 秒,再修改权威值,并分别查询 A 和 B。

当前虚拟时间:第 4 秒。按钮只推进这个数字,没有真实等待。

解析器 A

保存的地址
192.0.2.10
装入 / 到期
第 0 秒 / 第 10 秒
现在剩余 TTL
6 秒

仍可复用:下次查询会命中这份缓存。

最近一次查询在第 0 秒:重新获取,回答 192.0.2.10,当时剩余 TTL 为 10 秒。

解析器 B

保存的地址
192.0.2.10
装入 / 到期
第 4 秒 / 第 14 秒
现在剩余 TTL
10 秒

仍可复用:下次查询会命中这份缓存。

最近一次查询在第 4 秒:重新获取,回答 192.0.2.10,当时剩余 TTL 为 10 秒。

起点是第 4 秒:A 在第 0 秒装入,B 刚在第 4 秒装入,同样收到 10 秒 TTL。

这是内存中的虚构解析器,不联网、不查询或清理真实 DNS。到期后只有点击查询才获取新值,缓存命中不会延长到期时间。模型不含负缓存、serve-stale、预取、多层缓存和网络失败;刷新网页也不等于清空真实 DNS 缓存。

一边管原始记录,一边暂存答案

权威服务器负责给出某个范围内的正式 DNS 记录,可以先想成保管地址簿的人。递归解析器负责替使用者查找答案,也常把查到的结果暂存起来;这种副本就是缓存,目的是减少重复查找。

这里假设权威处的修改已经成功,而且每次重新查它都能拿到当前值。即便如此,解析器仍可能复用尚未到期的旧副本。更新权威值与更新这里每份缓存,是两件不同的事,不能只检查前者就断言大家的答案相同。

TTL 是副本的使用期限,不是全球倒计时

TTL 是记录可被缓存的时间长度,以秒计。我们的模型把收到答案的时刻加上 TTL,得到到期时刻;剩余时间则是到期时刻减去现在。缓存命中只会读出原答案,不会因为又有人问了一次,就重新赠送完整的十秒。

A 在第零秒直接查到旧地址,B 在第四秒才直接查到同一旧地址,都收到十秒 TTL。第六秒时,A 还剩四秒,B 还剩八秒。这里特意让两者各自从权威处获取;真实的多层缓存转交答案时,应关注收到的剩余 TTL,不能想成每经过一层都重新计满。

同一秒查询,可以得到两个地址

第六秒修改权威地址后,两个旧副本仍未到期。第十秒查询 A,它会重新获取新地址;同一秒查询 B,却还能命中旧地址。到第十四秒再问 B,它才在本例中重新获取。不同答案可以由不同装入时刻解释,不需要假设更新正沿一条固定路线传到全世界。

代码中的时间由我们直接给出,不会真的等待。HIT 表示用缓存,FETCH 表示在内存里读取当前权威值;两个 IP 都来自文档示例地址段,不代表真实服务。到期只是令旧副本不能再按本例规则复用,不会在无人查询时自动发起更新。

小实验 · python
TTL = 10
authority = "192.0.2.10"
caches = {"A": None, "B": None}


def query(name, now):
    entry = caches[name]
    hit = entry is not None and now < entry[1]
    if not hit:
        entry = (authority, now + TTL)
        caches[name] = entry
    remaining = entry[1] - now
    source = "HIT" if hit else "FETCH"
    print(f"t={now:02d} {name}: {source} {entry[0]} remaining={remaining}")


query("A", 0)
query("B", 4)
authority = "192.0.2.20"
print("t=06 authority: 192.0.2.20")
query("A", 6)
query("B", 6)
query("A", 10)
query("B", 10)
query("B", 14)

怎样运行:保存为 dns_ttl.py,用 Python 3.10 或更新版本运行:python dns_ttl.py。只操作内存和虚拟秒数,不联网、不等待。

运行后对照结果
t=00 A: FETCH 192.0.2.10 remaining=10
t=04 B: FETCH 192.0.2.10 remaining=10
t=06 authority: 192.0.2.20
t=06 A: HIT 192.0.2.10 remaining=4
t=06 B: HIT 192.0.2.10 remaining=8
t=10 A: FETCH 192.0.2.20 remaining=10
t=10 B: HIT 192.0.2.10 remaining=4
t=14 B: FETCH 192.0.2.20 remaining=10

还有两种情况,需要另外看规则

负缓存保存的是“这个名字不存在”或“没有所问类型的数据”等否定结果,它也有缓存期限。因此,刚补上一条原本不存在的记录,不一定能用本文只有成功答案的程序解释;RFC 2308 专门说明这类规则。

serve-stale 则是在无法成功刷新等规定条件下,允许解析器暂时使用过期数据,以改善故障时的可用性。RFC 8767 说明了这项例外。本例没有负缓存、预取或 serve-stale,也没有网络失败,所以不能据此保证真实世界到第十秒必定换成新答案。

刷新网页,不等于清掉所有 DNS 缓存

点击网页刷新,不等于把沿途解析器保存的答案全部清空。排查时应先分清看到的是旧地址,还是地址正确但页面内容仍旧;DNS 地址缓存与网页内容缓存,保存的是不同东西。把其中一个层次的变化,当成另一个层次也已更新,容易找错原因。

更有用的记录包括:问了哪台解析器、何时询问、返回哪个地址、剩余 TTL 是多少,以及权威处当前给出的记录。它们能帮助解释这一次观察。本文互动只让你改变虚拟时间与内存中的权威值,没有连接、修改或清理电脑的真实 DNS 配置。

记住这三点

  • 权威记录已经更新,不代表每份尚未到期的缓存都同时改变。
  • 看剩余 TTL 和装入时刻;缓存命中不会把使用期限重新计满。
  • 本例在到期后的下一次查询才重取,真实系统还需考虑负缓存、serve-stale 等规则。

别把例子用过头

  • 十秒只是教学数值,不是 DNS 更新的通用承诺;两台解析器都直接从同一权威值获取。
  • 模型不含负缓存、serve-stale、预取、多层转发或权威服务器之间的同步过程。
  • 网页刷新不等于清空 DNS 缓存;得到新地址也不能证明网页内容一定已更新。

自己试一小步

若 B 更晚才第一次装入缓存,两个使用者会在什么阶段看到不同答案?

  1. 先在纸上另定 B 的装入时刻,保持 TTL 与 A 的装入时刻不变。
  2. 在两台缓存都到期前修改权威值,分别计算每次查询的剩余时间。
  3. 只增加一次缓存命中,再检查它有没有改变原先的到期时刻。

做到这些,就算完成

  • 是否按收到答案的时间分别计算,而不是从修改权威值时一起起算?
  • 是否区分缓存到期与真的发起下一次查询?
  • 是否明确纸面模型没有覆盖负缓存或刷新失败后的过期回答?

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。

找一份资料继续练

先选一项,按下面的范围继续学;不必从头读完整门课程。