如何使Java遵守DNS缓存超时时间?

121
我们使用GSLB实现地理分布和负载均衡。每个服务被分配一个固定的域名。通过一些DNS技巧,该域名被解析为距离最近且负载最轻的服务器的IP地址。为了使负载平衡正常工作,应用程序服务器需要遵守DNS响应中的TTL,当缓存超时时再次解析域名。然而,我无法找到在Java中实现这一点的方法。
该应用程序运行在Linux(Centos 5)上的Java 5中。
8个回答

94
根据拜伦的回答,你不能使用-D标志或调用System.setProperty来设置networkaddress.cache.ttlnetworkaddress.cache.negative.ttl作为系统属性,因为它们不是系统属性,而是安全属性。
如果你想使用系统属性来触发这种行为(以便可以使用-D标志或调用System.setProperty),你需要设置以下系统属性:
-Dsun.net.inetaddr.ttl=0

这个系统属性将启用所需的效果。这里的“0”(零)表示在JVM级别禁用DNS缓存,因此将咨询和使用DNS的原始TTL值。
但要注意:如果您在启动JVM进程时不使用-D标志,并选择从代码中调用此功能:
java.security.Security.setProperty("networkaddress.cache.ttl" , "0")

这段代码必须在JVM中的任何其他代码尝试执行网络操作之前执行。

这很重要,因为例如,如果你在一个.war文件中调用了Security.setProperty并部署了该.war到Tomcat,这将不起作用:Tomcat使用Java网络堆栈在你的.war代码执行之前初始化自身。由于这种“竞争条件”,通常更方便在启动JVM进程时使用-D标志。

如果你不使用-Dsun.net.inetaddr.ttl=0或调用Security.setProperty,你需要编辑$JRE_HOME/lib/security/java.security并在该文件中设置这些安全属性,例如:

networkaddress.cache.ttl = 0
networkaddress.cache.negative.ttl = 0

请注意评论中有关这些属性的安全警告。只有当您相当有信心不容易受到 DNS欺骗攻击时,才可以执行此操作。

值得注意的是, Oracle JDK 8文档推荐的方法是使用networkaddress.cache.ttlnetworkaddress.cache.negative.ttl,正如官方文档中已经描述的那样。这两个属性无法通过命令行设置,而应该使用setProperty()或使用安全策略文件进行设置。


2
FQN 是 java.security.Security(至少在 jdk7 中) - Pablo Fernandez
1
只是一条评论,这些安全警告大多与安全管理器和远程加载有关。对于任何正常的服务器应用程序来说,它信任 DNS 在某种程度上减少 TTL 是可以接受的。(但我认为 0 不是一个好的最小值,对于非安全管理器的默认值 30 秒在大多数情况下都很好)。 - eckes
3
系统属性在 OpenJDK 上是否有效,还是仅限于 Oracle 特定? - mhlz
7
第一次查找DNS后不再进行检查并不能保护您免受欺骗攻击,相反会使欺骗攻击变得永久而非暂时。 - kbolino
1
java.security 文件自 Java 11 起位于 $JRE_HOME/conf/security/java.security参考链接 - jebeaudet
显示剩余2条评论

76

Java在DNS缓存方面有一些非常奇怪的行为。最好的方法是关闭DNS缓存或将其设置为低于5秒的时间。

networkaddress.cache.ttl(默认值:-1)
指示名称服务中成功查找的缓存策略。该值指定为整数,以表示缓存成功查找的秒数。值为-1表示“永久缓存”。

networkaddress.cache.negative.ttl(默认值:10)
指示名称服务中未成功查找的缓存策略。该值指定为整数,以表示对未成功查找的故障进行缓存的秒数。值为0表示“永不缓存”。值为-1表示“永久缓存”。


8
注意:这并不禁用操作系统中的所有DNS缓存。只是禁用了Java自身在库中损坏的内存缓存。当您调用JVM时,可以在命令行上简单地设置这些属性。 - Nelson
4
我不确定"broken"是否有效。出于安全考虑,Java会永久缓存DNS条目,或者直到JVM重新启动,以先到者为准。据我所知,这是有意设计的。设置可以在java.security策略文件或命令行中进行。每个设置都不同。参考链接:http://www.rgagnon.com/javadetails/java-0445.html - Milner
4
请注意,您不能将它们设置为系统属性(即使用-D标志或System.setProperty),因为它们不是系统属性,而是安全属性。 - Les Hazlewood
7
这份文档在1.7版本中有轻微变化。具体来说,永久缓存只会在安全管理器存在时才会发生变化:"默认情况下,在安装了安全管理器时,会永久缓存;在未安装安全管理器时,会根据实现的特定时间进行缓存。" http://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html - Brett Okken
2
@Michael 请查看 System.getSecurityManager()。Java 8 文档链接:https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#getSecurityManager-- - gesellix
显示剩余2条评论

22

显然这在更新的版本(SE 6和7)中已经得到了修正。当我运行以下代码片段时,使用tcpdump观察53端口活动,我最多会经历30秒的缓存时间。

/**
 * https://dev59.com/sHM_5IYBdhLWcg3wt1rD
 *
 * Result: Java 6 distributed with Ubuntu 12.04 and Java 7 u15 downloaded from Oracle have
 * an expiry time for dns lookups of approx. 30 seconds.
 */

import java.util.*;
import java.text.*;
import java.security.*;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

public class Test {
    final static String hostname = "www.google.com";
    public static void main(String[] args) {
        // only required for Java SE 5 and lower:
        //Security.setProperty("networkaddress.cache.ttl", "30");

        System.out.println(Security.getProperty("networkaddress.cache.ttl"));
        System.out.println(System.getProperty("networkaddress.cache.ttl"));
        System.out.println(Security.getProperty("networkaddress.cache.negative.ttl"));
        System.out.println(System.getProperty("networkaddress.cache.negative.ttl"));

        while(true) {
            int i = 0;
            try {
                makeRequest();
                InetAddress inetAddress = InetAddress.getLocalHost();
                System.out.println(new Date());
                inetAddress = InetAddress.getByName(hostname);
                displayStuff(hostname, inetAddress);
            } catch (UnknownHostException e) {
                e.printStackTrace();
            }
            try {
                Thread.sleep(5L*1000L);
            } catch(Exception ex) {}
            i++;
        }
    }

    public static void displayStuff(String whichHost, InetAddress inetAddress) {
        System.out.println("Which Host:" + whichHost);
        System.out.println("Canonical Host Name:" + inetAddress.getCanonicalHostName());
        System.out.println("Host Name:" + inetAddress.getHostName());
        System.out.println("Host Address:" + inetAddress.getHostAddress());
    }

    public static void makeRequest() {
        try {
            URL url = new URL("http://"+hostname+"/");
            URLConnection conn = url.openConnection();
            conn.connect();
            InputStream is = conn.getInputStream();
            InputStreamReader ird = new InputStreamReader(is);
            BufferedReader rd = new BufferedReader(ird);
            String res;
            while((res = rd.readLine()) != null) {
                System.out.println(res);
                break;
            }
            rd.close();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }
}

16
是的,Java 1.5具有无限缓存的默认值。Java 1.6和1.7的默认值为30秒。 - Michael
7
1.7版本的文档指出,这只有在没有安全管理器的情况下才可能成立:“当安装了安全管理器时,默认行为是永久缓存;而在没有安装安全管理器时,则会根据实现特定的一段时间进行缓存。” http://docs.oracle.com/javase/7/docs/technotes/guides/net/properties.html - Brett Okken
1
@Michael,能否分享一下这个信息的来源? - rustyx
4
Oracle的1.6和1.7 JDK中,在jre/lib/security/java.security文件中,networkaddress.cache.ttl的默认值是“FOREVER”(永久)。出于安全考虑,当设置了安全管理器时,此缓存将被永久保留。当没有设置安全管理器时,默认行为是缓存30秒。因此,通过Java Web Start部署的小程序和应用程序仍会永久缓存。 - Michael
1
这里提供一个指向OpenJDK 8的java.security的代码指针,该文件表明如果没有安全管理器,TTL为30秒:http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/f940e7a48b72/src/share/lib/security/java.security-linux#l298。我已在Mac OS X和Ubuntu 14.04上进行了测试。 - tro
显示剩余5条评论

19

在拜伦的回答基础上,我认为您需要编辑位于%JRE_HOME%\lib\security目录中的java.security文件以实现此更改。

以下是相关部分:

#
# The Java-level namelookup cache policy for successful lookups:
#
# any negative value: caching forever
# any positive value: the number of seconds to cache an address for
# zero: do not cache
#
# default value is forever (FOREVER). For security reasons, this
# caching is made forever when a security manager is set. When a security
# manager is not set, the default behavior is to cache for 30 seconds.
#
# NOTE: setting this to anything other than the default value can have
#       serious security implications. Do not set it unless 
#       you are sure you are not exposed to DNS spoofing attack.
#
#networkaddress.cache.ttl=-1 

关于java.security文件的文档在这里


5
除此之外,在使用tomcat6时,我不得不修改我的lib/security文件,因为通过编程或通过JAVA_OPTS变量设置networkaddress.cache.ttl或sun.net.inetaddr.ttl是无效的。 - bramp
1
@bramp 谢谢兄弟,我也遇到了同样的问题,并通过使用你的评论和答案解决了它。评论和答案加一分。 - Bhavik Ambani

8
总结其他答案,你可以在<jre-path>/lib/security/java.security中设置属性networkaddress.cache.ttl的值来调整DNS查找的缓存。请注意,这不是系统属性而是安全属性。我能够使用以下命令设置它:
java.security.Security.setProperty("networkaddress.cache.ttl", "<value>");

这也可以通过系统属性-Dsun.net.inetaddr.ttl进行设置,但是如果已经在其他位置设置了安全属性,则无法覆盖该属性。

我还想补充一点,如果您在WebSphere中的Web服务中遇到此问题(就像我一样),那么仅设置networkaddress.cache.ttl是不够的。您需要将系统属性disableWSAddressCaching设置为true。与TTL属性不同,这可以作为JVM参数或通过System.setProperty设置。

IBM在这里对WebSphere如何处理DNS缓存有一个非常详细的文章。与上面相关的一段是:

要禁用Web服务的地址缓存,请将附加的JVM自定义属性disableWSAddressCaching设置为true。使用此属性禁用Web服务的地址缓存。如果您的系统通常运行许多客户端线程,并且在wsAddrCache缓存上遇到锁争用,则可以将此自定义属性设置为true,以防止Web服务数据的缓存。


2
根据官方oracle java属性sun.net.inetaddr.ttl是Sun实现的具体属性,"可能在未来的版本中不被支持"。 "优选的方式是使用安全性属性"networkaddress.cache.ttl

1
所以我决定查看Java源代码,因为我发现官方文档有点令人困惑。我发现(对于OpenJDK 11),大多数内容与其他人写的相符。重要的是属性评估的顺序。
InetAddressCachePolicy.java(为了可读性省略了一些样板):

String tmpString = Security.getProperty("networkaddress.cache.ttl");
if (tmpString != null) {
   tmp = Integer.valueOf(tmpString);
   return;
}
...
String tmpString = System.getProperty("sun.net.inetaddr.ttl");
if (tmpString != null) {
   tmp = Integer.valueOf(tmpString);
   return;
}
...
if (tmp != null) {
  cachePolicy = tmp < 0 ? FOREVER : tmp;
  propertySet = true;
} else {
  /* No properties defined for positive caching. If there is no
  * security manager then use the default positive cache value.
  */
  if (System.getSecurityManager() == null) {
    cachePolicy = 30;
  }
}


你可以清楚地看到,安全属性首先被评估,系统属性其次,如果它们中的任何一个被设置,则cachePolicy值将设置为该数字,或者如果它们持有低于-1的值,则设置为-1(FOREVER)。如果没有设置任何内容,则默认为30秒。事实证明,对于OpenJDK来说,几乎总是这种情况,因为默认情况下java.security不设置该值,只设置负值。
#networkaddress.cache.ttl=-1 <- this line is commented out
networkaddress.cache.negative.ttl=10

顺便提一下,如果未设置networkaddress.cache.negative.ttl(从文件中删除),则Java类内部的默认值为0。文档在这方面是错误的。这就是让我犯错的原因。


我认为文档中所述的默认值为10秒是正确的。当您从java.security文件中删除配置后,它不再是原始的默认行为。 - Juraj Martinka

0
我遇到了同样的问题。默认情况下,JVM会永久缓存DNS查找的结果。将networkaddress.cache.ttl设置为0可以解决问题,但它不会“遵守”DNS记录的TTL。
我使用了dnsjava库(链接:https://github.com/dnsjava/dnsjava)。
String remoteIp = org.xbill.DNS.Address.getByName(remoteHostname).getHostAddress();

这将尊重DNS记录的TTL,无论networkaddress.cache.ttl设置如何。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接