赞
踩
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 中布置了一个基于 BIND 的 DNS 服务器(172.31.21.120),另外一台 Amazon EC2 作为客户端(172.31.189.232)。
DNS 服务器的配置中,现在注释部分是为了模拟解析失败的问题,而配置用来接管 Amazon S3 地址的解析。下面 amazonaws.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);
- }
- }
- }
![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)
同时,为了看到 SDK 的重试情况,创建了 log4j.properties,内容如下:
- log4j.rootLogger=WARN, A1
- log4j.appender.A1=org.apache.log4j.ConsoleAppender
- log4j.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 的相关配置:
以下为 tcpdump 结果,抓取了与 DNS 服务器之间的通讯信息:
以下为 dig betatest.s3.cn-north-1.amazonaws.com.cn 的结果,通过多次执行,发现 TTL 最大为 4 秒。
从上面截图上可以看到以下几点:
networkaddress.cache.ttl 默认是没有配置的,而生效 cache 时间是 30 秒。
networkaddress.cache.negative.ttl 默认是 10 秒。
运行的几次尝试中,只有一开始有抓到 DNS 的数据包。说明 Java 是缓存了 DNS 的,后续请求无需再次解析。
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 的结果显示没有该域名记录:
日志只截取了四次执行结果,都是以 UnknownHostException 为错误结束的。
下面的 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 域名。
总结来说,可以看到以下几点:
S3 SDK 在失败的时候自动执行了 3 次重试,每次重试间隔在几到几百毫秒之间,逐步扩大。
在每次重试时,并没有再次发生 DNS 请求,说明失败的解析也被缓存了。这就是通过 networkaddress.cache.negative.ttl 来控制的,默认 10 秒。
通过 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 的结果确认。
下面的 tcpdump 我用红绿框表示了两次查询,实际上还有一次,发生在 14:39:29,限于篇幅就不放更多截图了。
总结来说看到以下几点:
当 DNS 服务器没有响应时,请求会有较长时间在尝试解析,这和客户端 DNS 解析配置和有关。
DNS 解析失败同样会被缓存,在有效时间内不再向 DNS 服务器解析,直到过期后才会继续尝试。这点和测试 3.2 结果类似。
3.4 模拟无法解析情况时不缓存
从上面看到,因为有 negative cache 的存在,有时 DNS 服务器短暂故障而无法解析,会导致后续重试时继续失败。这次我们将 java.security 里面的配置改为 networkaddress.cache.negative.ttl=0,其他配置还是按照 3.2 的方式。
下图可以看到生效的 cacheNegativePolicy 确定为 0 秒。在四次请求的 1 秒内,tcpdump 显示有四次查询,虽然每次都返回了无此域名的错误。
总结来说看到以下:
当 networkaddress.cache.negative.ttl=0 时,失败的解析不再缓存,每次重试都会向 DNS 服务器方式请求。
针对偶尔 DNS 服务器出错的情况,这个配置可以让程序在重试时有机会成功。
3.5 模拟 DNS 服务器没有响应时不缓存
我们设置了 networkaddress.cache.negative.ttl=0,其他按照 3.3 的方式。下图可以看到每次重试时间间隔达到了 20 秒。
Tcpdump 也显示了每次重试都需要经过多次尝试。
总结来说看到以下:
当 networkaddress.cache.negative.ttl=0 时,失败的解析不再缓存,每次重试都会向 DNS 服务器方式请求。
由于 DNS 没有回复而导致了较长的重试时间,真可能会导致应用卡住,问题没有及时报告。
04
详细分析
4.1 缓存配置
Sun Java 官方网站对于 DNS 缓存的两次参数描述还是很简单的:
https://docs.oracle.com/javase/8/docs/technotes/guides/net/properties.html
首先针对 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)。
其次针对 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 服务那边去找。
我们在从 Cache 里面取结果的部分,会判断这个结果是否已经过期。那这个过期时间是否和 DNS 服务器返回的 TTL 有关呢?
答案是:非也。下面给 Cache 里面加内容的代码中,expiration 只是根据 cachePolicy 来算的,也就是上面提到的配置参数 networkaddress.cache.ttl。
4.3 重试机制
这里我们只讨论 Amazon Java SDK 的重试机制,其他语言的请参考相应文档。在 Java SDK 的文档中(https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/section-client-configuration.html)写明,默认的重试次数是 3 次,用户可以通过 ClientConfiguration.setMaxErrorRetry 方法来修改。重试是采用指数回退机制,来让重试变得更有效率,具体可以参考文档:https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html
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
[2] https://docs.aws.amazon.com/zh_cn/sdk-for-java/latest/developer-guide/jvm-ttl-dns.html
[3] https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/section-client-configuration.html
[4] https://docs.aws.amazon.com/sdkref/latest/guide/feature-retry-behavior.html
本篇作者
周平
西云数据高级技术客户经理,致力于大数据技术的研究和落地,为亚马逊云科技中国客户提供企业级架构和技术支持。
星标不迷路,开发更极速!
关注后记得星标「亚马逊云开发者」
听说,点完下面4个按钮
就不会碰到bug了!
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。