DNS解析链路与缓存层级:名字在哪一层被决定
先读 DNS 缓存与连接生命周期——那页是 JVM 正负缓存与"解析/连接两状态机"的唯一权威;本页往下走:名字从应用请求到权威应答要经过哪几层、每层在哪里缓存与失败、排障时怎么区分"哪一层出的错"。协议依据 RFC 1034 §4/§7、RFC 2308 负缓存(查询 2026-10-25);Linux 解析器配置就近引用 resolv.conf(5) 与 nsswitch.conf(5)。JDK 基线 jdk-17.0.12+7,本机实测 macOS 26.7.1 x86_64 + Oracle JDK 17.0.12+8。
一条名字要过几层
一次 InetAddress.getByName 的完整路径至少有五层,每层都可能给出不同答案:
| 层 | 谁实现 | 缓存 | 本机怎么看 |
|---|---|---|---|
| 应用/库缓存 | 框架或客户端自建 | 有,策略各异 | 看框架文档,不假设与 JVM 一致 |
| JVM 缓存 | InetAddress 正负缓存 | 有,安全属性控制 | DNS 缓存页 |
| 系统名称服务 | macOS mDNSResponder / Linux glibc+nsswitch | macOS 内建;Linux 可选 nscd/s systemd-resolved | macOS scutil --dns、dscacheutil;Linux resolv.conf、getent hosts |
| 递归解析器 | 运营商/公司 resolver(本机实测 resolver #1 指向局域网网关 192.168.5.1) | 有,按 TTL | dig @<resolver> 名字(另一个调用者,不是进程的证据) |
| 权威 DNS | 域名的授权服务器 | 无缓存,记录源头 | dig @权威 +norecurse |
hosts 文件在系统层最先命中:Linux 由 nsswitch.conf 的 hosts: files dns 规定顺序,macOS 由 mDNSResponder 合并 /etc/hosts 与查询。由此两条工程结论:改了 /etc/hosts 不影响已在 JVM 缓存里的旧值(两状态机分离);dig 拿到的是递归层的答案,既跳过 hosts,也跳过 JVM 缓存——它可以是排障线索,但不能当作"进程当时用的地址"。
JDK 选哪个名称服务
JVM 只有两个名称服务实现可选,选择发生在类初始化时:
原码连续摘录:InetAddress.java L1161–L1174。
private static NameService createNameService() {
String hostsFileName =
GetPropertyAction.privilegedGetProperty("jdk.net.hosts.file");
NameService theNameService;
if (hostsFileName != null) {
theNameService = new HostsFileNameService(hostsFileName);
} else {
theNameService = new PlatformNameService();
}
return theNameService;
}设了 -Djdk.net.hosts.file 就整体替换平台名称服务(PlatformNameService 才会调用操作系统解析库);没设则一切交给系统层。javadoc 同时规定"非存在的 hosts 文件按空文件处理"——实测行为是每次查名都抛 UnknownHostException,消息点名文件路径(… as hosts file … not found),配合 HostsFileNameService 对该文件的每次重读:这个文件不是启动时读一次,而是按查询重读(见实验节的时序观察)。JDK 层的 hosts 查找命中"精确名优先、别名跟随"的解析语义,与 /etc/hosts 的 IP 名 别名… 格式一致。
各层缓存的 TTL 从哪来
- 权威记录给出正缓存 TTL;负缓存 TTL 来自权威应答中的 SOA minimum(RFC 2308 §5,本票未跑真实递归,按协议推导)。
- 递归层遵守上述 TTL 逐条衰减,不会"提前忘记"。
- 系统层:macOS mDNSResponder 缓存没有公开的按条 TTL 配置;Linux 是否缓存取决于 nscd/systemd-resolved 是否启用(resolv.conf(5) 推导)。
- JVM 层:策略与默认值见 DNS 缓存页,本页不重复。
排障含义:一条记录"修复后多久收敛"由各层剩余寿命的最大值决定,不是任何单层 TTL;而"最坏叠加"是各层之和。没有一层可跳过验证——用进程自己的解析结果(日志里打印的实际连接地址)做收敛证据。
解析失败不是一个原因
UnknownHostException 在 JVM 层只有一个类,来源却有四类以上:hosts/hosts 文件配置错误、名称确实不存在(NXDOMAIN)、递归层故障(SERVFAIL/超时)、本机网络不可达。异常类名不指认根因。证据链按层采集,每层一个工具:
- 进程内:实际连接的 peer 地址、解析耗时(实验节打印的是本进程观察值);
- 系统层:
dscacheutil -q host -a name <名字>(macOS)或getent hosts <名字>(Linux),回答"系统给进程的答案"; - 递归层:
dig @<resolver> <名字>,对比 TTL 与应答; - 权威层:
dig @<权威> <名字> +norecurse,确认记录源头。
同一名字四层答案不一致时,层数越低越接近"业务该信的答案";修复后按同一顺序复验,而不是只看权威已改。
实验:私有 hosts 与本机解析器
公开 runner 的 dnschain 模式不触碰系统 hosts、不发真实 DNS 查询:每个 JVM 用私有 -Djdk.net.hosts.file 验证三件事——hosts 条目命中(chain.test → 127.0.0.1)、缺失名字抛 UnknownHostException(负 TTL 设 0,失败不缓存)、hosts 文件缺失时每个查名都失败且消息点名文件(与 createNameService 摘录的"按空文件处理"边界对应)。同时只读观察本机系统解析器:scutil --dns 列出 resolver #1 的 nameserver(实测 192.168.5.1,局域网网关),dscacheutil -q host -a name localhost 证明系统层 hosts 优先(实测返回 127.0.0.1)。在 TTL=0 下连续三次解析的耗时打印为单机观察值(hosts 文件按查询重读,毫秒级)——它证明文件重读的存在,不证明真实 DNS 延迟。
本实验没有运行:真实递归/权威查询、TTL 传播、DNSSEC、运营商劫持对照——这些需要外部网络,本次全部未跑、按协议文档推导。
架构师推演:切换预算要按层算(算例,非实测)
目标"权威记录修改后 30 秒内切换到新地址"。四段预算:权威与递归的剩余 TTL(改记录前先降到 30s,最坏还要等旧值到期);系统层缓存(macOS 不可配、Linux 看部署);JVM 缓存(安全属性控制,见缓存页);已建立连接(不受 DNS 影响,靠连接最大年龄/排空,见缓存页的排空预算)。三个方案对比:短 TTL 全局(简单,故障放大到解析服务,收敛=最慢层);服务发现推送(秒级,控制面是新故障域,推送失败要有回退);多地址客户端重试(对解析无要求,旧地址失败才有感知,恢复靠连接预算)。恢复责任:解析层归 DNS 运维,连接层归应用,两者预算必须分别验证——"TTL 改小了"不等于"旧节点不再有流量"。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。
来源与版本
- RFC 1034、RFC 2308(查询 2026-10-25):解析层级、负缓存 TTL 来源;resolv.conf(5)、nsswitch.conf(5):Linux 解析顺序(推导,macOS 侧以 scutil/dscacheutil 实测为准)。
- OpenJDK
jdk-17.0.12+7InetAddress.javaL1161–L1174(本页摘录):名称服务选择与 hosts 文件替换语义;本机运行为 Oracle JDK 17.0.12+8,构建号不同如实分记。 - 本机实验:Oracle JDK 17.0.12+8-LTS-286、macOS 26.7.1 x86_64,见实验 README 与证据记录;真实递归/权威 DNS、TTL 传播、DNSSEC 未实跑,按协议文档推导。