DNS、缓存与连接生命周期:改了地址为什么仍访问旧节点
本页接续 TCP 连接边界。DNS 回答名称与地址/记录的关系,不搬运已经建立的连接。协议参照 RFC 1034、RFC 2308 负缓存;Java 契约参照 JDK 17 InetAddress,原码固定 jdk-17.0.12+7,不把一种客户端的缓存策略泛化为全部运行库。
解析缓存与已连接 socket 是两个状态机
一次域名调用可能经过应用/库缓存、JVM InetAddress、系统名称服务、本地或递归解析器及权威 DNS。解析路径并非每次都完整走一遍:hosts、库自带解析器、代理代解析可能绕开某些层。dig 是另一个调用者,其结果不是 Java 进程当时使用的地址。
解析状态机按名称保存值与寿命;连接状态机在建连时选择一个地址,此后以已连接 socket 通信。时间线:t0 解析 A 并连接 → t30 权威改 B → t45 请求复用 A,没有解析 → t70 新连接可能还命中缓存 A → t90 新解析才得到 B。TTL=0 只影响相应缓存,不让池内连接自动迁到 B。
这也解释“降低 TTL 后立即切换”不可靠:老的递归缓存可能仍持旧 TTL,JVM 又可能按自己的安全属性缓存,已有连接无需再查 DNS。降 TTL 应先传播并验证,再执行迁移;加新地址不是事务型流量开关。
安全属性:配置入口和初始化时点都重要
networkaddress.cache.ttl 与 networkaddress.cache.negative.ttl 是 Java 安全属性,值 -1 为永不过期、0 为不缓存、正数为秒。默认值受 JDK/安全配置影响,不能假设所有 Java 17 正缓存一律 30 秒或永久。这里的“安全属性”指配置体系,不是 DNSSEC 验证;TLS 主机身份与业务授权也不由缓存 TTL 提供。
连续原码:InetAddressCachePolicy.java L93–L106,仅统一公共缩进。
String tmpString = Security.getProperty(cachePolicyProp);
if (tmpString != null) {
return Integer.valueOf(tmpString);
}
} catch (NumberFormatException ignored) {
// Ignore
}
try {
String tmpString = System.getProperty(cachePolicyPropFallback);
if (tmpString != null) {
return Integer.decode(tmpString);
}
} catch (NumberFormatException ignored) {正式名称先从 Security.getProperty 读;没有有效值才尝试实现特有的系统属性 fallback(本版本正缓存是 sun.net.inetaddr.ttl),不是同名 -Dnetworkaddress.cache.ttl。非法数字没有被当作新策略,而是走后续默认逻辑。负缓存有独立初始化路径,不能只改正缓存就期待 UnknownHostException 立刻消失。
策略在类初始化时读取。实验可在第一次解析前 Security.setProperty;生产应在进程启动前按部署安全配置管理,不能在已运行服务中随意改全局策略并声称立刻重载。打印 Security.getProperty 只是“现在的配置值”,还应以实际查名行为验证初始化时点、已有缓存与库自带缓存。本票不更改用户 shell、系统 hosts 或 Java 全局安全文件。
负缓存不是失败重试间隔
DNS 协议层的 NXDOMAIN 表示名称不存在,NODATA 表示名称存在但无所求类型,递归层负缓存遵守相应协议与记录寿命。Java 的 UnknownHostException 是运行库解析失败的表现,不能不经证据就一一对应到线上某种 DNS 响应;网络故障与系统解析器行为也可能进入失败路径。
负缓存省下反复查不存在名称的开销,却可能在记录修复后继续快速失败。此时每毫秒业务重试不一定产生新 DNS 请求,只会重复读负缓存。过短或禁用缓存则可能在流量高峰把故障放大到解析服务。修复要分开观测递归响应、Java 解析结果、失败次数及连接实际目的地,而非仅看“权威记录已恢复”。
InetAddress 如何发布正负结果
调用链:getAllByName → getAllByName0 → 地址缓存对象 → getAddressesFromNameService。解析中的对象与已缓存对象不同;同名并发请求可以等待正在进行的查名,而不是每个业务线程都发一份查询。过期集合使用单调时间判断缓存寿命,不是为每条记录建立一个线程定时器。
连续原码:InetAddress.java L850–L877。
int cachePolicy;
try {
inetAddresses = getAddressesFromNameService(host, reqAddr);
ex = null;
cachePolicy = InetAddressCachePolicy.get();
} catch (UnknownHostException uhe) {
inetAddresses = null;
ex = uhe;
cachePolicy = InetAddressCachePolicy.getNegative();
}
// remove or replace us with cached addresses according to cachePolicy
if (cachePolicy == InetAddressCachePolicy.NEVER) {
cache.remove(host, this);
} else {
CachedAddresses cachedAddresses = new CachedAddresses(
host,
inetAddresses,
cachePolicy == InetAddressCachePolicy.FOREVER
? 0L
// cachePolicy is in [s] - we need [ns]
: System.nanoTime() + 1000_000_000L * cachePolicy
);
if (cache.replace(host, this, cachedAddresses) &&
cachePolicy != InetAddressCachePolicy.FOREVER) {
// schedule expiry
expirySet.add(cachedAddresses);
}
}异常分支把地址设为 null 并选择负策略,后续仍可缓存“失败”,不是失败即删除。NEVER 删除当前解析占位对象;FOREVER 用 0L 作为内部标记而不加入过期集合,不能解释成“马上过期”。有限寿命使用秒到纳秒换算。replace(host, this, cachedAddresses) 必须匹配当前对象才发布,避免旧解析覆盖后来替换的状态;若去掉条件而无脑 put,会破坏并发交接。缓存修改没有任何 socket 迁移操作,这在代码边界上再次证明解析与连接解耦。
地址变化之后:排空而不是同时断开
下线 A 可拆成:停止向 A 分配新任务 → 发布/验证新地址 B → 让旧连接不再接新请求并处理在途 → 在期限内关闭剩余连接 → 核验 B 的真实成功率。单纯立即杀 A 会把所有旧连接同时变成重连/重试,B 尚未预热时容易出现洪峰。
最大连接年龄、空闲超时、健康探测回答不同问题:忙碌连接可能永不空闲;健康的旧节点未必应继续接流量;按连接年龄刷新才能给“仍被使用的旧连接”设边界,但在途排空也需 deadline。不要直接切断有不可判定写入的连接并把错误当作没执行。
假设 1 万 req/s,平均每连接服务 100 次请求,则粗略平均建连率 100/s;每请求新建则可能 1 万/s。缩短连接寿命换来拓扑收敛,却增加 TCP/TLS、端口、FD 与 CPU 压力。抖动刷新时刻、限速预热和总连接预算比全实例同秒过期更稳。有关 TLS 与池的完成边界见 HTTP/TLS。
多地址、切换预算与失败证据
多 A/AAAA 不等于公平轮询:地址排序、IPv4/IPv6 可达性、连接复用、客户端回退算法改变流量。一次 getAllByName 成功也不保证任何地址可连接;多地址更不提供多主写一致性。必须记录每次解析结果、选择/尝试的地址、实际 socket peer 与请求终态。
架构师不能简单把 TTL 最小值当 RTO,也不能把所有 TTL 机械相加。阶段可能重叠,也可能在关键时点串联:发现故障、控制面发布、旧缓存到期、新解析、建连、排空与业务恢复都有预算。没有最大连接年龄时,即使 DNS TTL 很短,也无法给旧节点完全退出设硬上限。秒级撤销需求应比较代理层路由、服务发现推送、服务端拒新与强制排空,注明它们的控制面依赖及失败时行为。
隔离实验:临时 hosts,不触碰系统解析
runner 的 dns 模式 只使用每个实验子进程私有的 -Djdk.net.hosts.file 临时文件,不修改 /etc/hosts。启动先设安全属性,再首次解析;正缓存 -1 时修改文件仍读旧值;负缓存 -1 时新增记录仍失败;正负 0 时观察即时新结果。
第三组先将名称解析为 127.0.0.1 建一个连接,再仅把临时记录改为文档专用地址 192.0.2.1:新解析读到新值,旧连接仍向 127.0.0.1 成功传输。不尝试连接 192.0.2.1,全部实际监听/连接都在 loopback。同时设置同名系统属性为 600、安全属性为 0,用行为验证它们不是同一入口。
这验证 JDK 名称服务缓存和持久 socket 的独立生命周期,不是权威/递归 DNS、真实 TTL 传播、DNSSEC 或 TLS 实验;这些真实网络实验本次未运行。私有 hosts 机制是本 JDK 的测试入口,不是跨 JVM 标准 API。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。