Java DNS解析问题-以Socket Client为例

JDK 的InetSocketAddress被设计了为一个不可变的对象,在Socket等长连接使用时需要注意DNS映射关系更新问题。

问题描述

最近遇到一个zookeeper-client 更新DNS映射关系未生效问题,记录一下该问题的原因。

域名映射关系:

1
127.0.0.1 test.lan-gl-42.dev

java应用程序

1
-Dsun.net.inetaddr.ttl=15

Java Socket Client Code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void testSocket() {
String host = "test.lan-gl-42.dev";
int port = 9876;
try (Socket socket = new Socket(host, port);
ObjectOutputStream oos = new ObjectOutputStream(socket.getOutputStream())) {
//write to socket using ObjectOutputStream
System.out.println("Sending request to Socket Server,socket remote:" + socket.getRemoteSocketAddress().toString());
while (true){
Thread.sleep(10000);
System.out.println("Sending request to Socket Server,socket remote:" + socket.getRemoteSocketAddress().toString());
}
//Thread.sleep(10000);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}catch (Exception e){
e.printStackTrace();
}
}

执行日志:

1
2
3
 Sending request to Socket Server,socket remote:test.lan-gl-42.dev/127.0.0.1:9876
Sending request to Socket Server,socket remote:test.lan-gl-42.dev/127.0.0.1:9876
Sending request to Socket Server,socket remote:test.lan-gl-42.dev/127.0.0.1:9876
修改域名的映射关系为:

1
10.224.17.250 test.lan-gl-42.dev

** socket连接的地址依然是 127.0.0.1 **

原因

Java进程及OS DNS解析过程: 图片来源 JVM会缓存DNS name lookups,它在指定的时间段内缓存IP地址,称为生存时间(time-to-live, TTL)

因为InetSocketAddress被设计了为一个不可变的对象(immutable object ),Socket 对象实例的InetSocketAddress 对象,只在实例化对象时解析hostName与hostAddress的映射关系。

修复方法

可以借鉴 zookeeper 的 StaticHostProvider.java 调用next()方法在解析一次域名与IP地址的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public InetSocketAddress next(long spinDelay) {
boolean needToSleep = false;
InetSocketAddress addr;
synchronized(this) {
if (this.reconfigMode) {
addr = this.nextHostInReconfigMode();
if (addr != null) {
this.currentIndex = this.serverAddresses.indexOf(addr);
return this.resolve(addr);
}

this.reconfigMode = false;
needToSleep = spinDelay > 0L;
}

++this.currentIndex;
if (this.currentIndex == this.serverAddresses.size()) {
this.currentIndex = 0;
}

addr = (InetSocketAddress)this.serverAddresses.get(this.currentIndex);
needToSleep = needToSleep || this.currentIndex == this.lastIndex && spinDelay > 0L;
if (this.lastIndex == -1) {
this.lastIndex = 0;
}
}

if (needToSleep) {
try {
Thread.sleep(spinDelay);
} catch (InterruptedException var7) {
LOG.warn("Unexpected exception", var7);
}
}

return this.resolve(addr);
}

private InetSocketAddress resolve(InetSocketAddress address) {
try {
String curHostString = address.getHostString();
List<InetAddress> resolvedAddresses = new ArrayList(Arrays.asList(this.resolver.getAllByName(curHostString)));
if (resolvedAddresses.isEmpty()) {
return address;
} else {
Collections.shuffle(resolvedAddresses);
return new InetSocketAddress((InetAddress)resolvedAddresses.get(0), address.getPort());
}
} catch (UnknownHostException var4) {
LOG.error("Unable to resolve address: {}", address.toString(), var4);
return address;
}
}

扩展

JVM的DNS缓存时间可用通过一下参数配置:

1
2
3
4
5
#globally, for all applications that use the JVM. Set networkaddress.cache.ttl in the $JAVA_HOME/jre/lib/security/java.security file
networkaddress.cache.ttl

#for your application only, set networkaddress.cache.ttl in your application’s initialization code:
sun.net.inetaddr.ttl

networkaddress.cache.ttl 是全局参数,所有使用该JVM的程序 sun.net.inetaddr.ttl JVM应用的配置参数,一般通过 -Dsun.net.inetaddr.ttl=10j进行配置

参考