JVM DNS 缓存 TTL 配置避坑指南
JVM DNS 缓存 TTL 配置避坑指南(实验验证版)
适用范围:所有 Java 业务,尤其在用域名而非 IP 接入下游服务(数据库、Kafka、Redis、HTTP 服务等)的场景。
验证环境:OpenJDK 17(Rocky Linux 8.10);结论同样适用 Java 8/11/21。
验证日期:2026-05-25。
TL;DR(先看这部分)
如果你只想拿走一条结论:
-Dnetworkaddress.cache.ttl=N是错的,不会生效。想用 JVM 启动参数控制 DNS 缓存 TTL,只有
-Dsun.net.inetaddr.ttl=N这一种正确写法。
如果还能拿走第二条:
代码里
Security.setProperty("networkaddress.cache.ttl", "N")极易被 log4j2、Spring、Kafka client 等抢跑而失效。生产配置不要依赖这种方式。
如果还能拿走第三条:
推荐配置(任选其一,不用全选):
- 启动参数:
-Dsun.net.inetaddr.ttl=30 -Dsun.net.inetaddr.negative.ttl=10- 或修改
$JAVA_HOME/conf/security/java.security里networkaddress.cache.ttl=30、networkaddress.cache.negative.ttl=10
下面是 11 组对照实验的证据。
1. 背景:两个属性、两套存储、一个静态字段
1.1 控制 JVM DNS 缓存 TTL 的两个属性
| 属性名 | 类别 | 设置途径 | 备注 |
|---|---|---|---|
networkaddress.cache.ttl |
Security property | 代码 Security.setProperty(...);$JAVA_HOME/conf/security/java.security 文件 |
Java 1.4+ 官方正名 |
sun.net.inetaddr.ttl |
System property | 代码 System.setProperty(...);JVM 启动参数 -D |
Sun 私有的旧名,作为 fallback 保留 |
负向缓存对应 networkaddress.cache.negative.ttl / sun.net.inetaddr.negative.ttl。
1.2 JDK 内部的读取逻辑(OpenJDK 17 源码 sun.net.InetAddressCachePolicy)
1 | public final class InetAddressCachePolicy { |
三个关键事实:
- 静态块只跑一次(class load 时),之后不再读 Security/System property。
- 所以业务代码里再调
Security.setProperty(...)改 TTL,InetAddressCachePolicy 不会回头读——值已经被冻在cachePolicy字段里了。 - JVM 内部 System Properties 与 Security Properties 是两套独立存储:
-Dnetworkaddress.cache.ttl=N写进的是 System Properties- 但 InetAddressCachePolicy 第一步走
Security.getProperty(...),根本不读 System Properties - 所以这种写法虽然不报错,但完全没有效果
1.3 默认值
- 无 SecurityManager(绝大多数现代生产场景):正向 30 秒,负向 10 秒
- 有 SecurityManager(极少见):正向 forever(永久),负向 10 秒
2. 实验设计
2.1 待验证的核心假设
任何在你 Security.setProperty(...) 之前调用 InetAddress.getXxxByName() / getLocalHost() 的代码,都会触发 InetAddressCachePolicy 类首次加载,使 cachePolicy 字段被冻结到 JVM 默认值。其中最常见的抢跑者就是 log4j2 的 LogManager.getLogger(...)——尤其是写在 static final 字段里的那种。
2.2 测量方法(双重证据)
- 直接证据:反射读取
sun.net.InetAddressCachePolicy.get()拿到 effective 值 - 间接证据:
tcpdump抓udp dst port 53包,统计 20 秒内对目标域名的实际 DNS 查询次数- 若 TTL=2 生效:约 20+ 次(每秒一查,缓存基本不命中)
- 若 TTL 失效(默认 30s):约 4~6 次(启动 overhead + 全程命中缓存)
- 差异 4~6 倍,结论稳定
2.3 11 个对照场景
| # | TTL 设置方式 | log4j Logger 时机 |
|---|---|---|
| S1 | -Dnetworkaddress.cache.ttl=2 |
不引入 log4j(基线) |
| S2 | 代码 Security.setProperty(main 首行) |
不引入 log4j(基线) |
| S3 | 代码 setProperty(main 首行) | setProperty 之后再 LogManager.getLogger |
| S4 | 代码 setProperty | static final Logger 字段(类加载即触发,生产里最常见的写法) |
| S5 | 代码 setProperty | main 中先 LogManager.getLogger 后 setProperty(手写错顺序) |
| S6 | -Dnetworkaddress.cache.ttl=2 |
static final Logger 字段 |
| S7 | -Dsun.net.inetaddr.ttl=2 |
不引入 log4j |
| S8 | -Dsun.net.inetaddr.ttl=2 |
static final Logger 字段(验证 -D 能否兜底) |
| S9 | -Dsun.net.inetaddr.ttl=2 |
main 中先 getLogger 后 setProperty |
| S10 | java.security 文件 networkaddress.cache.ttl=2 |
不引入 log4j |
| S11 | java.security 文件 networkaddress.cache.ttl=2 |
static final Logger 字段(验证文件能否兜底) |
3. 实验结果
| # | TTL 设置方式 | log4j Logger 时机 | DNS 查询数 | cachePolicy | TTL 生效 |
|---|---|---|---|---|---|
| S1 | -Dnetworkaddress.cache.ttl=2 |
无 | 6 | 30 | ❌ |
| S2 | 代码 Security.setProperty |
无 | 26 | 2 | ✅ |
| S3 | 代码 setProperty | setProperty 后 getLogger | 26 | 2 | ✅ |
| S4 | 代码 setProperty | static final 字段 | 4 | 30 | ❌ |
| S5 | 代码 setProperty | main 中 getLogger 先于 setProperty | 4 | 30 | ❌ |
| S6 | -Dnetworkaddress.cache.ttl=2 |
static final 字段 | 6 | 30 | ❌ |
| S7 | -Dsun.net.inetaddr.ttl=2 |
无 | 26 | 2 | ✅ |
| S8 | -Dsun.net.inetaddr.ttl=2 |
static final 字段 | 26 | 2 | ✅ |
| S9 | -Dsun.net.inetaddr.ttl=2 |
main 中 getLogger 先 | 24 | 2 | ✅ |
| S10 | java.security 文件 | 无 | 26 | 2 | ✅ |
| S11 | java.security 文件 | static final 字段 | 26 | 2 | ✅ |
TTL=2,每秒解析一次共 20 次。生效时 cachePolicy=2、DNS 查询数 ≈ 20+;失效时 cachePolicy=30、查询数 ≈ 4
6(这 46 次是 JVM 启动期 hostname/reverse-lookup 等开销,业务期完全命中缓存)。
3.1 四种主流设置方式的最终判定
| 设置方式 | 验证场景 | 判定 |
|---|---|---|
-Dnetworkaddress.cache.ttl=N |
S1, S6 | ❌ 完全无效(写错 property store) |
代码 Security.setProperty(...) |
S2, S3 / S4, S5 | ⚠️ 理论可行,实际极易被抢跑 |
-Dsun.net.inetaddr.ttl=N |
S7, S8, S9 | ✅ 稳定生效,能兜底任意代码顺序 |
java.security 文件 networkaddress.cache.ttl=N |
S10, S11 | ✅ 稳定生效,能兜底任意代码顺序 |
4. 为什么 log4j2 会”覆盖”你的设置(机制澄清)
严格说不是覆盖,是读取时机错过。
1 | 时间轴(坏写法 S4): |
4.1 抢跑者远不止 log4j2
任何在 Security.setProperty 之前触发 InetAddress.getXxxByName/getLocalHost 的代码,都会冻结 cachePolicy。常见的有:
| 抢跑者 | 触发路径 |
|---|---|
| log4j2 / logback | LogManager.getLogger() 初始化 LoggerContext,配置里 ${hostName}/%H 或某些默认 Lookup 触发 |
| java.util.logging (JUL) | LogRecord 默认携带 hostname |
| Spring Boot | 启动 banner、Actuator、InetUtils 自动选 IP |
| Tomcat / Jetty / Netty | bind() 时解析监听地址;Netty 还会初始化 DNS resolver |
| Kafka client | KafkaProducer/Consumer 构造时 ClientUtils.parseAndValidateAddresses 解析 bootstrap 域名 |
| HikariCP / Druid 等连接池 | 初始化时连第一个 DB 实例(解析 JDBC URL host) |
| JMX / RuntimeMXBean | getRuntimeMXBean().getName() 拼 pid@hostname,触发 reverse lookup |
| Micrometer / Prometheus | 注册 metrics 时打 hostname tag |
| Apache HttpClient / OkHttp | client 构造时可能预解析 |
Java agent (-javaagent) |
APM agent (Skywalking、Pinpoint、NewRelic、DataDog) 在 premain() 阶段就跑,早于 main,常自报 hostname——这条最阴险,业务代码完全没写错,agent 默默把你的 setProperty 干掉了 |
只要这个清单里任意一项先于业务代码 Security.setProperty,TTL 就被冻在默认值。
5. 业务方避坑清单(必读)
5.1 必须避免
| ❌ 错误写法 | 错误原因 |
|---|---|
-Dnetworkaddress.cache.ttl=30 |
写进了 System Properties,但 InetAddressCachePolicy 只从 Security Properties 读这个名字。完全无效。 |
Security.setProperty(...) 写在 static {} 块或字段初始化里 |
与其他 static 初始化的执行顺序不可控,几乎注定被 log4j 抢跑 |
| 把 setProperty 放在某个工具类的 init 方法里、依赖被调用一次 | 同上,时机不可控 |
| 只设正向、不设负向 | 默认负向 10 秒(部分 Linux 发行版打包甚至改过这个值);DNS 失败的负向缓存仍然影响故障切换 |
5.2 推荐做法(任选其一)
方案 A:JVM 启动参数(最方便、最不容易出错)
1 | java -Dsun.net.inetaddr.ttl=30 \ |
适用场景:业务团队普遍写 java.security 文件不方便,或者运维平台/发布系统能统一注入 JVM 参数。
方案 B:修改 java.security 配置文件(系统级、所有 Java 进程统一生效)
编辑 $JAVA_HOME/conf/security/java.security(Java 8 是 $JAVA_HOME/jre/lib/security/java.security):
1 | networkaddress.cache.ttl=30 |
注意:
- 该文件可能本来就有这两行,要么注释掉、要么是默认值。不要新增重复行。
- 修改后所有运行该 JDK 的进程都会生效,改之前确认无副作用。
- RPM 装的 JDK 在升级时会用包里的版本覆盖该文件,升级流程要带回这个修改。
- 实测发现 RHEL/Rocky 默认就有
networkaddress.cache.negative.ttl=10这行(不是 JDK 默认 0),删它会改变行为。
方案 C:代码内 Security.setProperty(...) —— 不推荐
如果实在因为某些原因要用代码方式设置:
1 | public static void main(String[] args) { |
且必须确保这个 main 类不持有任何 static final Logger 字段——否则 log4j 在类加载期就跑完了。
但即便这样,仍然防不住 -javaagent 在 premain 阶段抢跑。所以代码方式只在简单场景下能用,在带 APM agent 的环境里基本必败。
5.3 取值建议
| 场景 | 正向 TTL | 负向 TTL |
|---|---|---|
| 一般业务 | 30 秒 | 10 秒 |
| 频繁 DNS 切换/扩缩容 | 10 ~ 30 秒 | 5 ~ 10 秒 |
| DNS 极少变更、追求性能 | 60 ~ 300 秒 | 30 秒 |
| 永远不要 | -1(永久缓存)—— 这是带 SecurityManager 时的默认值,DNS 一变就再也感知不到 |
0 也要慎用——DNS 失败时会变成每次解析打 DNS server,可能造成查询风暴 |
6. 验证与诊断
6.1 一行命令验证 TTL 是否生效
1 | # 替换 -Dsun.net.inetaddr.ttl=2 为你实际的写法,看打印是不是 2 |
或者写一个最简验证类:
1 | public class TtlCheck { |
运行:java --add-opens java.base/sun.net=ALL-UNNAMED TtlCheck
期望输出:Effective TTL = 30 seconds(或你设置的值);如果是其他奇怪值就有问题。
6.2 定位是谁在抢跑(深度排查)
1 | java -Xlog:class+init=info ...your-app... 2>&1 | grep -E 'InetAddressCachePolicy|InetAddress\$' |
能看到 InetAddressCachePolicy 在哪个时点、被哪个类引用首次初始化。结合 -XX:+TraceClassLoading 或在该类静态块挂断点能看到完整调用栈。
6.3 用 tcpdump 验证业务运行期的 DNS 查询频次
1 | # 在业务实例上抓 30 秒 |
把这个数字除以 30 得到每秒查询频次。如果查询频次 << 1/TTL,说明 TTL 比预期大;如果 ≈ 业务每秒解析次数,说明缓存几乎不命中。
7. 附录
7.1 实验脚本与原始日志
- 实验目录:
~/q/tmp/jvm-dns-ttl-log4j-test/(本地)//root/jvm-dns-ttl-log4j-test/(测试机 172.30.65.103) - 主程序:
TtlProbe.java - 跑 S1~S9:
bash run-experiment.sh - 跑 S10/S11:
bash run-jsec-experiment.sh(自动备份/还原 java.security) - 原始 pcap + log:
results/
7.2 历史背景:为什么 JVM 有”两个名字”
- JDK 1.4 之前:只有
sun.net.inetaddr.ttl,Sun 私有 system property - JDK 1.4(2002 年):把 DNS 缓存策略正式化到 Security property 标准里,引入
networkaddress.cache.ttl作为官方名 - 此后至今:旧名
sun.net.inetaddr.ttl没废弃,作为 fallback 保留——保兼容
设计层面的内在矛盾:官方名是 Security property,但 JVM 最方便的设置方式(-D)只能设 System property。绝大多数业务方踩坑,根源就是直觉地把这两者拼成 -Dnetworkaddress.cache.ttl=N,然后悄悄失效。
7.3 相关资源
- Java 官方文档(Networking Properties):
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/net/doc-files/net-properties.html - OpenJDK 源码 InetAddressCachePolicy.java:
https://github.com/openjdk/jdk17u/blob/master/src/java.base/share/classes/sun/net/InetAddressCachePolicy.java - 个人验证 demo(带 log4j2 的 DNS 解析重现):
https://github.com/plantegg/DnsDemo
7.4 验证环境
- OS:Rocky Linux 8.10
- JDK:OpenJDK 17.0.19(Red_Hat-17.0.19.0.10-1)
- log4j2:2.20.0
- 测试域名:3 条 A 记录的内网域名
- 验证日期:2026-05-25