背景
JVM DNS Cache机制,在应用程序使用域名来访问外部服务时可能会存在问题。
举个例子:
在做应用容灾秒级切换场景中,应用层通过域名访问中间件服务,若应用层DNS Cache没有及时更新,则无法做到秒级切换,因为DNS Cache中缓存的是旧的IP地址。
特别是,多数中间件服务会对外提供域名的方式来提供服务,对外屏蔽底层的高可用切换。应用程序往往是先解析域名,获得IP地址,再通过IP跟中间件进行数据传输。若IP地址迟迟没更新,即使中间件服务做了高可用切换,应用层还是感知不到。
本文对JVM DNS Cache机制进行探究分析。
JVM DNS解析域名是使用java.net.InetAddress这个类来实现域名解析服务。
常见的用法如下
InetAddress inetAddress = InetAddress.getByName("youkuaiyun.com");
System.out.println(inetAddress);
InetAddress源码剖析
查看InetAddress源码。
主要是这个内部类来缓存过期时间、地址。
验证
原理
通过定期解析域名地址,并设置休眠时间,查看缓存的过期时间
方法
通过反射机制查看Cache过期时间。
环境
使用JVM 11来作为验证环境
代码
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Security;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
public class DnsCache {
public static void main(String[] args) throws Exception {
// 判断是否有Security Manager
System.out.println(System.getSecurityManager());
// negative取得到值
System.out.println(Security.getProperty("networkaddress.cache.negative.ttl"));
// 这个取不到值,返回结果是null
System.out.println(Security.getProperty("networkaddress.cache.ttl"));
printDns("www.baidu.com");
System.out.println("Sleep 10 Second");
TimeUnit.SECONDS.sleep(10);
printDns("www.baidu.com");
System.out.println("Sleep 19 Second");
TimeUnit.SECONDS.sleep(19);
printDns("www.baidu.com");
System.out.println("Sleep 2 Second");
TimeUnit.SECONDS.sleep(2);
printDns("www.baidu.com");
System.out.println("Sleep 10 Second");
TimeUnit.SECONDS.sleep(10);
printDns("www.baidu.com");
}
private static void printDns(String domain) throws UnknownHostException, NoSuchFieldException, IllegalAccessException {
// do dns resolve
InetAddress inetAddress = InetAddress.getByName(domain);
System.out.println(inetAddress);
Class inetAddressClass = java.net.InetAddress.class;
final Field cacheField = inetAddressClass.getDeclaredField("cache");
cacheField.setAccessible(true);
final Map cacheMap = (Map) cacheField.get(inetAddressClass);
cacheMap.forEach((k, v) -> {
Class cacheEntryClass = v.getClass();
try {
Field expf = cacheEntryClass.getDeclaredField("expiryTime");
expf.setAccessible(true);
long expires = (Long) expf.get(v);
Field af = cacheEntryClass.getDeclaredField("inetAddresses");
af.setAccessible(true);
InetAddress[] addresses = (InetAddress[]) af.get(v);
List<String> ads = new ArrayList<String>(addresses.length);
for (InetAddress add : addresses) {
ads.add(add.getHostAddress());
}
System.out.println(new Date() + ",Host:" + k + ", Cache Expires At:" + new Date(expires) + ",Address:" + ads);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
}
结果
可以看到,在29秒前,Cache还是不变。
在31秒后,Cache发生了更改。
猜测缓存默认是30秒。
官方解释
JDK 8 & 11
Oracle JDK 8
https://docs.oracle.com/javase/8/docs/technotes/guides/net/properties.html
JDK 11
Networking Properties (Java SE 11 & JDK 11 )
若设置-1,则永不过期(在JVM运行期间),需要重启JVM或者手动设置此值才会变更。
查看Security文件(java安装环境下,%JAVA_HOME%/conf/security/java.security)
大概翻译下:默认是永不过期。但是有个例外,如果没有设置Security Manager,则默认过期时间是30秒。
那么怎么查看有没有设置Security Manager呢
输出为Null,则表示没有设置,所以本文的JVM环境,DNS Cache TTL默认就是30秒
System.out.println(System.getSecurityManager());
其它版本JDK
其它版本的JDK,可以在帮助中心,全局搜一下此参数。
JVM如何读取DNS TTL配置
源码
InetAddress中使用了sun.net.InetAddressCachePolicy来获取缓存策略,可以细看下这个类
读取过程
优先从Security Policy配置文件读取
networkaddress.cache.ttl
读取不到就读取系统变量
sun.net.inetaddr.ttl
若此系统变量还是读取不到,则判断有没有设置SecurityManager,没有的化,就设置默认值30.
如何查看JVM DNS TTL
有了上述的加载过程,就比较容易理解了。
import java.security.Security;
public class JVMDnsCacheManager {
public static void main(String[] args) throws Exception {
// 判断是否有Security Manager
System.out.println(System.getSecurityManager());
System.out.println(Security.getProperty("networkaddress.cache.ttl"));
System.out.println(System.getProperty("sun.net.inetaddr.ttl"));
// 设置后,可以取到值
Security.setProperty("networkaddress.cache.ttl" , "20");
System.out.println(Security.getProperty("networkaddress.cache.ttl"));
}
}
如何修改JVM DNS TTL
方式一 使用代码更改TTL
注意,必须在使用InetAddress之前更改,若程序已经使用了InetAddress进行域名解析,则再使用程序的方式进行修改就不生效。(所以无法动态更改此参数,一旦设置,需要重启应用)
请看如下试验
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.Security;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.text.SimpleDateFormat;
import java.util.concurrent.TimeUnit;
public class DnsCache {
public static void main(String[] args) throws Exception {
// 判断是否有Security Manager
System.out.println(System.getSecurityManager());
// negative取得到值
System.out.println(Security.getProperty("networkaddress.cache.negative.ttl"));
// 这个取不到值,返回结果是null
System.out.println(Security.getProperty("networkaddress.cache.ttl"));
// change the Cache TTL to 10 Second
System.out.println("We set the ttl to 10 Second");
Security.setProperty("networkaddress.cache.ttl" , "10");
printDns("www.baidu.com");
System.out.println("Sleep 2 Second");
TimeUnit.SECONDS.sleep(2);
printDns("www.baidu.com");
System.out.println("Sleep 9 Second,Total Sleep 11 Second");
TimeUnit.SECONDS.sleep(9);
printDns("www.baidu.com");
TimeUnit.SECONDS.sleep(5);
System.out.println("Sleep 5 Second,Total Sleep 16 Second");
printDns("www.baidu.com");
TimeUnit.SECONDS.sleep(15);
// 验证在使用InetAddress进行域名解析后,再设置Cache TTL是否生效
System.out.println("We set the ttl to 3 Second");
Security.setProperty("networkaddress.cache.ttl" , "3");
printDns("www.baidu.com");
TimeUnit.SECONDS.sleep(5);
System.out.println("Sleep 5 Second,Total Sleep 5 Second");
printDns("www.baidu.com");
TimeUnit.SECONDS.sleep(6);
System.out.println("Sleep 6 Second,Total Sleep 11 Second");
printDns("www.baidu.com");
}
private static void printDns(String domain) throws UnknownHostException, NoSuchFieldException, IllegalAccessException {
// do dns resolve
InetAddress inetAddress = InetAddress.getByName(domain);
System.out.println(inetAddress);
Class inetAddressClass = java.net.InetAddress.class;
final Field cacheField = inetAddressClass.getDeclaredField("cache");
cacheField.setAccessible(true);
final Map cacheMap = (Map) cacheField.get(inetAddressClass);
cacheMap.forEach((k, v) -> {
Class cacheEntryClass = v.getClass();
try {
Field expf = cacheEntryClass.getDeclaredField("expiryTime");
expf.setAccessible(true);
long expires = (Long) expf.get(v);
Field af = cacheEntryClass.getDeclaredField("inetAddresses");
af.setAccessible(true);
InetAddress[] addresses = (InetAddress[]) af.get(v);
List<String> ads = new ArrayList<String>(addresses.length);
for (InetAddress add : addresses) {
ads.add(add.getHostAddress());
}
System.out.println(new Date() + ",Host:" + k + ", Cache Expires At:" + new Date(expires) + ",Address:" + ads);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
});
}
}
试验结果
所以最好在整个应用Init入口进行初始化设置,防止不生效。
方式二 修改Security配置文件
方式 三 设置JVM启动变量
如下所示,设置启动参数。
-Dsun.net.inetaddr.ttl=10
按照JVM读取DNS Cache TTL逻辑,正确读取到了sun.net.inetaddr.ttl设置的10秒
总结
影响JVM Dns Cache主要有两个值
- 一个是解析成功的的缓存时间(networkaddress.cache.ttl),默认不过期(-1);若没有设置Security Manager,则默认是30秒。
- 一个是解析不成功时的缓存时间(networkaddress.cache.negative.ttl),默认是10秒。设置成0,表示不缓存,设置成-1,表示一直缓存,不过期
- 推荐使用方式三,直接设置JVM启动参数的方式更改DNS Cache TTL值