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 等抢跑而失效。生产配置不要依赖这种方式。

如果还能拿走第三条:

推荐配置(任选其一,不用全选)

  1. 启动参数:-Dsun.net.inetaddr.ttl=30 -Dsun.net.inetaddr.negative.ttl=10
  2. 或修改 $JAVA_HOME/conf/security/java.securitynetworkaddress.cache.ttl=30networkaddress.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
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class InetAddressCachePolicy {
private static volatile int cachePolicy; // ← 真正起作用的值

static { // ← 类首次加载时执行 1 次,之后再不读取
Integer tmp = AccessController.doPrivileged(...) {
// 1) 优先读 Security.getProperty("networkaddress.cache.ttl")
// 2) fallback 读 System.getProperty("sun.net.inetaddr.ttl")
};
cachePolicy = (tmp != null) ? tmp
: (hasSecurityManager ? FOREVER : 30); // 默认 30s
}

public static int get() { return cachePolicy; }
}

三个关键事实:

  1. 静态块只跑一次(class load 时),之后不再读 Security/System property。
  2. 所以业务代码里再调 Security.setProperty(...) 改 TTL,InetAddressCachePolicy 不会回头读——值已经被冻在 cachePolicy 字段里了。
  3. 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 值
  • 间接证据tcpdumpudp 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.getLoggersetProperty(手写错顺序)
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、查询数 ≈ 46(这 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
时间轴(坏写法 S4):

t0 类加载 MyClass(业务的 main 类)
t0a → static final Logger LOG = LogManager.getLogger(MyClass.class); ← 类加载阶段
t0b → log4j2 LoggerContext init
t0c → 解析配置 / 注册 ServiceLoader / hostname lookup
t0d → 第一次调到 InetAddress.getXxxByName()
t0e → 触发 InetAddressCachePolicy.<clinit>
t0f → Security.getProperty("networkaddress.cache.ttl") = null
t0g → System.getProperty("sun.net.inetaddr.ttl") = null
t0h → 用默认值 30,写入 cachePolicy 字段,**永久冻结**

t1 main() 进入
t1a Security.setProperty("networkaddress.cache.ttl", "5");
↑ 写进了 Security Properties Map,但 cachePolicy 字段不再被读
t1b 之后所有 InetAddress 操作 → InetAddressCachePolicy.get() → 30

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
2
3
java -Dsun.net.inetaddr.ttl=30 \
-Dsun.net.inetaddr.negative.ttl=10 \
-jar app.jar

适用场景:业务团队普遍写 java.security 文件不方便,或者运维平台/发布系统能统一注入 JVM 参数。

方案 B:修改 java.security 配置文件(系统级、所有 Java 进程统一生效)

编辑 $JAVA_HOME/conf/security/java.security(Java 8 是 $JAVA_HOME/jre/lib/security/java.security):

1
2
networkaddress.cache.ttl=30
networkaddress.cache.negative.ttl=10

注意:

  • 该文件可能本来就有这两行,要么注释掉、要么是默认值。不要新增重复行
  • 修改后所有运行该 JDK 的进程都会生效,改之前确认无副作用
  • RPM 装的 JDK 在升级时会用包里的版本覆盖该文件,升级流程要带回这个修改
  • 实测发现 RHEL/Rocky 默认就有 networkaddress.cache.negative.ttl=10 这行(不是 JDK 默认 0),删它会改变行为。

方案 C:代码内 Security.setProperty(...) —— 不推荐

如果实在因为某些原因要用代码方式设置:

1
2
3
4
5
6
public static void main(String[] args) {
// 必须是 main 第一行(甚至要早于该类的 static field 初始化,但 static field 在 main 之前已执行)
Security.setProperty("networkaddress.cache.ttl", "30");
Security.setProperty("networkaddress.cache.negative.ttl", "10");
// ……
}

且必须确保这个 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
2
3
4
5
# 替换 -Dsun.net.inetaddr.ttl=2 为你实际的写法,看打印是不是 2
java -Dsun.net.inetaddr.ttl=2 \
--add-opens java.base/sun.net=ALL-UNNAMED \
-e 'System.out.println(((Integer)Class.forName("sun.net.InetAddressCachePolicy").getMethod("get").invoke(null)));' \
2>/dev/null

或者写一个最简验证类:

1
2
3
4
5
6
7
8
public class TtlCheck {
public static void main(String[] args) throws Exception {
java.lang.reflect.Method m = Class.forName("sun.net.InetAddressCachePolicy")
.getMethod("get");
m.setAccessible(true);
System.out.println("Effective TTL = " + m.invoke(null) + " seconds");
}
}

运行: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
2
3
4
# 在业务实例上抓 30 秒
sudo tcpdump -i any -nn 'udp port 53' -w /tmp/dns.pcap &
sleep 30 ; sudo killall tcpdump
sudo tcpdump -nn -r /tmp/dns.pcap | grep '<your-domain>' | wc -l

把这个数字除以 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 相关资源

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