我的服务通过域名调用下游,下游因为故障切换换了 IP,我的服务却还死死连着那个已经下线的旧 IP 一直报连接失败,重启之后才恢复,原来是 DNS 解析结果被缓存了很久不刷新:一次 DNS 缓存把动态映射固化、抵消了域名这层解耦的深度复盘
那次"下游明明好好的,我却连不上、重启才恢复"的诡异故障,让我重新认识了 DNS 缓存。我的服务通过域名 api.downstream.com 调用一个下游服务。某天下游发生了故障切换:原来的节点挂了,运维把域名指向了新的健康节点(DNS 记录里的 IP 变了)。按理说,我用的是域名,IP 变了我应该自动跟着连到新 IP 才对。可现实是:我的服务一直在连那个已经下线的旧 IP,疯狂报 Connection refused/超时,怎么都连不上;而新部署的、别的服务却一切正常;直到我把服务重启,它才重新解析域名、连上新 IP、恢复。复盘这件事,我才彻底搞懂,后背发凉:问题出在 DNS 解析结果被缓存了、而且缓存了很久不刷新。用域名访问服务,要先经过 DNS 解析把域名翻译成 IP;而这个"域名→IP"的解析结果会被缓存(在操作系统、JVM、应用层多个层面),避免每次请求都去查 DNS;关键的坑在于:JVM(Java)在某些配置下对成功的 DNS 解析结果缓存时间极长、甚至永久缓存(老版本开启安全管理器时 networkaddress.cache.ttl 默认 -1 即永不过期);所以当下游 IP 变了,我的服务不知道——它还守着启动时缓存的那个旧 IP,一直往那个已下线的地址连;我用域名,本是为了"IP 可以随便变,我不用改、自动跟上"——可过久的 DNS 缓存,把这层"动态映射"固化成了"一次解析、长期不变",彻底抵消了域名带来的灵活性。根本原因是:DNS 解析结果被(JVM/系统/应用层)长时间缓存,下游 IP 变更后我的服务不感知、仍连缓存的旧 IP;而我误以为"用了域名 IP 就会自动跟着变",忽视了 DNS 缓存的 TTL 会把这层动态映射固化。问题的根,是 DNS 缓存——解析结果被长时间(JVM 甚至永久)缓存,下游 IP 变更后不刷新、仍连旧 IP;根源是误以为用域名就自动跟随 IP 变化,忽视了缓存 TTL 把动态映射固化了。这篇就把这次"DNS 缓存"的坑,从头到尾复盘一遍。
故障现场:IP 变了,服务却还连着旧 IP
问题在于 DNS 解析结果被长时间缓存、不随 IP 变更刷新:
# 现象时间线:
1. 我的服务启动, 解析 api.downstream.com → 1.2.3.4(当时的健康节点), 缓存这个结果;
2. 下游故障切换: 1.2.3.4 节点挂了, 运维把DNS记录改成 → 5.6.7.8(新健康节点);
3. 我的服务: 还守着缓存里的 1.2.3.4, 一直往这个【已下线】的IP连 → Connection refused/超时;
4. 别的新启动的服务: 解析时拿到新的 5.6.7.8, 一切正常;
5. 我把服务重启 → 重新解析 → 拿到 5.6.7.8 → 恢复。
→ 结论: 我的服务"被缓存的旧IP"困住了。
# 为什么会这样(DNS缓存的多个层面):
# 用域名访问 = 先把"域名"通过DNS解析成"IP", 再连那个IP;
# 为了不每次都查DNS, 解析结果会被缓存在多个层面:
# - 操作系统/本地DNS缓存 (有TTL);
# - JVM 进程内缓存 (java.net.InetAddress):
# * 成功解析: networkaddress.cache.ttl 控制;
# 老JVM在开启SecurityManager时默认 -1(永久缓存!), 否则一般默认30秒;
# * 失败解析: networkaddress.cache.negative.ttl, 默认10秒;
# - 应用/连接池/HTTP客户端层面可能还各自缓存;
# → 我的JVM把 1.2.3.4 缓存了很久(甚至到进程重启才失效), 完全不知道IP已经变成 5.6.7.8。
# DNS记录本身有TTL(权威服务器告诉你"这条记录可缓存多久"), 但:
# - 各级缓存"是否尊重"这个TTL、缓存多久, 取决于各自的实现和配置;
# - JVM 那层尤其容易"缓存过久", 是Java服务最常踩的;
# - 故障切换/扩缩容/迁移这些"IP会变"的场景, 就会被过久的缓存坑到。
★ 核心: 域名→IP 是一层"会变的映射", DNS缓存为了性能保存了它的快照;
缓存太久 → IP变了不感知 → 连旧IP失败; 用域名本想"IP可变我自动跟上",
却被"过久的缓存"把这层动态映射固化了。要设置合理的缓存TTL、让它能及时刷新。
看着"别的服务都好、就我这个连不上、一重启就好"的诡异现象,我又困惑又恍然:"我用域名,不就是图它'IP 变了我不用动'吗?怎么 IP 真变了,我反而被旧 IP 困住、还得重启?原来是 DNS 解析结果被缓存死了,根本没去重新解析。"这个坑最隐蔽的地方在于:它平时完全无感——IP 不变时,缓存让解析更快,毫无问题;只有在"IP 发生变更"(故障切换、扩缩容、迁移)这种低频但关键的时刻才爆发;而且"重启能好"这个特征极具迷惑性,容易让人误判成别的问题(还以为是偶发,重启了事)。下面就来拆解,DNS 缓存到底该怎么治。
第一件事:搞懂 DNS 缓存与 TTL
我顺着这次事故,把 DNS 解析、缓存和域名这层间接的意义彻底理清了。
DNS 缓存怎么回事? 为什么会连旧IP? 域名这层间接是干嘛的?
【核心: 域名→IP是一层"为解耦而设、可动态变化"的映射; DNS解析结果会在系统/JVM/应用多层缓存(有TTL),
缓存太久会在IP变更时连旧IP; JVM尤其易缓存过久(老版本可能永久); 要设合理TTL让缓存能及时刷新】
1. 域名这层"间接"是为了什么:
- 你连的是"域名"(逻辑名), 不是写死的IP(物理地址);
- 好处: 后端IP可以自由变(扩容、换机、故障切换、迁移), 而调用方不用改代码;
- 这是一层"解耦": 把"叫什么(稳定)"和"在哪(可变)"分开。
2. DNS缓存为什么存在、有什么风险:
- 每次请求都查DNS太慢, 所以解析结果被缓存(系统、JVM、应用层多级);
- 缓存有TTL(存活时间): TTL内直接用缓存的IP, 过期才重新解析;
- 风险: 缓存期间IP变了, 你不知道, 还连旧IP → 这正是上面的故障。
3. 各层缓存(以Java为例, 最常踩):
- DNS记录自带TTL(权威服务器设定, 如60s/300s)——建议值;
- 操作系统/本地缓存——一般尊重TTL;
- JVM(InetAddress): networkaddress.cache.ttl
* 老JVM+SecurityManager: 默认 -1(永久缓存!)← 最坑;
* 否则: 通常默认30s(JVM版本而异);
失败缓存 networkaddress.cache.negative.ttl 默认10s;
- 连接池/HTTP客户端: 可能持有长连接、或自己缓存解析, 也要考虑。
4. 正解方向:
① 给JVM设合理的DNS缓存TTL:
java.security.Security.setProperty("networkaddress.cache.ttl", "30"); // 30秒
或 -Dsun.net.inetaddr.ttl=30; 别用 -1(永久);
② 别在代码里硬缓存解析出的IP(自己缓存比JVM还死);
③ 长连接场景: IP变了已建立的连接不会自动迁移, 要配合健康检查/重连/连接池剔除;
④ 用支持TTL感知、能刷新的DNS解析/客户端; 重要场景靠LB(连LB的稳定VIP, 由LB转发到后端)
把"后端IP变化"挡在LB之后, 自己只连稳定的入口。
5. 本质: 不要把"一个会变的映射"缓存得比它的"变化周期"还久
- 域名→IP 是为"可变"而生的映射; 缓存它就要尊重它"会变"的本性、设合理TTL;
- 缓存太久, 等于把"动态"当"静态", 抵消了域名解耦的好处。
一句话: 域名→IP是为解耦而设的可变映射, DNS解析结果多层缓存(有TTL); 缓存太久(JVM尤其易永久)会在
IP变更时连旧IP; 要设合理的缓存TTL(别用-1)、别硬缓存IP、长连接配重连、靠LB稳定入口挡住后端变化。
这套认知,是整个坑的根。域名这层间接:你连的是逻辑名而非写死的 IP,好处是后端 IP 可自由变而调用方不用改——把"叫什么(稳定)"和"在哪(可变)"解耦。DNS 缓存的风险:为性能缓存解析结果(多级、有 TTL),缓存期间 IP 变了不知道、还连旧 IP。各层缓存:DNS 记录 TTL、系统缓存、JVM(老版本+SecurityManager 可能永久缓存,最坑)、连接池/客户端。正解方向:给 JVM 设合理 cache.ttl(别用 -1)、别硬缓存 IP、长连接配健康检查/重连、靠 LB 稳定 VIP 挡住后端变化。本质:别把"会变的映射"缓存得比它的"变化周期"还久,否则把动态当静态、抵消域名解耦的好处。一句话:域名→IP 是为解耦而设的可变映射,DNS 解析结果多层缓存(有 TTL);缓存太久(JVM 尤其易永久)会在 IP 变更时连旧 IP;要设合理的缓存 TTL(别用 -1)、别硬缓存 IP、长连接配重连、靠 LB 稳定入口挡住后端变化。
第二件事:正解——设合理 TTL、别硬缓存 IP、靠 LB 稳定入口
知道了缓存太久是祸根,正解就清楚了:让"域名→IP"这层映射保持它该有的"可刷新"。
// 正解1: 给 JVM 设合理的 DNS 缓存 TTL(别让它永久缓存)
// 方式A: 代码里设置(尽早, 在任何网络调用之前)
java.security.Security.setProperty("networkaddress.cache.ttl", "30"); // 成功解析缓存30秒
java.security.Security.setProperty("networkaddress.cache.negative.ttl", "5"); // 失败解析缓存5秒
// 方式B: JVM参数 -Dsun.net.inetaddr.ttl=30
// 方式C: 在 $JAVA_HOME/jre/lib/security/java.security 里配 networkaddress.cache.ttl=30
// 关键: 绝对别用 -1(永久缓存); 按下游IP变化频率和可接受的切换延迟设, 常用 10~60 秒。
// 正解2: 别在应用代码里"硬缓存"解析出来的IP(那比JVM缓存还死)
// ✗ 错误: 启动时解析一次, 存成静态变量一直用
// static String downstreamIp = InetAddress.getByName("api.downstream.com").getHostAddress();
// ✓ 正确: 每次用域名, 让下层(带合理TTL的解析)去管缓存; 别自己固化IP。
// 正解3: 长连接场景——IP变了, 已建立的连接不会自动迁移, 要主动剔除/重连
// - 连接池配健康检查: 定期探测连接是否可用, 失败就剔除、重新解析建连;
// - 设连接最大存活时间(maxLifetime): 定期重建连接, 顺带重新解析DNS;
// - 别让一条长连接"焊死"在某个旧IP上。
// 正解4: 用稳定入口(LB/服务网格)把"后端IP变化"挡在外面(架构层最优)
// - 自己连一个"稳定不变的入口"(LB的VIP、K8s的Service ClusterIP、网关域名);
// - 后端Pod/实例IP怎么变, 由LB/Service去感知和转发, 你这一侧的"目标"始终稳定;
// - 这样把"IP会变"的复杂性收敛到基础设施层, 业务侧不必操心DNS缓存。
// 正解5: 云SDK/HTTP客户端注意各自的解析缓存
// - 有些客户端(如旧AWS SDK)自己缓存DNS, 要按其文档配置刷新;
// - 用支持TTL感知、定期刷新的解析器(如dns-java、客户端内置的refresh)。
// 核心: 让"域名→IP"映射保持可刷新——JVM设合理TTL(别-1)、别硬缓存IP、长连接配重连/健康检查、
// 架构上靠LB/Service稳定入口把后端IP变化挡在基础设施层。
这套正解的关键,是让"域名→IP"这层为"可变"而生的映射,真正保持它的"可刷新",别被缓存固化。设合理 JVM TTL:networkaddress.cache.ttl 设 10~60 秒,绝不用 -1——这正是本次该做的。别硬缓存 IP:别在代码里把解析出的 IP 存成静态变量,那比 JVM 缓存还死。长连接配重连:IP 变了已建连接不会自动迁移,要靠健康检查剔除、设连接最大存活时间定期重建。靠 LB 稳定入口(架构最优):自己连 LB 的 VIP/K8s Service 这类稳定入口,把"后端 IP 变化"的复杂性收敛到基础设施层。
第三件事:其他几个"缓存了会变的东西却不刷新"的坑
顺着这次 DNS 缓存,我把"缓存动态信息却忘了失效/刷新"的几类坑也一并理了:
几类"缓存了会变的东西却不刷新"的坑:
坑1: 缓存了配置/开关却不热更新——配置中心改了, 服务还用启动时加载的旧配置, 重启才生效;
正解: 监听配置变更、定期拉取、支持热更新。
坑2: 缓存了下游服务的"能力/路由"信息——下游升级/迁移了, 本地缓存的路由还是旧的;
正解: 用注册中心+订阅变更; 别长期缓存服务发现结果。
坑3: 缓存了token/证书却不管过期——token过期了还在用 → 401; 证书轮换了还用旧的;
正解: 关注有效期、提前刷新、失败时重新获取。
坑4: 连接池里的连接"焊死"在旧IP/旧后端(上文长连接)——
正解: 连接最大存活时间 + 健康检查, 定期重建。
坑5: 缓存了"权限/角色"却不失效——用户权限被收回了, 缓存还放行 → 越权(安全相关, 但此处只谈失效);
正解: 权限变更要让缓存失效或设短TTL。
坑6: CDN/浏览器缓存了静态资源不更新——发版了用户还看旧JS/CSS;
正解: 文件名加hash(指纹), 内容变URL就变, 自然不命中旧缓存。
共同的根: 缓存的本质是"保存一份数据的副本以加速访问"; 但凡被缓存的数据"会变化",
缓存的副本就会与"源头的真相"脱节; 所以缓存一个会变的东西, 必须配一套"失效/刷新策略"
(TTL、订阅变更、版本指纹、主动失效)——只缓存不失效, 等于把"过去的快照"当"现在的真相"。
这些坑看似不同,根却是同一个:缓存的本质是"保存一份会变数据的副本以加速";而但凡被缓存的东西"会变化",这份副本就注定会和源头的真相脱节;所以凡缓存会变的东西,必须配一套"失效/刷新"策略(TTL、订阅、版本指纹、主动失效)。认清这个根("缓存动态数据必须有失效机制,只缓存不刷新=把旧快照当真相"),才不会被各种"缓存陈旧"的坑反复绊倒。
第四件事:DNS 缓存配置与场景——两张对照表
我把 JVM DNS 缓存的关键配置、以及不同场景该怎么处理,整理成对照表,贴在了团队的服务接入规范里:
| 配置项 | 含义 | 建议 |
|---|---|---|
| networkaddress.cache.ttl | 成功解析缓存秒数 | 设 10~60,别用 -1(永久) |
| networkaddress.cache.negative.ttl | 失败解析缓存秒数 | 5~10,别太长 |
| -Dsun.net.inetaddr.ttl | JVM 参数等价设置 | 同上,30 左右 |
| DNS 记录 TTL | 权威服务器建议缓存时长 | 会变的设短(60s) |
| 连接池 maxLifetime | 连接最大存活 | 设上限,定期重建顺带重解析 |
| 硬编码 IP / 缓存 IP | — | 禁止,失去域名灵活性 |
| 场景 | 处理 |
|---|---|
| 下游会故障切换/扩缩容 | JVM 设短 TTL + 长连接重连 |
| 调用走 LB | 连 LB 稳定 VIP,后端变化交给 LB |
| K8s 集群内调用 | 用 Service ClusterIP/域名,稳定 |
| 用云 SDK | 查其 DNS 缓存配置,设刷新 |
| 长连接(数据库/gRPC) | 健康检查 + 最大存活 + 重连 |
这两张表的核心,第一张是JVM 的 networkaddress.cache.ttl 绝不能是 -1,要设成一个合理的秒数,让缓存能定期刷新;第二张是按场景选对策略,而架构上最优的是"连稳定入口(LB/Service)",把后端 IP 变化挡在基础设施层。记住一条:用域名是为了"IP 可变我自动跟上",那就别让缓存(尤其永久缓存)把这份"可变"给冻住。
第五件事:关于 DNS 与域名解析的几组容易想当然的认知
这次事故也让我厘清了几组关于 DNS 的、容易想当然的概念:
| 直觉以为 | 实际上 |
|---|---|
| 用了域名,IP 变了就自动跟上 | 解析结果被缓存,缓存期内仍连旧 IP |
| DNS 解析每次都是实时的 | 多层缓存,有 TTL,不是每次都查 |
| JVM 默认 DNS 缓存时间很短 | 老版本+SecurityManager 可能永久缓存 |
| 重启能好就是偶发问题 | "重启好"恰恰暗示是进程内缓存(如 DNS) |
| 设了 DNS 记录 TTL 就够了 | 各级缓存是否尊重、缓存多久看各自配置 |
| 短连接没 DNS 缓存问题 | JVM 进程级缓存照样影响短连接 |
| 缓存只影响性能不影响正确性 | 缓存陈旧会连错地方,影响可用性/正确性 |
这张表里,我栽的是第一行和第三行:以为"用了域名,IP 变了我自动跟上",又不知道"JVM 可能把解析结果缓存得极久甚至永久",结果被旧 IP 死死困住。厘清这些,核心是一个意识:"用域名"提供的灵活性(IP 可变),只有在"解析结果能及时刷新"时才成立;DNS 是有多层缓存和 TTL 的,要主动管好这个缓存(尤其 JVM 那层),别让它把动态映射固化成静态。
第六件事:用域名调用 / 处理会变的目标时,我现在的自检习惯
现在每当我的服务要通过域名调用一个 IP 可能变化的下游,我都会先按这张图问自己:
这张图的精髓,是"下游IP会变就设短TTL别硬缓存IP、长连接配重连、优先连稳定入口"。先问IP 会不会变、再看JVM TTL 是不是太长、有没有硬缓存 IP、长连接配没配重连、能不能连稳定入口。这套习惯,让我从"用了域名就以为高枕无忧"变成了"主动管好 DNS 缓存的刷新"——核心始终是:域名→IP 是为解耦而设的可变映射,解析结果多层缓存有 TTL;缓存太久会在 IP 变更时连旧 IP;要设合理 TTL(别用 -1)、别硬缓存 IP、长连接配重连、靠 LB 稳定入口挡住后端变化。
我立下的几条规矩
这场"IP 变了却被旧 IP 困住"的事故,换来了我做服务调用时,刻进骨子里的几条铁律:
- 域名→IP 是一层为"解耦、IP 可变"而设的映射;DNS 解析结果会在系统/JVM/应用多层缓存。
- JVM 对成功解析可能缓存极久甚至永久(networkaddress.cache.ttl=-1),是 Java 服务最常踩的坑。
- 给 JVM 设合理的 DNS 缓存 TTL(10~60 秒),绝不用 -1。
- 别在代码里硬缓存解析出的 IP(比 JVM 缓存还死)。
- 长连接场景 IP 变了不会自动迁移,要配健康检查 + 最大存活时间 + 重连。
- 架构上优先连稳定入口(LB VIP / K8s Service),把后端 IP 变化挡在基础设施层。
- 凡缓存"会变的东西"(DNS/配置/token/路由),必须配失效刷新策略,别把旧快照当真相。
附:排查 DNS 缓存问题的命令清单
最后,把我现在排查"是不是被 DNS 缓存坑了"常用的命令贴成一份清单。
# 1. 查域名当前实际解析到哪个IP(对比你服务连的是不是旧IP)
dig api.downstream.com +short # 看现在解析出的IP
nslookup api.downstream.com
dig api.downstream.com # 完整信息, 看 ANSWER 段的 TTL(这条记录建议缓存多久)
# 2. 查你服务实际连到了哪个IP(对比上面的"正确IP")
netstat -anp | grep <下游端口> # 看ESTABLISHED连接的对端IP
ss -tnp | grep # 同上, 看实际连的对端
# 3. 查 JVM 的 DNS 缓存配置(Java服务重点)
# 看 $JAVA_HOME/jre/lib/security/java.security 里:
# networkaddress.cache.ttl=? # 是不是 -1(永久)? 是就是元凶
# networkaddress.cache.negative.ttl=?
# 启动参数里有没有 -Dsun.net.inetaddr.ttl / -Dnetworkaddress.cache.ttl
# 4. 验证猜想: 若"重启就好" + "dig出的IP和服务连的IP不一致" → 基本就是DNS缓存
# 临时缓解: 重启(刷新缓存); 根治: 设置合理的 cache.ttl
# 5. 清本地系统DNS缓存(若怀疑系统层)
# Linux: systemd-resolve --flush-caches (或重启 nscd/dnsmasq)
# Windows: ipconfig /flushdns
# 排查思路: 现象"重启才好/别人都好就我不行" → dig看正确IP → netstat看我连的IP →
# 两者不一致 → 查JVM cache.ttl是不是-1或过长 → 设成30秒。
这份清单的核心,是一个对比:dig 出的"域名现在正确的 IP" vs netstat 看到的"我的服务实际连的 IP"——两者不一致,基本就锁定是 DNS 缓存(尤其 JVM 那层)把我困在旧 IP 上了。而"重启就好"这个特征,反而是定位它的最有力线索:能被重启清掉的,多半是进程内的缓存。
写在最后
回头看,这场由"DNS 解析结果缓存过久"引发的、被旧 IP 困住的故障,真正教给我的,远不止"设个合理的 DNS 缓存 TTL"这一个技巧。它让我对"我们常常为了'不变的稳定'而引入一层'间接/抽象'(用域名而非 IP、用接口而非实现、用名字而非地址), 好让背后的东西'可以自由变化'; 可一旦我们'为了图快, 把这层间接的结果缓存死了', 就亲手抵消了这层间接本想带来的灵活性——把'本应动态'的东西又'固化'了回去",有了一次刻骨的体会。我栽跟头,是因为我享受了"用域名"这层间接带来的好处(不用写死 IP),却没有维护好它正常工作所依赖的前提(解析结果要能及时刷新)——我以为"用了域名, 灵活性就自动到手了";可域名这层间接的灵活性, 是有"使用条件"的: 它要求"名字到地址的映射"能跟随真实情况更新; 而我那个永久的 DNS 缓存, 恰恰切断了这个更新——它把"启动那一刻的地址"当成了"永恒的地址", 于是域名退化成了一个"伪装成域名的、写死的旧 IP"。这让我领悟到一个关于"间接、抽象与其代价"的深刻认知:"间接层 / 抽象 / 解耦"(域名、接口、配置中心、服务发现、指针)能带来巨大的灵活性——让背后的东西可以变, 而依赖方无感; 但这份灵活性不是免费的、也不是自动永久成立的: 它依赖"这层间接被正确地、动态地维护";一旦我们为了性能/省事, 把这层间接的"当前结果"缓存死、固化掉, 就等于偷偷拆掉了它的动态性, 让一层"本为应对变化而生"的抽象, 退化成了一个"过时的、写死的具体值"——灵活性的好处没了, 还多了"陈旧"的坑;所以享受任何间接/抽象带来的灵活性时, 都要清楚它"动态生效的前提", 并维护好这个前提(让映射能刷新、让缓存会失效), 而不是把它的某次结果当成永恒。这给了我一种使用一切"间接机制"时的清醒:当我依赖一层"间接/抽象"来获得"背后可变、我无感"的灵活性时,要警惕任何"把这层间接的结果固化/缓存死"的做法——问自己"我有没有为了图快, 把一个'本应动态查询/刷新'的东西, 缓存成了'一次取得、永久不变'?这会不会让它在背后变化时失灵?";"缓存一个动态映射时, 必须尊重它'会变'的本性、配上刷新机制, 别让缓存抵消了间接层的灵活性",是用好一切抽象/解耦而不被其反噬的关键。认清间接/抽象的灵活性依赖其被动态维护、把间接结果缓存死会让抽象退化成过时的具体值、缓存动态映射必须配刷新——这,是我用一次 DNS 缓存的事故,换来的、关于网络、也关于如何对待间接与抽象的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次发现服务"连着旧 IP、重启才好"时,第一时间想到去查那个 networkaddress.cache.ttl 是不是 -1,那我对着那个"别人都好就我连不上"的诡异现象排查的这段时间,就值了。
—— 别看了 · 2026