微服务启动后的“第一声叹息”:为什么 Feign 第一次调用要等 3 秒?
你的 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 第一次调用慢,不是单一原因造成的,而是三个因素叠加:
-
类加载与动态代理初始化:Feign 在第一次调用时才生成代理类、初始化
FeignClientFactoryBean。 -
DNS 解析:如果服务地址是域名(如 K8s 的 Service 域名),第一次调用触发完整的 DNS 递归查询。
-
连接池建立: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 堆积:为什么短连接压测后端口被耗尽?”
更多推荐
所有评论(0)