你的 SpringCloud 微服务刚刚启动,日志刷完,一切正常。
你迫不及待地发第一个请求——然后,卡住了。3 秒后,响应才姗姗来迟。
后续请求全部正常,几十毫秒就返回。
这 3 秒的“见面礼”从哪来的?为什么只有第一次?
答案藏在 JVM 的 DNS 缓存机制 里——它让第一次域名解析变成了“龟速”,后续却“光速”。

        大家好,我是 Evan,一个曾被 Feign 第一次调用“卡 3 秒”逼疯过的 Java+AI 学生。
        今天,我从 DNS 解析的完整流程 讲起,揭开 JVM DNS 缓存的“双面人格”——正缓存和负缓存的不同策略,以及那个让无数微服务开发者踩坑的 sun.net.inetaddr.ttl
        读完这篇,你不仅能搞定“第一次调用慢”,还能彻底掌控 JVM 的 DNS 行为。

📌 写在前面

        在智荟Agent项目中,我用 SpringCloud Feign 调用另一个微服务。服务启动后第一次请求,愣是等了 3 秒多才返回。后续请求秒回。我查了数据库、查了网络、查了 GC,都没问题。最后用 tcpdump 抓包才发现——第一次调用时,JVM 正在做一次完整的 DNS 递归查询,从根域名服务器一路问到权威 DNS。而 JVM 默认的 DNS 缓存策略,在“没有 Security Manager”的情况下,正缓存 TTL 默认只有 30 秒。更坑的是,很多人试图用 -Dnetworkaddress.cache.ttl 去改,结果完全无效。
        这篇博客,我就把这套机制彻底讲透。

一、DNS 解析为什么“慢”?

当你调用 InetAddress.getByName("order-service.prod.svc.cluster.local") 时,JVM 要做一次完整的 DNS 解析:

一次完整的 DNS 递归查询,通常需要 50~200ms。如果 DNS 服务器响应慢,甚至可能达到 500ms~1s。再加上 Feign 的类加载、动态代理初始化、连接池建立,第一次调用耗时 2~3 秒 完全不奇怪。

问题:为什么后续调用不慢?因为 JVM 把 DNS 解析结果缓存了。

二、JVM DNS 缓存的“两幅面孔”

InetAddress 类内部维护了一个 DNS 缓存,但它对成功失败的解析采用了完全不同的策略。

2.1 正缓存(成功解析)——默认 30 秒

当域名成功解析为 IP 后,JVM 会缓存这个结果。默认缓存时间取决于是否启用了 Security Manager:

关键配置(注意:这是 security property,不是普通的系统属性):

  • networkaddress.cache.ttl:正缓存 TTL,单位秒

  • 值 -1 表示永久缓存,0 表示不缓存

一个巨大的坑:很多人试图用 -Dnetworkaddress.cache.ttl=60 来修改,但这个参数不起作用!因为它是一个 security property,必须写在 $JAVA_HOME/lib/security/java.security 文件中,或者通过 -Djava.security.properties 指定。

正确的 JVM 参数是:

bash

-Dsun.net.inetaddr.ttl=60

这个 undocumented 系统属性才会生效。

2.2 负缓存(解析失败)——默认 10 秒

当域名解析失败(UnknownHostException)时,JVM 也会缓存这个“失败结果”,默认 10 秒

bash

# 负缓存配置
networkaddress.cache.negative.ttl=10   # security property
-Dsun.net.inetaddr.negative.ttl=10     # JVM 参数(undocumented)

为什么要有负缓存? 防止应用在 DNS 服务短暂不可用时,疯狂发起重复查询,造成“DNS 雪崩”。

2.3 一张表看懂全部

缓存类型默认 TTL配置方式(security property)配置方式(JVM 参数)
正缓存(成功)30 秒networkaddress.cache.ttl-Dsun.net.inetaddr.ttl
负缓存(失败)10 秒networkaddress.cache.negative.ttl-Dsun.net.inetaddr.negative.ttl

三、Feign 第一次调用慢的“三重奏”

Feign 第一次调用慢,不是单一原因造成的,而是三个因素叠加

  1. 类加载与动态代理初始化:Feign 在第一次调用时才生成代理类、初始化 FeignClientFactoryBean

  2. DNS 解析:如果服务地址是域名(如 K8s 的 Service 域名),第一次调用触发完整的 DNS 递归查询。

  3. 连接池建立:HTTP 连接池(如 OkHttp、Apache HttpClient)在第一次调用时才建立连接。

后续调用时,代理已生成、DNS 已缓存、连接已建立,自然就快了。

四、解决方案:三招根治

方案一:设置合理的 DNS 缓存 TTL(推荐)

在 JVM 启动参数中添加:

bash

java -Dsun.net.inetaddr.ttl=60 -Dsun.net.inetaddr.negative.ttl=10 -jar app.jar
  • 正缓存 60 秒:既能减少后续 DNS 查询,又能在一定时间内感知 IP 变化

  • 负缓存 10 秒:保持默认,防止 DNS 故障时的雪崩

AWS 官方建议:对于云上资源(IP 可能动态变化),推荐设置为 5 秒;一般场景 30~60 秒 是合理区间。

方案二:启动预热(Pre-warm)

在应用启动后、对外提供服务前,主动触发一次 DNS 解析:

java

@PostConstruct
public void warmup() {
    // 提前解析所有依赖的服务域名
    String[] services = {"order-service", "user-service", "product-service"};
    for (String svc : services) {
        try {
            InetAddress.getByName(svc + ".prod.svc.cluster.local");
        } catch (UnknownHostException e) {
            log.warn("DNS warmup failed for {}", svc);
        }
    }
}

这样第一次业务请求来时,DNS 缓存已经“热”好了。

方案三:使用 IP 地址替代域名(不推荐)

直接在 Feign 客户端中配置 IP 地址,绕过 DNS 解析。
缺点:IP 变化时需要重新部署,且无法利用 K8s 的服务发现和负载均衡。

yaml

# application.yml
order-service:
  ribbon:
    listOfServers: 192.168.1.100:8080,192.168.1.101:8080

五、排障工具:如何确认是 DNS 问题?

5.1 抓包看 DNS 查询

bash

tcpdump -i any port 53 -nn

如果第一次调用时有 DNS 请求发出,后续没有,说明是 DNS 缓存问题。

5.2 开启 JVM DNS 日志

bash

-Dsun.net.inetaddr.debug=true

启动后可以看到类似输出:

text

DNS: InetAddress.getByName("order-service") 
DNS: lookup host order-service.prod.svc.cluster.local 
DNS: positive cache entry for order-service.prod.svc.cluster.local / 192.168.1.100

5.3 查看当前 TTL 配置

bash

# 查看 security properties 配置
cat $JAVA_HOME/lib/security/java.security | grep networkaddress.cache

📝 总结

问题根因解决方案
微服务第一次调用慢Feign 懒加载 + DNS 首次解析 + 连接池建立预热 + 合理 TTL
DNS 解析每次都慢TTL 太短或未缓存设置 -Dsun.net.inetaddr.ttl=60
配置不生效用了错误的参数名用 -Dsun.net.inetaddr.ttl,不是 -Dnetworkaddress.cache.ttl
DNS 故障导致雪崩负缓存未启用保持 -Dsun.net.inetaddr.negative.ttl=10

核心结论

  • JVM DNS 正缓存默认 30 秒,负缓存默认 10 秒

  • 修改 TTL 要用 -Dsun.net.inetaddr.ttl,而不是 -Dnetworkaddress.cache.ttl

  • Feign 第一次调用慢是“类加载 + DNS + 连接池”三重因素叠加。

  • 通过预热脚本合理 TTL,可以让第一次调用“不再特殊”。

🤔 思考题
你的微服务部署在 Kubernetes 中,使用 StatefulSet,每个 Pod 的 IP 可能会在重启后变化。
你设置了 -Dsun.net.inetaddr.ttl=300(5 分钟),希望减少 DNS 查询压力。
某天,一个下游服务发生了滚动更新,IP 变了。你的服务还在使用旧的缓存 IP(因为 5 分钟未到),导致调用失败。
问题:在不重启应用的前提下,你有什么办法让 JVM “主动刷新” 某个域名的 DNS 缓存?
(提示:考虑 InetAddress 缓存的内部实现、SecurityManager 绕行、或使用 dnsjava 等第三方库)

欢迎在评论区留下你的方案 —— 下一篇我会聊聊 “TIME_WAIT 堆积:为什么短连接压测后端口被耗尽?”

更多推荐