一个 JVM 服务因为默认永久缓存了 DNS 解析结果,在下游域名切换 IP 后还死连着早已下线的旧地址,连接全失败:一次 DNS 缓存的深度复盘

下游第三方做机房迁移,把域名解析到新 IP、旧 IP 下线,域名没变我们本该无感,结果服务大面积连接失败,而服务器上 nslookup 查的是新 IP、重启服务就好了。根因是 JVM 有独立于 OS 的 DNS 缓存,networkaddress.cache.ttl 默认可能是 -1(永久缓存)——进程首次把域名解析成旧 IP 后就永久记住、再也不重新解析,下游换 IP 后还死连旧地址、不重启不自愈。本文讲透 JVM 的 DNS 缓存机制,给出设合理 TTL(30~60 秒)、用会刷新的客户端、服务发现/LB 屏蔽 IP 变化的正解,梳理 DNS 与缓存常见坑,最后落到'缓存是用过去的真相换性能、世界会变所以缓存必须能过期自我纠正'的认知。

一个 JVM 服务因为默认永久缓存了 DNS 解析结果,在下游域名切换 IP 后还死连着早已下线的旧地址,连接全失败:一次 DNS 缓存的深度复盘

那次故障发生在一次再寻常不过的下游迁移之后:我们依赖的一个第三方服务做了机房迁移,把它的域名 api.partner.com 解析到了一批新的 IP,旧 IP 上的实例随后下线。按理说,域名没变,我们这边什么都不用动,DNS 会自动把流量引到新 IP。可现实是:对方切换完之后,我们的服务大面积连接失败、超时,日志里全是连不上的报错。我用 nslookup 在服务器上一查,域名明明已经解析到新 IP 了;我重启了一下服务,居然就好了。这个"重启就好"的线索,让我把矛头指向了"缓存",排查后才终于撞上那个 Java 程序员的经典陷阱,后背发凉:JVM 默认会永久缓存 DNS 解析结果。出于安全考虑(防 DNS 欺骗),老版本 JDK 的 networkaddress.cache.ttl 在装了 SecurityManager 时默认是 -1,意思是"缓存永不过期"——也就是说,我的 JVM 进程第一次api.partner.com 解析成旧 IP 后,就把这个结果一直死死地记在内存里,之后再也不去重新解析了。所以哪怕 DNS 服务器早已更新、操作系统 nslookup 查的是新 IP,我那个长期运行的 JVM 进程,还固执地连着它启动时记下的那个旧 IP——而旧 IP 上的实例已经下线,连接自然全失败。重启之所以"好了",是因为重启让 JVM 重新解析了一次,拿到了新 IP。这篇就把这次"JVM DNS 永久缓存"的坑,从头到尾复盘一遍。

故障现场:JVM 把旧 IP 缓存了一辈子

问题不在某行代码,而在一个被忽视的 JVM 默认行为:

故障的来龙去脉:

1. 我们的服务(长期运行的JVM进程)调用 https://api.partner.com/...;
2. JVM第一次解析 api.partner.com → 得到旧IP(比如 1.1.1.1), 缓存下来;
3. 对方机房迁移: DNS把 api.partner.com 改解析到新IP(2.2.2.2), 旧IP 1.1.1.1 下线;
4. 但我们的JVM【还缓存着旧IP 1.1.1.1】, 不重新解析 → 继续连 1.1.1.1;
5. 1.1.1.1 已下线 → 连接失败/超时 → 大面积报错;
6. 在服务器上 nslookup api.partner.com → 显示新IP 2.2.2.2(操作系统/DNS是对的);
7. 重启服务 → JVM重新解析 → 拿到新IP → 恢复正常。

为什么JVM会永久缓存DNS:
- JVM(java.net.InetAddress)有自己的DNS缓存, 独立于操作系统;
- 缓存时长由 networkaddress.cache.ttl 控制(单位秒):
  - -1: 永久缓存(永不过期)——【危险的默认值之一】(老JDK装SecurityManager时);
  -  0: 不缓存(每次都解析);
  -  正数: 缓存这么多秒。
- 设计初衷: 减少DNS查询、并出于安全(防DNS缓存投毒/欺骗, 避免频繁解析被攻击)。
- 但副作用: 在"下游IP会变"(云环境扩缩容、迁移、故障转移、蓝绿发布)的今天, 永久缓存=灾难。

# 关键: JVM有独立于OS的DNS缓存, 默认可能永久缓存(ttl=-1); 下游域名换IP后, JVM还连旧IP
#   → 连接失败; 而OS的nslookup是对的、重启JVM能恢复——这是排查时的典型线索。

第一次明白这个坑时,我又意外又警醒:"操作系统都解析到新 IP 了,我的 Java 进程居然还在用启动时记的旧 IP,而且是'永久'记着?"这个坑最隐蔽的地方在于:平时完全无感——只要下游 IP 不变,缓存旧 IP 也没关系(反正一直是那个 IP);它只在"下游域名切换了 IP"的那一刻才爆发,而"IP 切换"对调用方是完全透明、无法预知的(对方迁移、扩缩容、故障转移你根本不知道)更迷惑人的是"重启就好":这个现象太容易让人误以为是"偶发抖动、重启解决",而错过"DNS 缓存"这个真正的根因下面就来拆解,JVM 的 DNS 缓存机制和正解。

第一件事:搞懂 JVM 的 DNS 缓存机制

我认真研究了 JVM 的 DNS 缓存,才彻底理解这个坑。

JVM 的 DNS 缓存: 它在OS之上, 自己又缓存了一层

【核心: JVM(InetAddress)有自己独立的DNS缓存, 由networkaddress.cache.ttl控制, 默认可能永久缓存】

1. DNS解析其实有多层缓存:
   - 应用层(JVM自己的InetAddress缓存)← 本文的坑在这层;
   - 操作系统层(OS的DNS缓存/nscd等);
   - DNS服务器层(各级DNS的缓存, 受域名记录的TTL控制)。
   - → 你 nslookup 查的是OS/DNS层(可能是对的), 但JVM层可能还缓存着旧的!

2. JVM的两个关键参数(java.security里配置, 或代码设置):
   - networkaddress.cache.ttl: 成功解析的缓存秒数;
     -1=永久, 0=不缓存, 正数=缓存N秒。
   - networkaddress.cache.negative.ttl: 解析失败的缓存秒数(默认10秒)。

3. 默认值的"坑":
   - 历史上: 装了SecurityManager时, ttl默认-1(永久缓存); 没装时默认30秒(实现相关);
   - 不同JDK版本/配置下默认值不一致, 这种"不确定的默认"本身就危险;
   - → 长期运行的服务, 如果撞上"永久缓存", 下游一换IP就连不上、且不会自愈(除非重启)。

4. 为什么云原生时代这个坑更突出:
   - 以前服务器IP很固定; 现在云环境下IP【经常变】:
     扩缩容(新实例新IP)、故障转移、容器漂移、蓝绿/滚动发布、域名指向变更...
   - → "域名背后的IP是会变的"成了常态, 而"永久缓存IP"和这个常态严重冲突。

一句话: JVM在OS之上有自己独立的DNS缓存, 由networkaddress.cache.ttl控制、默认可能永久缓存;
   云时代下游IP常变, 永久缓存会让JVM连着旧IP连不上、且不重启不自愈——必须配置合理的缓存TTL。

这套机制,是整个坑的根。DNS 解析有多层缓存:应用层(JVM 自己的 InetAddress 缓存,本文的坑)、操作系统层、DNS 服务器层;你 nslookup 查的是 OS/DNS 层(可能是对的),但 JVM 层可能还缓存着旧的JVM 的关键参数:networkaddress.cache.ttl(成功解析缓存秒数:-1 永久、0 不缓存、正数 N 秒)、negative.ttl(失败缓存)。默认值的坑:历史上装了 SecurityManager 时 ttl 默认 -1(永久缓存),不同 JDK 版本默认不一致——这种"不确定的默认"本身就危险,长期运行的服务撞上永久缓存就连不上且不自愈。云原生时代更突出:以前 IP 固定,现在云环境 IP 经常变(扩缩容、故障转移、容器漂移、蓝绿发布),"域名背后 IP 会变"成了常态,与"永久缓存 IP"严重冲突。一句话:JVM 在 OS 之上有自己独立的 DNS 缓存,由 networkaddress.cache.ttl 控制、默认可能永久缓存;云时代下游 IP 常变,永久缓存会让 JVM 连旧 IP 连不上且不重启不自愈——必须配置合理的缓存 TTL。

第二件事:正解——配置合理的 DNS 缓存 TTL,并用支持刷新的客户端

搞懂了原理,正解就清晰了:把 networkaddress.cache.ttl 设成一个合理的秒数(如 30~60 秒)、别用永久缓存;用支持连接池+DNS 刷新的客户端;关键服务用服务发现/负载均衡屏蔽 IP 变化

# ====== 正解一: 配置 JVM 的 DNS 缓存 TTL(别永久缓存) ======
# 方式A: 在 $JAVA_HOME/jre/lib/security/java.security 里设置
networkaddress.cache.ttl=30          # 成功解析缓存30秒(到点重新解析, 能感知IP变化)
networkaddress.cache.negative.ttl=5  # 失败缓存5秒(别把"暂时解析失败"也缓存太久)

# 方式B: 代码里在启动早期设置(注意要在第一次DNS解析之前)
# java.security.Security.setProperty("networkaddress.cache.ttl", "30");

# 方式C: 部分场景可用 -Dsun.net.inetaddr.ttl=30 (但优先用上面的标准配置)

# → 设成30~60秒: 既享受短期缓存(减少DNS查询), 又能在IP变化后较快(30秒内)感知并切换。
# → 别设成 -1(永久); 也别设成 0(完全不缓存, 每次都查DNS, 高频时有性能/DNS压力)。
// ====== 正解二: 用会"刷新DNS"的HTTP客户端 ======
// - Apache HttpClient/OkHttp等的连接池, 配合上面的JVM TTL, 会在连接失效后重新解析;
// - 现代 HttpClient(JDK11+ java.net.http)、各SDK也多会按JVM的DNS缓存策略来;
// - 关键: JVM的DNS缓存TTL是【底层基础】, 上层客户端的连接池要在连接坏掉时能重建连接(重新解析)。

// ====== 正解三(治本): 关键依赖用服务发现/负载均衡, 屏蔽IP变化 ======
// - 用服务注册发现(Nacos/Consul/Eureka/K8s Service): 客户端从注册中心拿"当前健康的实例列表",
//   实例上下线由注册中心实时反映 → 不依赖DNS缓存、IP变化自动感知;
// - 或在下游前面挂一个稳定的负载均衡(LB/网关), 你只连LB的固定地址, 后端IP变化由LB处理;
// - → 把"应对下游IP变化"这件事, 交给专门的基础设施, 比依赖DNS缓存TTL更可靠。

// ====== 排查口诀(下次遇到"重启就好"的连接问题) ======
// - OS层 nslookup/dig 查到的IP, 和 应用实际在连的IP, 一致吗?(用netstat看实际连接)
// - 是不是"重启就好"?→ 高度怀疑应用层缓存(DNS缓存、连接池缓存)。

// 核心: 把JVM的networkaddress.cache.ttl设成合理秒数(30~60), 别永久缓存也别完全不缓存;
//   用能在连接失效时重新解析的客户端; 关键依赖用服务发现/LB屏蔽IP变化——别让永久缓存的旧IP坑你。

修复的核心,是"让 DNS 缓存有一个合理的过期时间,并用基础设施屏蔽 IP 变化"正解一:配置 JVM 的 DNS 缓存 TTL——在 java.security 里设 networkaddress.cache.ttl=30(成功缓存 30 秒,到点重新解析、能感知 IP 变化),negative.ttl 设小(别把暂时解析失败缓存太久);别设 -1(永久)、也别设 0(完全不缓存有 DNS 压力)正解二:用会刷新 DNS 的客户端(JVM TTL 是底层基础,上层连接池要在连接坏掉时能重建/重新解析)。正解三(治本):关键依赖用服务发现/负载均衡——从注册中心拿健康实例列表、或只连稳定的 LB,把"应对 IP 变化"交给专门基础设施,比依赖 DNS 缓存 TTL 更可靠排查口诀:"重启就好"的连接问题,高度怀疑应用层缓存;对比 OS 解析的 IP 和应用实际在连的 IP归根结底:把 JVM 的 networkaddress.cache.ttl 设成合理秒数(30~60),别永久也别完全不缓存;用能在连接失效时重新解析的客户端;关键依赖用服务发现/LB 屏蔽 IP 变化。

第三件事:DNS 与缓存相关的其他常见坑

排查后我把 DNS 和缓存相关的其他常见坑也系统梳理了一遍。

DNS / 缓存的其他常见坑

# 1. JVM永久缓存DNS(本文): ttl=-1, IP变了不重新解析。→ 设合理TTL(30~60秒)。

# 2. 失败结果也被缓存(negative.ttl): 一次解析失败被缓存, 短时间内都失败。→ 设小negative.ttl。

# 3. 只信OS的nslookup: 排查时只看OS解析对不对, 忽略了应用层(JVM/连接池)的缓存。

# 4. 连接池缓存了到旧IP的连接: 就算重新解析了, 连接池里的旧连接还连着旧IP。→ 连接要有空闲回收/健康检查。

# 5. 域名记录TTL设太长: DNS记录自身TTL很大, 切换IP后各级缓存很久才更新。→ 切换前调小记录TTL。

# 6. 各种缓存没设过期: 不只是DNS, 配置缓存、数据缓存、token缓存等"忘了设过期"→ 数据陈旧。

# 7. "重启就好"当成解决: 重启清了缓存看似好了, 但没找根因, 下次还会犯。

# 8. 多层缓存不一致: 应用缓存、OS缓存、DNS缓存、CDN缓存各有TTL, 排查要逐层看。

# 共同根源: 缓存是"用'一段时间内数据不变'的假设, 换取性能"; 而当数据真的变了(IP变、配置变),
#   缓存就成了"陈旧的旧数据", 若没有合理的过期/失效机制, 就会用着过期数据出错。

# 核心: 凡是缓存都要有合理的"过期/失效"策略(别永久缓存); DNS缓存设合理TTL; 排查连接问题要
#   逐层看缓存(应用/OS/连接池); "重启就好"是缓存问题的强信号——找到根因而非靠重启。

排查让我把 DNS/缓存的其他坑也梳理清了。一、JVM 永久缓存 DNS(本文)。二、失败结果也被缓存(设小 negative.ttl)。三、只信 OS 的 nslookup(忽略应用层缓存)。四、连接池缓存了到旧 IP 的连接(要健康检查/空闲回收)。五、域名记录 TTL 太长(切换前调小)。六、各种缓存没设过期七、"重启就好"当成解决(没找根因)。八、多层缓存不一致(逐层看)。它们的共同根源是:缓存是"用'一段时间内数据不变'的假设换取性能";而当数据真的变了(IP 变、配置变),缓存就成了陈旧的旧数据,若没有合理的过期/失效机制就会用着过期数据出错核心是:凡是缓存都要有合理的过期/失效策略(别永久缓存);DNS 缓存设合理 TTL;排查连接问题逐层看缓存(应用/OS/连接池);"重启就好"是缓存问题的强信号——找根因而非靠重启下面这张图,是这次 DNS 缓存坑的成因与解法:

第四件事:DNS 缓存 TTL 取值速查表

这次踩坑后,我把 networkaddress.cache.ttl 的几种取值及其影响整理成一张表。

ttl取值 行为 适用/风险
-1 永久缓存, 永不重新解析 ✗ 危险(本文), IP变了不自愈
0 不缓存, 每次都解析 能即时感知IP变化, 但DNS压力大
30~60 缓存数十秒 ✓ 推荐: 兼顾性能与IP变化感知
很大(如3600) 缓存1小时 IP变化后要很久才感知
negative.ttl小(如0~5) 失败少缓存 ✓ 别把暂时失败缓存太久

这张表把 DNS 缓存 TTL 取值钉清了。核心是:DNS 缓存 TTL 的取值是一个"性能 vs 实时性"的权衡——TTL 越大,DNS 查询越少(性能好)、但 IP 变化感知越慢;TTL 越小,越能即时感知变化、但 DNS 查询越频繁;两个极端都不好(-1 永久缓存=变了不自愈,0=DNS 压力大),30~60 秒是兼顾两者的合理平衡点它给我的最大启发是:几乎所有"缓存"的设计,核心都是同一个权衡——"缓存多久"决定了"性能"和"数据新鲜度(一致性)"之间的平衡:缓存越久越快、但数据越可能陈旧;缓存越短越新、但越没缓存的意义;"设置一个合理的过期时间",本质就是在回答"我能容忍数据陈旧多久"这个业务问题这让我对"缓存 TTL"有了更清醒的认识:设任何缓存的过期时间,都不是随便填一个数,而要想"这个数据多久会变?变了之后,我最多能容忍用旧数据多久?"——DNS(IP 几十秒级别要能切)、配置(分钟级)、字典数据(小时/天级)、几乎不变的数据(很长);"按数据的变化频率和你对陈旧的容忍度,来定缓存 TTL",是用好一切缓存的关键判断理解 DNS 缓存 TTL 的性能与实时性权衡、按容忍度定过期时间——是这个坑带给我的缓存认知。

第五件事:这个坑暴露的"看不见的多层缓存"

这次最该记取的,是"OS 查到的对、应用却用着旧的"——问题藏在我没意识到的那层缓存里。我把 DNS 的多层缓存整理成表。

层级 缓存在哪 排查方式
应用层(JVM) InetAddress缓存(本文) 看TTL配置/重启对比/netstat实连IP
连接池层 已建立的到旧IP的连接 看连接池配置/空闲回收
操作系统层 OS的DNS缓存/nscd nslookup/dig、刷新OS缓存
DNS服务器层 各级DNS按记录TTL缓存 查域名记录TTL
CDN/网关层 CDN的解析/路由缓存 查CDN配置

这张表道出了一个排查时的关键认知。核心是:一次 DNS 解析,背后其实有好几层缓存(应用/连接池/OS/DNS 服务器/CDN),每一层都可能缓存着旧值;我栽就栽在只检查了 OS 层(nslookup 是对的),却完全没意识到应用层(JVM)还藏着一层缓存——而问题恰恰在那一层它给我的深刻启发是:排查问题时,一个常见的陷阱是"只检查了你想到的那几层,而问题恰恰在你没想到、没检查的那一层"——现代系统是层层叠叠的(多层缓存、多层代理、多层抽象),一个现象(连不上)的成因可能藏在任何一层;如果你对系统的分层结构没有完整的认知,就会"查了半天都在错的层里打转";"看到 OS 是对的就以为问题不在 DNS",正是因为我没意识到 JVM 之上还有一层应用缓存这给了我一种系统性排查的自觉:排查问题(尤其涉及"数据/状态不一致")时,要主动地、系统地把"从源头到使用处之间的每一层"都列出来、逐层检查——"数据从产生到我用到,中间经过了哪些层?哪一层可能缓存/篡改/延迟了它?";"建立对系统分层结构的完整认知、逐层排查",才能避免"在错的层里反复打转、漏掉真正出问题的那一层"认清系统的多层缓存结构、排查时逐层检查别漏掉没想到的那层——是这个坑带给我的排查方法论。

第六件事:部署长期运行的服务时,我现在的检查习惯

现在每当我部署一个长期运行、依赖外部域名的服务,我都会按这张图把 DNS 这块过一遍:

这张图的精髓,是"下游 IP 会变就必须处理 DNS 缓存,设合理 TTL 或用服务发现"下游 IP 会变(云环境常态)就必须处理:配 networkaddress.cache.ttl=30~60(别用 -1)、或用服务发现/LB 屏蔽 IP 变化、并确认客户端连接失效时会重新解析这套习惯,让我从"域名没变就什么都不用管"变成了"部署时先确认 DNS 缓存策略"——核心始终是:下游 IP 在云时代常变,长期服务必须配合理的 DNS 缓存 TTL 或用服务发现,别让永久缓存的旧 IP 坑你。

我立下的几条规矩

这场"JVM 永久缓存 DNS、连旧 IP 全失败"的事故,换来了我做服务时,刻进骨子里的几条铁律:

  1. JVM 有独立于 OS 的 DNS 缓存。OS 解析对了,JVM 可能还缓存着旧 IP。
  2. networkaddress.cache.ttl 默认可能 -1(永久)。下游换 IP 后不重启不自愈。
  3. 把 DNS 缓存 TTL 设成 30~60 秒。别永久缓存,也别完全不缓存。
  4. 关键依赖用服务发现/LB 屏蔽 IP 变化。比依赖 DNS 缓存 TTL 更可靠。
  5. "重启就好"是缓存问题的强信号。别满足于重启,要找根因。
  6. 排查连接问题逐层看缓存。应用/连接池/OS/DNS,别只信 nslookup。
  7. 凡缓存都要有合理的过期策略。按数据变化频率和容忍度定 TTL,别永久。

写在最后

回头看,这场由"JVM 永久缓存了一个 IP"引发的、下游切换后全线连不上的事故,真正教给我的,远不止"配置 networkaddress.cache.ttl"这一个技巧。它让我对"缓存,是用'过去的真相'换性能;而世界是会变的,'过去的真相'迟早会过期",有了一次刻骨的体会。我栽跟头,根源在于一个被缓存悄悄固化了的、却已经过期的"事实":"api.partner.com 就在 1.1.1.1"——这在我的服务启动那一刻,是千真万确的事实;于是 JVM 把它当成"永恒的真理"缓存了下来。可问题在于,这个"事实"本质上是会变的(域名背后的 IP 随时可能迁移),而我的缓存却把一个"会变的事实",当成"永不变的真理"永久地记住了;当现实变了(IP 切换),我的缓存还固执地停留在那个早已过期的旧真相里,于是和现实脱了节这让我领悟到一个关于缓存(乃至一切"记住的状态")的深刻认知:缓存的本质,是"用'一个曾经为真的值'来代替'每次都去问现实'",以换取速度——隐含了一个赌注:"这个值在缓存期间不会变";而这个赌注,赌的就是"世界的某种稳定性";一旦世界变了(IP 变、配置变、数据变)、而缓存没有及时过期更新,缓存就从"加速器"变成了"谎言的来源"——它会自信地给你一个早已不对的旧答案这给了我一种使用缓存的根本清醒:用任何缓存时,都要清醒地问自己:"我缓存的这个东西,会变吗?如果变了,我的缓存怎么知道、多久能知道?"——为缓存设一个"合理的过期时间",本质就是在为"世界会变"这件事留出一个自我纠正的窗口;永久缓存,等于赌"这个东西永远不变"——而在一个不断变化的系统里,这个赌注几乎注定会输;"承认世界会变、给缓存留下感知变化的能力(过期/失效)",是用好缓存而不被它反噬的关键认清缓存是用过去的真相换性能、世界会变所以缓存必须能过期自我纠正——这,是我用一次 DNS 永久缓存的事故,换来的、关于网络、也关于如何对待一切"被缓存的状态"的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次部署长期服务、或设任何缓存时,先想一句"它会变吗?我留了过期的窗口吗?",那我对着那个"重启就好"的连接故障排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

一条 NOT IN 子查询的 SQL,因为子查询里混进了一个 NULL,把本该返回几千行的结果集变成了空,我栽进了 SQL 三值逻辑的坑:一次 NULL 处理的深度复盘

2026-6-2 17:53:28

技术教程

一个没有配置日志轮转的服务,把一个几十 GB 的日志文件一路写到磁盘爆满,然后整台机器上的服务集体瘫痪:一次磁盘写满的深度复盘

2026-6-2 18:03:48

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索