深蓝黑背景封面:右侧大标题「域名到 IP,递归和迭代各走哪段」,副标题「本地缓存、根、顶级、权威:解析链路全景」;左侧原创示意图——一条阶梯式问路路线:客户端把问题整包托付给本地 DNS(递归),本地 DNS 依次拜访根、顶级、权威三座服务器塔(迭代),每座塔只指下楼方向不给最终答案,最后权威塔交出 IP;左上标注「面试经典 · 网络」,右上标注「Day 09 · 11-06」 图 1:本文封面,客户端只问一次,本地 DNS 替它跑完全程。

先说明属性:这是一篇历史笔试题复盘,题目出自本地历史笔试资料,与任何公司的当前招聘无关。题面:在浏览器输入一个域名,到拿到 IP 为止,DNS 解析走了哪些步骤?这道题的标准答案是一条链路,但面试官真正想听的是你能不能把递归和迭代这两个词落到具体的角色上,以及能不能解释「为什么改了域名记录,有人看到的还是旧 IP」。

第一步:客户端自己先翻两遍兜。

解析不是从网络开始的。操作系统先查 hosts 文件——本地写死的映射优先级最高;再查系统级 DNS 缓存——不久前解析过的域名直接复用结果。两级本地查找都落空,才把查询发给配置好的本地 DNS 服务器(通常是运营商或公司内网的递归解析器)。注意客户端与本地 DNS 之间的约定是递归:客户端把问题整包托付出去,本地 DNS 必须给出最终答案或明确错误,不许回一句「你去问别人」。1987 年的 RFC 1034 把这套分工定了下来,至今没变。

第二步:本地 DNS 替你跑完迭代链。

本地 DNS 手上没有答案时,它替客户端去逐级问,这一段的约定是迭代:每一级服务器只回答「下一级在哪」,不替你跑腿。顺序固定:先问根服务器,根不存任何具体域名的答案,只告诉你对应的顶级域服务器在哪;再问顶级域服务器(比如管 .com 的那些),它告诉你这个域名的权威服务器在哪;最后问权威服务器——它是域名主人自己配置的真相来源,给出真正的 A 记录。结果沿原路返回,并且层层按 TTL 缓存:TTL 倒计时内任何人再问,都直接甩缓存答案。

把链路写成代码跑一遍。

下面这份模拟实现 hosts、系统缓存、本地 DNS 递归、根/顶级/权威迭代与 TTL 缓存的完整行为,包括「权威改了记录但缓存没过期」的经典桥段,用 Node.js 22.15.0 直接运行(node dnschain.js):

'use strict';

// 各级服务器:根和顶级只「指路」,权威给最终答案
const root = { name: '根服务器', answer: () => ({ referral: '顶级域服务器(.com)' }) };
const tld = { name: '顶级域服务器(.com)', answer: () => ({ referral: '权威服务器(example.com)' }) };
let authoritativeRecord = { name: 'www.example.com', ip: '203.0.113.10', ttl: 300 };
const authoritative = { name: '权威服务器(example.com)', answer: () => ({ record: { ...authoritativeRecord } }) };

// 本地 DNS:替客户端走完迭代链(递归服务),按 TTL 缓存
function makeLocalDNS(log) {
  const cache = new Map(); // qname -> { ip, expiresAt }
  return {
    resolve(qname, now) {
      const hit = cache.get(qname);
      if (hit && now < hit.expiresAt) {
        log.push(`本地 DNS 缓存命中:${qname} -> ${hit.ip}(TTL 剩 ${hit.expiresAt - now}s,不问任何人)`);
        return hit.ip;
      }
      log.push('本地 DNS 缓存未命中,开始迭代问询');
      log.push(`问${root.name}:它不存答案,只指路 -> ${root.answer(qname).referral}`);
      log.push(`问${tld.name}:继续指路 -> ${tld.answer(qname).referral}`);
      const r = authoritative.answer();
      log.push(`问${authoritative.name}:给出答案 ${r.record.ip}(TTL=${r.record.ttl}s)`);
      cache.set(qname, { ip: r.record.ip, expiresAt: now + r.record.ttl });
      return r.record.ip;
    },
  };
}

// 客户端:先 hosts,再系统缓存,最后托付给本地 DNS(递归)
function makeClient(localDNS, log) {
  const hosts = new Map([['printer.local', '10.0.0.8']]);
  const sysCache = new Map();
  return {
    resolve(qname, now) {
      log.push(`--- 客户端解析 ${qname}(t=${now}) ---`);
      if (hosts.has(qname)) { log.push(`hosts 命中:${hosts.get(qname)}`); return hosts.get(qname); }
      const hit = sysCache.get(qname);
      if (hit && now < hit.expiresAt) { log.push(`系统缓存命中:${hit.ip}`); return hit.ip; }
      log.push('hosts 与系统缓存都没有,托付给本地 DNS(递归:它必须给我最终答案)');
      const ip = localDNS.resolve(qname, now);
      sysCache.set(qname, { ip, expiresAt: now + 60 });
      return ip;
    },
  };
}

const trace = [];
const localDNS = makeLocalDNS(trace);
const client = makeClient(localDNS, trace);
client.resolve('printer.local', 0);
client.resolve('www.example.com', 10);  // 首次:走完五级链路
client.resolve('www.example.com', 100); // 系统缓存过期,本地 DNS 缓存命中
authoritativeRecord = { name: 'www.example.com', ip: '203.0.113.99', ttl: 300 };
console.log('权威服务器把记录改为 203.0.113.99');
client.resolve('www.example.com', 200); // 本地 DNS TTL 未过:仍回旧答案
client.resolve('www.example.com', 400); // TTL 过期:拿到新答案
console.log(trace.join('\n'));

实测输出:

权威服务器把记录改为 203.0.113.99
--- 客户端解析 printer.local(t=0) ---
hosts 命中:10.0.0.8
--- 客户端解析 www.example.com(t=10) ---
hosts 与系统缓存都没有,托付给本地 DNS(递归:它必须给我最终答案)
本地 DNS 缓存未命中,开始迭代问询
问根服务器:它不存答案,只指路 -> 顶级域服务器(.com)
问顶级域服务器(.com):继续指路 -> 权威服务器(example.com)
问权威服务器(example.com):给出答案 203.0.113.10(TTL=300s)
--- 客户端解析 www.example.com(t=100) ---
hosts 与系统缓存都没有,托付给本地 DNS(递归:它必须给我最终答案)
本地 DNS 缓存命中:www.example.com -> 203.0.113.10(TTL 剩 210s,不问任何人)
--- 客户端解析 www.example.com(t=200) ---
hosts 与系统缓存都没有,托付给本地 DNS(递归:它必须给我最终答案)
本地 DNS 缓存命中:www.example.com -> 203.0.113.10(TTL 剩 110s,不问任何人)
--- 客户端解析 www.example.com(t=400) ---
hosts 与系统缓存都没有,托付给本地 DNS(递归:它必须给我最终答案)
本地 DNS 缓存未命中,开始迭代问询
问根服务器:它不存答案,只指路 -> 顶级域服务器(.com)
问顶级域服务器(.com):继续指路 -> 权威服务器(example.com)
问权威服务器(example.com):给出答案 203.0.113.99(TTL=300s)

t=200 那一幕就是「改了记录为什么不生效」的标准解释:权威服务器已经把 IP 改成 203.0.113.99,但本地 DNS 手里的旧记录 TTL 还剩 110 秒,它继续用旧答案,直到 t=400 缓存过期才拿到新值。TTL 是性能与 freshness 的交易:定长了变更生效慢,定短了权威服务器被打爆。

链路分解图:上方完整链路五步——客户端先查 hosts 与系统缓存,未命中把查询托付给本地 DNS 服务器(递归:本地 DNS 必须给出最终答案或错误);本地 DNS 从根服务器问起,根只回答顶级域在哪,顶级只回答权威服务器在哪,权威给出真正的 A 记录(迭代:每一级只指方向);结果沿原路返回并层层按 TTL 缓存。中部三栏讲各级职责与 why:根与顶级只做指路所以能扛全球查询;权威是域名主人自己管的真相来源;本地 DNS 的缓存是「改记录不生效」的元凶——旧记录 TTL 没过,它就一直用旧答案。底部提示:排查 DNS 问题先分清你问的是哪一级,dig 加 trace 能把这条链完整走一遍 图 2:递归只有一跳,迭代才有四级。

可能的追问。

「根服务器为什么只指路?」它要扛全互联网的查询入口,只指路才能把单点压力压到最低;层级委托正是 RFC 1034 的核心设计。「递归和迭代一句话区分?」递归是「你必须给我答案」,发生在客户端与本地 DNS 之间;迭代是「你只告诉我下一站在哪」,发生在本地 DNS 与各级服务器之间。「怎么亲眼看这条链?」dig +trace 域名 从根开始把每一级的回答打出来,是验证理解的最快方式。

模型服务端点迁移、多机房调度都压在 DNS 上,看懂这条链,「切流后部分用户还在访问旧机房」就不再是玄学。你排查过 DNS 缓存导致的「改了不生效」吗?最后是等 TTL 熬过,还是手动清了缓存?欢迎聊聊。