从 UnknownHostException 错误来分析 Java 的 DNS 解析和缓存机制

Amazon Simple Storage Service (S3)
Amazon EC2
Amazon VPC
0
0
### 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
目录
亚马逊云科技解决方案 基于行业客户应用场景及技术领域的解决方案
联系亚马逊云科技专家
亚马逊云科技解决方案
基于行业客户应用场景及技术领域的解决方案
联系专家
0
目录
关闭