### 01 概述
接连几个客户在直接使用 Amazon Java SDK 或者其他应用依赖 Amazon Java SDK 时,偶尔会遇到 API 端点无法解析的错误,也就是 Java 报出的 UnknownHostException 异常。这种问题偶尔发生,很难追踪和定位故障。本文从几个问题出发,希望能给予大家一个非常合适的解决方案。为了方便快速阅读,先将问题和简单答案给出,后续再细细分析。
> **Q**:Amazon Java SDK 会在遇到这种 UnknownHostException 问题时会自动重试吗?
>
> **A**:会,默认会重试 3 次。
>
> **Q**:为什么重试了还是出错?
>
> **A**:大部分情况下 3 次重试都在数秒(甚至在一秒)内完成,而 JVM 默认针对失败的解析会有 10 秒的缓存(见下个问题),毫无疑问重试会继续失败。
>
> **Q**:JVM 的 DNS 缓存机制是什么?
>
> **A**:主要由这两个配置参数来控制:networkaddress.cache.ttl 控制成功的解析缓存时间,如果没有启用 Java Security Manager,默认是 30 秒;如果启用了,则是 -1,表示永久。networkaddress.cache.negative.ttl 控制失败的解析缓存时间,默认是 10 秒。
>
> **Q**:JVM 的缓存 DNS 的时候会遵守 DNS 服务器返回的 TTL 吗?
>
> **A**:不会,详见下文。
>
> **Q**:我该如何避免 UnknownHostException?
>
> **A**:设置 networkaddress.cache.ttl 为 30 秒,设置 networkaddress.cache.negative.ttl 为 0 秒。
>
> **注意**:该配置只是为了解决 DNS 服务器偶尔无法解析的情况。
接下来,我会通过一系列实验和源码解读来详细解释以上问题的答案。
### 02 实验准备
我在 [Amazon VPC](https://aws.amazon.com/cn/vpc/?trk=cndc-detail) 中布置了一个基于 BIND 的 DNS 服务器(172.31.21.120),另外一台 [Amazon EC2 ](https://aws.amazon.com/cn/ec2/?trk=cndc-detail)作为客户端(172.31.189.232)。
![image.png](https://dev-media.amazoncloud.cn/1e72a6bb84664fbe9b07e2b7e3d831b2_image.png "image.png")
DNS 服务器的配置中,现在注释部分是为了模拟解析失败的问题,而配置用来接管 [Amazon S3](https://aws.amazon.com/cn/s3/?trk=cndc-detail) 地址的解析。下面 amazonamaozn.com.cn 匹配域名都直接转发到 VPC.2 的 Amazon Route53 解析服务。
```
/*
zone "s3.cn-north-1.amazonaws.com.cn" {
type master;
file "s3.cn-north-1.zone";
};
*/
zone "amazonaws.com.cn" {
type forward;
forwarders { 172.31.0.2;};
};
```
客户端的 DNS 服务器设置为:
```
[root@ip-172-31-189-232 ~]# cat /etc/resolv.conf
options timeout:2 attempts:5
; generated by /usr/sbin/dhclient-script
search cn-north-1.compute.internal
nameserver 172.31.21.120
```
Java JDK 版本选择了 1.8.0,Amazon Java SDK 版本为 1.11.563。
这里采用了一个简单的 S3 headBucket 请求来做测试,具体代码如下:
```
package org.beta.manages3;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.HeadBucketRequest;
import com.amazonaws.services.s3.model.HeadBucketResult;
import org.apache.log4j.Logger;
import sun.net.InetAddressCachePolicy;
/**
* @author Beta Zhou
*/
public class DnsIssueTest {
private static final Logger LOGGER = Logger.getLogger(DnsIssueTest.class);
public static void main(String[] args) throws InterruptedException {
int sleepInterval = 5; // Default interval between each request.
//get bucket and sleep interval from args
if (args.length < 1) {
System.out.println("Usage: DnsIssueTest <bucketName> <sleepInterval>");
System.exit(1);
} else if (args.length == 2) {
sleepInterval = Integer.parseInt(args[1].trim());
}
String bucketName = args[0].trim();
// Look up current security settings
String cacheTtl = java.security.Security.getProperty("networkaddress.cache.ttl");
LOGGER.error("networkaddress.cache.ttl = " + cacheTtl);
String negativeTtl = java.security.Security.getProperty("networkaddress.cache.negative.ttl"); LOGGER.error("networkaddress.cache.negative.ttl = " + negativeTtl);
// Get current cache policy
int cachePolicy = InetAddressCachePolicy.get();
LOGGER.error("cachePolicy = " + cachePolicy);
int cacheNegativePolicy = InetAddressCachePolicy.getNegative();
LOGGER.error("cacheNegativePolicy = " + cacheNegativePolicy);
// S3 client
AmazonS3 s3Client = AmazonS3ClientBuilder.standard().build();
//head a s3 bucket for 5 times
for (int i = 0; i < 5 ; i++) {
try {
LOGGER.error("-------------------------------------");
LOGGER.error("Test round: "+i);
LOGGER.error("-------------------------------------");
HeadBucketRequest headBucketRequest = new HeadBucketRequest(bucketName);
HeadBucketResult headBucketResult = s3Client.headBucket(headBucketRequest);
if (headBucketResult != null) {
LOGGER.info("Bucket is there: " + bucketName);
}
} catch(Exception e){
// The call was transmitted successfully, but Amazon S3 couldn't process
// it and returned an error response.
e.printStackTrace();
LOGGER.error("Error: " + e.getMessage());
}
//sleep sleepInterval seconds
Thread.sleep(sleepInterval * 1000L);
}
}
}
```
同时,为了看到 SDK 的重试情况,创建了 log4j.properties,内容如下:
```
log4j.rootLogger=WARN, A1
log4j.appender.A1=org.apache.
log4j.ConsoleAppenderlog4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%d [%t] %-5p %c - %m%n
# Log all HTTP content (headers, parameters, content, etc) for
# all requests and responses. Use caution with this since it can
# be very expensive to log such verbose data!
#log4j.logger.org.apache.http.wire=DEBUG
log4j.logger.com.amazonaws.request=DEBUG
```
### 03 测试过程
#### **3.1 正常情况**
首先进行一个正常情况下的测试,这里用了 tcpdump 来抓包。
程序运行日志(由于篇幅,只截取了 4 次运行结果),一开始为 cache 的相关配置:
![image.png](https://dev-media.amazoncloud.cn/08da6e89bf8d4c0d90f4984c21184f3f_image.png "image.png")
以下为 tcpdump 结果,抓取了与 DNS 服务器之间的通讯信息:
![image.png](https://dev-media.amazoncloud.cn/bfef0cd58a724d52a22c116ee1aa6ae1_image.png "image.png")
以下为 dig betatest.s3.cn-north-1.amazonaws.com.cn 的结果,通过多次执行,发现 TTL 最大为 4 秒。
![image.png](https://dev-media.amazoncloud.cn/1a13cff531e94e8c94920ff73be28ddb_image.png "image.png")
从上面截图上可以看到以下几点:
1. networkaddress.cache.ttl 默认是没有配置的,而生效 cache 时间是 30 秒。
2. networkaddress.cache.negative.ttl 默认是 10 秒。
3. 运行的几次尝试中,只有一开始有抓到 DNS 的数据包。说明 Java 是缓存了 DNS 的,后续请求无需再次解析。
4. DNS 返回的 S3 地址的 TTL 是 4 秒,每次执行间隔为 5 秒,所以显然 Java 在缓存的时候没有考虑该 TTL,还是按照 cachePolicy 的 30 秒来缓存。
#### **3.2 模拟无法解析情况**
该情况下,DNS 服务器将返回该域名不存在。具体是通过接管 s3.cn-north-1.amazonaws.com.cn 域的解析,并不设置 betatest.s3.cn-north-1.amazonaws.com.cn 的 A 记录来完成。
```
zone "s3.cn-north-1.amazonaws.com.cn" {
type master;
file "s3.cn-north-1.zone";
};
zone "amazonaws.com.cn" {
type forward;
forwarders { 172.31.0.2;};
};
```
dig betatest.s3.cn-north-1.amazonaws.com.cn 的结果显示没有该域名记录:
![image.png](https://dev-media.amazoncloud.cn/c952f293015c4b9face8c0deded8baeb_image.png "image.png")
日志只截取了四次执行结果,都是以 UnknownHostException 为错误结束的。
![image.png](https://dev-media.amazoncloud.cn/6f3d0df5e75b4364bd20b7a924f22f48_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/5ba59b4fd6cc4ec9935778390c3930e2_image.png "image.png")
下面的 tcpdump 只有包含 3 次请求,而非 5 次,和上面的请求日志并非完全匹配,说明存在了缓存机制。这里提一下在客户端/etc/resolve.conf 中配置了 search domain,所以在查询 betatest.s3.cn-north-1.amazonaws.com.cn 失败后,又尝试了 betatest.s3.cn-north-1.amazonaws.com.cn.cn-north-1.computer.internal 域名。
![image.png](https://dev-media.amazoncloud.cn/001b21b53e7b45e2880bd05896820e1e_image.png "image.png")
总结来说,可以看到以下几点:
1. S3 SDK 在失败的时候自动执行了 3 次重试,每次重试间隔在几到几百毫秒之间,逐步扩大。
2. 在每次重试时,并没有再次发生 DNS 请求,说明失败的解析也被缓存了。这就是通过 networkaddress.cache.negative.ttl 来控制的,默认 10 秒。
3. 通过 tcpdump,三次请求的间隔为 11 秒,正好印证了上面设置。而中间 33 秒时候的请求就没有再发送 DNS 请求。
#### **3.3 模拟 DNS 服务器没有响应**
我们通过 iptables -A INPUT -s 172.31.189.232 -p udp –dport 53 -j DROP 命令来阻断所有来自客户端的 DNS 请求来模拟 DNS 服务器没有响应的情况。从下图可以看到第一次请求到重试之间花了 20 秒,这是由于在/etc/resolve.conf 设置了重试 5 次,每次 2 秒,并且存在 search domain 多做一轮,所以总共 DNS 花了 20 秒在尝试查询,这个可以从 tcpdump 的结果确认。
![image.png](https://dev-media.amazoncloud.cn/35b68538134546079b4e469d2918415f_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/9a2e0145433b440eaaa3578774b59b35_image.png "image.png")
下面的 tcpdump 我用红绿框表示了两次查询,实际上还有一次,发生在 14:39:29,限于篇幅就不放更多截图了。
![image.png](https://dev-media.amazoncloud.cn/3f6c730aa04c40dab875ae92561aad0e_image.png "image.png")
总结来说看到以下几点:
1. 当 DNS 服务器没有响应时,请求会有较长时间在尝试解析,这和客户端 DNS 解析配置和有关。
2. DNS 解析失败同样会被缓存,在有效时间内不再向 DNS 服务器解析,直到过期后才会继续尝试。这点和测试 3.2 结果类似。
#### **3.4 模拟无法解析情况时不缓存**
从上面看到,因为有 negative cache 的存在,有时 DNS 服务器短暂故障而无法解析,会导致后续重试时继续失败。这次我们将 java.security 里面的配置改为 networkaddress.cache.negative.ttl=0,其他配置还是按照 3.2 的方式。
下图可以看到生效的 cacheNegativePolicy 确定为 0 秒。在四次请求的 1 秒内,tcpdump 显示有四次查询,虽然每次都返回了无此域名的错误。
![image.png](https://dev-media.amazoncloud.cn/0b13ee47e9214eedb3639d8b2ecf331d_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/330a32ac2589479e9815fe7bf632a757_image.png "image.png")
总结来说看到以下:
1. 当 networkaddress.cache.negative.ttl=0 时,失败的解析不再缓存,每次重试都会向 DNS 服务器方式请求。
2. 针对偶尔 DNS 服务器出错的情况,这个配置可以让程序在重试时有机会成功。
#### **3.5 模拟 DNS 服务器没有响应时不缓存**
我们设置了 networkaddress.cache.negative.ttl=0,其他按照 3.3 的方式。下图可以看到每次重试时间间隔达到了 20 秒。
![image.png](https://dev-media.amazoncloud.cn/47bf0ad7fbb146e9b768a2cd5288afae_image.png "image.png")
Tcpdump 也显示了每次重试都需要经过多次尝试。
![image.png](https://dev-media.amazoncloud.cn/e704d50225f747aca34bf381f4ba6533_image.png "image.png")
总结来说看到以下:
1. 当 networkaddress.cache.negative.ttl=0 时,失败的解析不再缓存,每次重试都会向 DNS 服务器方式请求。
2. 由于 DNS 没有回复而导致了较长的重试时间,真可能会导致应用卡住,问题没有及时报告。
### 04 详细分析
#### **4.1 缓存配置**
Sun Java 官方网站对于 DNS 缓存的两次参数描述还是很简单的:
https\://docs.oracle.com/javase/8/docs/technotes/guides/net/properties.html?trk=cndc-detail
![image.png](https://dev-media.amazoncloud.cn/609828f45203413da9719da2fdfc2d4b_image.png "image.png")
首先针对 networkaddress.cache.ttl,在有没有启用 Security Manager 功能情况下结果是不同的。在 Security Manager 启用时为 -1,表示永久缓存。这是 Java 为了避免遭受 DNS spoofing(也叫 **DNS cache poisoning**)攻击而设置的,这样可以避免通过黑客篡改 DNS 来攻击。而 Amazon 服务的 API 终端节点对应的 IP 会经常变化,不能把 DNS 永久缓存,而应该把这个 TTL 的值设置的小一点。而没有启用 Security Manager 的情况下,JDK 版本 1.8 及以上的缺省值都是 30 秒。Amazon 官方文档也建议该参数要小于 60 秒(https\://docs.aws.amazon.com/zh_cn/sdk-for-java/latest/developer-guide/jvm-ttl-dns.html?trk=cndc-detail)。
其次针对 networkaddress.cache.negative.ttl,无论是否启用 Security Manager,缺省都是 10 秒,这个可以在 JDK 目录的 java.security 配置文件中找到,也可以做修改。其中 -1 表示永久缓存,0 表示不缓存。从上面的测试来看,设置为 0 的利大于弊。
#### **4.2 源码探究**
在写本文之前,我一直以为 Java 会遵循 DNS 的 TTL,测试的结果确实出乎意料,虽然网上也有些资料说明这点,但最好的办法来确认这个还是看代码。
首先找到 java/net/InetAddress.java中 getAllByName0 这个方法,跳过 SecurityManager 部分,可以看到 getCachedAddresses,说明会先去 Cache 里面找,如果找不到,才会到 DNS 服务那边去找。
![image.png](https://dev-media.amazoncloud.cn/c645ad0ab3ca4412862f8272d6fde84a_image.png "image.png")
我们在从 Cache 里面取结果的部分,会判断这个结果是否已经过期。那这个过期时间是否和 DNS 服务器返回的 TTL 有关呢?
![image.png](https://dev-media.amazoncloud.cn/392ced2741094ea2a89a4defeb3a3751_image.png "image.png")
答案是:非也。下面给 Cache 里面加内容的代码中,expiration 只是根据 cachePolicy 来算的,也就是上面提到的配置参数 networkaddress.cache.ttl。
![image.png](https://dev-media.amazoncloud.cn/897cd7251cb346fe8c6005e553f6c4a4_image.png "image.png")
![image.png](https://dev-media.amazoncloud.cn/26e3e0ac7b9344038d72d47eeeb781fb_image.png "image.png")
#### **4.3 重试机制**
这里我们只讨论 Amazon Java SDK 的重试机制,其他语言的请参考相应文档。在 Java SDK 的文档中(https\://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/section-client-configuration.html?trk=cndc-detail) 写明,默认的重试次数是 3 次,用户可以通过 ClientConfiguration.setMaxErrorRetry 方法来修改。重试是采用指数回退机制,来让重试变得更有效率,具体可以参考文档:https\://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html?trk=cndc-detail
### 05 总结
DNS 服务可以说是网络世界中最重要的一个服务了,本文从 Java 程序遇到的 DNS 解析问题出发,通过各种测试以及阅读源码,颠覆了之前认知,学习了 Amazon Java SDK 和 JDK 之间合作依赖关系,为解决偶发的 DNS 解析故障,即 UnknownHostException 错误给出了一个可行建议,供大家参考:
**设置 networkaddress.cache.ttl 为 30 秒,**
**设置 networkaddress.cache.negative.ttl 为 0 秒。**
#### **参考链接**
[1] https\://docs.oracle.com/javase/8/docs/technotes/guides/net/properties.html?trk=cndc-detail
[2] https\://docs.aws.amazon.com/zh_cn/sdk-for-java/latest/developer-guide/jvm-ttl-dns.html?trk=cndc-detail
[3] https\://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/section-client-configuration.html?trk=cndc-detail
[4] https\://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html?trk=cndc-detail