[입 개발] DNS Caching in JVM

다음과 같은 오류가 발견되어서 정정합니다. 현재는 JVM에서의 DNS Caching 이 30초입니다.
자바6 이후로는 계속 그렇게 설정되어 있는듯 합니다. 흑흑흑 나는 왜 지금까지 그 옵션을 열심히 사용했을까요?
그러나 흐름 자체는 도움이 될듯하여 내용은 수정해서 남겨둡니다. 알려주신 역촋 정상혁님께 감사를

JVM 에서 (혹은 Java) 에서는 DNS Caching 이 디폴트로 FOREVER 입니다. 이 말은 한번 쿼리한 DNS 주소는 다시는 쿼리하지 않는다는 것입니다. 이러면 당연히 DNS 쿼리 시간이 들지 않으니, 속도면에서는 유리하지만, DNS 변화를 주는 것으로 뭔가 처리할려고 하면, 결국 계속 실패하게 됩니다.  이전 ip를 계속 써버리니… 그럼 이 동작을 어떻게 확인하는가? JDK 소스를 까보시면 간단하게 아실 수 있습니다.

./src/share/classes/java/net/InetAddress.java
./src/share/classes/sun/net/InetAddressCachePolicy.java

InetAddress class 는 getAllByName0 함수가 불려질 때 먼저 cache 되어있는지를 getCachedAddresses 함수를 통해서 확인합니다. 아래 코드르 보면 cache 에 없을때만 실제 DNS 질의를 하게 됩니다.

    private static InetAddress[] getAllByName0 (String host, InetAddress reqAddr, boolean check)
        throws UnknownHostException  {

        /* If it gets here it is presumed to be a hostname */
        /* Cache.get can return: null, unknownAddress, or InetAddress[] */

        /* make sure the connection to the host is allowed, before we
         * give out a hostname
         */
        if (check) {
            SecurityManager security = System.getSecurityManager();
            if (security != null) {
                security.checkConnect(host, -1);
            }
        }

        InetAddress[] addresses = getCachedAddresses(host);

        /* If no entry in cache, then do the host lookup */
        if (addresses == null) {
            addresses = getAddressesFromNameService(host, reqAddr);
        }

        if (addresses == unknown_array)
            throw new UnknownHostException(host);

        return addresses.clone();
}

그리고 getCachedAddresses 함수는 InetAddressCachePolicy 가 FOREVER일 때는 해당 값을 expire 시키지 않습니다.

private int getPolicy() {
    if (type == Type.Positive) {
        return InetAddressCachePolicy.get();
    } else {
        return InetAddressCachePolicy.getNegative();
   }
}

public CacheEntry get(String host) {
    int policy = getPolicy();
    if (policy == InetAddressCachePolicy.NEVER) {
        return null;
    }
    CacheEntry entry = cache.get(host);
 
    // check if entry has expired
    if (entry != null && policy != InetAddressCachePolicy.FOREVER) {
        if (entry.expiration >= 0 &&
            entry.expiration < System.currentTimeMillis()) {
            cache.remove(host);
            entry = null;
        }
    }
 
    return entry;
}

따로 설정을 하지 않으면 InetAddressCachePolicy 는 디폴트로 FOREVER 입니다.

// Controls the cache policy for successful lookups only
private static final String cachePolicyProp = "networkaddress.cache.ttl";
private static final String cachePolicyPropFallback =
"sun.net.inetaddr.ttl";

// Controls the cache policy for negative lookups only
private static final String negativeCachePolicyProp =
"networkaddress.cache.negative.ttl";
private static final String negativeCachePolicyPropFallback =
"sun.net.inetaddr.negative.ttl";

public static final int FOREVER = -1;
public static final int NEVER = 0;

private static int cachePolicy = FOREVER;

public static synchronized int get() {
return cachePolicy;
}

그리고 해당 값은 Security Property 와 System Property에 의해서 제어가 가능합니다.

스크린샷 2017-12-27 오후 5.28.32

단위는 초고, 초가 자동으로 밀리세컨으로 변경됩니다.

long expiration;
if (policy == InetAddressCachePolicy.FOREVER) {
    expiration = -1;
} else {
    expiration = System.currentTimeMillis() + (policy * 1000);
}

우선순위는 networkaddress.cache.ttl 가 있으면 이걸 먼저 사용하고, 없으면 그 다음에 sun.net.inetaddr.ttl 을 사용합니다. 즉 Security Property 설정이 우선입니다.
아래 한글 주석을 확인하면 아무 옵션이 없을 때, SecurityManager 가 설정되어 있지 않으면 30초로 설정되게 됩니다.

Integer tmp = java.security.AccessController.doPrivileged(
  new PrivilegedAction() {
    public Integer run() {
        try {
            String tmpString = Security.getProperty(cachePolicyProp);
            if (tmpString != null) {
                return Integer.valueOf(tmpString);
            }
        } catch (NumberFormatException ignored) {
            // Ignore
        }
 
        try {
            String tmpString = System.getProperty(cachePolicyPropFallback);
            if (tmpString != null) {
                return Integer.decode(tmpString);
            }
        } catch (NumberFormatException ignored) {
            // Ignore
        }
        return null;
    }
  });
if (tmp != null) {
    cachePolicy = tmp.intValue();
    if (cachePolicy < 0) {
        cachePolicy = FOREVER;
    }
    propertySet = true;
} else {
    /* SecurityManager 가 설정되어 있지 않으면, 여기서 cachePolicy 가 DEFAULT_POSITIVE 가
     * 30초로 설정됩니다. 기본적으로 -Djava.security.manager 를 주지 않으면 
     * SecurityManager가 셋팅되지 않습니다.
     */
    /* No properties defined for positive caching. If there is no
     * security manager then use the default positive cache value.
     */
    if (System.getSecurityManager() == null) {
        cachePolicy = DEFAULT_POSITIVE;
    }
}

즉 위와 같이 해당 값을 설정하면 그 이후에는 캐시가 날라가서 다시 실제 쿼리를 날리게 됩니다. 보통 로컬에 dnsmasq 나 unbound 같은 로컬 DNS 캐시 서버를 둬서, 거기서 캐싱을 하면 실제적으로 외부로 날라가는 것보다는 훨씬 DNS 쿼리 비용을 줄일 수 있습니다.

참고문헌:
https://www.lesstif.com/pages/viewpage.action?pageId=17105897