[Redis] Redis Scale up 과정에서 발생한 DNS Cache 이슈

Hocaron·2024년 12월 21일
4

네트워크

목록 보기
7/7

Redis Scale up 과정에서 약 45초간 장애가 발생했다. 해당 장애는 서비스의 레디스 클러스터가 스케일업되면서 도메인에 연결된 IP 주소가 변경되었지만, 클라이언트에서 이를 즉시 반영하지 못해 발생했다.

Redis 복제 시간인 15초에 더해 JVM DNS Cache의 기본 설정인 30초 동안 이전 IP를 참조하면서 총 45초간 장애가 이어졌다.

JVM DNS Cache의 이해

  • JVM은 성능을 위해 기본적으로 DNS 조회 결과를 캐싱한다
  • 이는 대부분의 상황에서는 좋지만, 인프라의 IP가 동적으로 변경되는 환경에서는 문제가 된다
    1. Scale up/down 시 새로운 IP를 즉시 인식하지 못함
    2. Failover 상황에서 장애 복구 시간 증가
    3. 클러스터 구성 변경 시 연결 지연 발생

DNS Cache 동작 방식

  • JVM은 한번 조회한 DNS 결과를 Security Manager의 캐시에 저장한다
  • 기본 설정:
    • 성공한 조회 결과: 30초 동안 캐시
    • 실패한 조회 결과: 10초 동안 캐시
  • 이 캐시는 JVM 프로세스 전체에 영향을 미친다

CachePolicy 코드 분석

fyi; 관련 코드

CachePolicy (성공한 조회 결과의 TTL) 설정

Integer tmp = java.security.AccessController.doPrivileged(
  new PrivilegedAction<Integer>() {
    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;
    }
  });

java.security.Security에서 networkaddress.cache.ttl 값이 없으면, 시스템 프로퍼티에서 sun.net.inetaddr.ttl 값을 조회하여 TTL 을 설정한다.

값이 없는 경우, 30초를 TTL 로 사용한다.

negativeCachePolicy (실패한 조회 결과의 TTL) 설정

tmp = java.security.AccessController.doPrivileged(
  new PrivilegedAction<Integer>() {
    public Integer run() {
        try {
            String tmpString = Security.getProperty(negativeCachePolicyProp);
            if (tmpString != null) {
                return Integer.valueOf(tmpString);
            }
        } catch (NumberFormatException ignored) {
            // Ignore
        }

        try {
            String tmpString = System.getProperty(negativeCachePolicyPropFallback);
            if (tmpString != null) {
                return Integer.decode(tmpString);
            }
        } catch (NumberFormatException ignored) {
            // Ignore
        }
        return null;
    }
  });

java.security.Security에서 networkaddress.cache.negative.ttl 값이 없으면, sun.net.inetaddr.negative.ttl 값을 조회하여 TTL 로 사용한다.

# grep 'networkaddress.cache.negative.ttl' /Users/122d6417/Library/Java/JavaVirtualMachines/corretto-22.0.2/Contents/Home/conf/security/java.security
networkaddress.cache.negative.ttl=10

이미 networkaddress.cache.negative.ttl 이 10으로 설정되어있어, sun.net.inetaddr.negative.ttl 으로는 TTL 설정이 불가능하다.

JVM DNS Cache 설정

  1. 시스템 속성을 사용하여 JVM 시작 시 설정
java -Dsun.net.inetaddr.ttl=0 -jar your-application.jar
  1. java.security 수정

$JAVA_HOME/conf/security/java.security 파일을 편집하여 DNS Cache 설정을 변경할 수 있다.

networkaddress.cache.ttl=0
networkaddress.cache.negative.ttl=0
  1. JVM Security Property를 동적으로 설정(🔥 위 설정은 Spring Boot 에서는 적용이 되지 않는다.)

InetAddressCachePolicy가 JVM 초기화 시점에 클래스 로딩이 이루어지기 때문에, main 메서드에서 동적으로 설정한 값은 무시된다.

import java.security.Security;

public class ApiApplication {
    public static void main(String[] args) {
        // 성공한 DNS 조회 결과의 캐시 TTL 설정
        Security.setProperty("networkaddress.cache.ttl", "0"); // 0은 캐시 비활성화
        // 실패한 DNS 조회 결과의 캐시 TTL 설정
        Security.setProperty("networkaddress.cache.negative.ttl", "0"); // 0은 캐시 비활성화
        
        SpringApplication.run(PrivateApiApplication.class, args);
    }
}

위 설정은 DNS 조회 결과가 JVM의 전역 캐시에 저장되기 때문에, 같은 JVM에서 실행 중인 모든 애플리케이션과 클라이언트가 영향을 받는다. Redis 클러스터에서만 빠른 IP 감지가 필요하다면 Lettuce 설정을 변경하여 DNS Cache 설정을 변경할 수 있다.

⭐️ Lettuce DNS Cache 설정 이해

Lettuce에서 DnsAddressResolverGroup을 사용하면 DNS 조회 시 JVM DNS Cache를 우회한다.

Lettuce DNS Cache 설정

Dependency 추가 후에 Configuration 설정을 다음과 같이 해보자.

implementation 'io.netty:netty-resolver-dns:4.1.115.Final'
    @Bean
    public ClientResources clientResources() {
        NioEventLoopGroup dnsEventLoopGroup = new NioEventLoopGroup(1);

        DnsAddressResolverGroup dnsAddressResolverGroup = new DnsAddressResolverGroup(
                new DnsNameResolverBuilder(dnsEventLoopGroup.next())
                        .datagramChannelType(NioDatagramChannel.class)
                        .resolveCache(NoopDnsCache.INSTANCE)
                        .cnameCache(NoopDnsCnameCache.INSTANCE)
                        .authoritativeDnsServerCache(NoopAuthoritativeDnsServerCache.INSTANCE)
                        .consolidateCacheSize(0)
        );

        return ClientResources.builder()
                .addressResolverGroup(dnsAddressResolverGroup)
                .eventExecutorGroup(dnsEventLoopGroup)
                .build();
    }

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .clientResources(clientResources())
                .build();

        var redisConnect = new RedisStaticMasterReplicaConfiguration(primaryHost, primaryPort);
        redisConnect.addNode(replicaHost, replicaPort);
        return new LettuceConnectionFactory(
                redisConnect,
                clientConfig
        );
    }

주의가 필요한 부분


fyi; Lettuce 공식 문서

netty 의존성 추가 시에 lettuce 에서는 DefaultDnsAddressResolverGroupWrapper 을 사용하게 된다. 이 리졸버는 DNS 서버에서 반환하는 TTL 값을 기반으로 캐싱 동작을 수행하므로, 캐싱 동작이 DNS 서버의 설정에 의존적이다. 따라서 캐시를 사용하지 않으려면 커스텀 리졸버를 직접 설정하여 사용해야 한다.

주요 코드 설명

NioEventLoopGroup

NioEventLoopGroup dnsEventLoopGroup = new NioEventLoopGroup(1);
  • DNS 조회 전용 이벤트 루프 그룹이다
  • 스레드 수를 1로 설정하는 이유는 DNS 조회는 비교적 가벼운 작업이므로 과도한 스레드 생성을 방지하기 위해서이다.

DnsNameResolverBuilder 설정

DnsAddressResolverGroup dnsAddressResolverGroup = new DnsAddressResolverGroup(
    new DnsNameResolverBuilder(dnsEventLoopGroup.next())
        .datagramChannelType(NioDatagramChannel.class)
        .resolveCache(NoopDnsCache.INSTANCE)
        .cnameCache(NoopDnsCnameCache.INSTANCE)
        .authoritativeDnsServerCache(NoopAuthoritativeDnsServerCache.INSTANCE)
        .consolidateCacheSize(0)
);
  • NoopDnsCache: A 레코드 캐시를 비활성화
  • NoopDnsCnameCache: CNAME 캐시를 비활성화
  • NoopAuthoritativeDnsServerCache: 권한 DNS 서버 정보 캐시를 비활성화
  • consolidateCacheSize(0): 동시 DNS 요청을 통합하지 않고 독립적으로 처리

권한 DNS 서버(Authoritative DNS Server)와 캐싱 DNS 서버(Caching DNS Server)의 차이
권한 DNS 서버는 도메인의 공식 DNS 레코드를 보유하며, 최종적인 응답을 제공한다. 캐싱 DNS 서버는 클라이언트의 DNS 요청을 받아 캐시에 저장된 데이터로 응답한다. 캐시에 데이터가 없는 경우 권한 있는 DNS 서버에 쿼리를 보내 정보를 가져온다.

특정 도메인의 네임서버(Name Server) 정보를 조회하고 싶다면
# nslookup -type=ns example.com

Server:  192.168.1.1
Address: 192.168.1.1#53

Non-authoritative answer:
example.com   nameserver = ns1.example.com.
example.com   nameserver = ns2.example.com.

Authoritative answers can be found from:
ns1.example.com    internet address = 203.0.113.1
ns2.example.com    internet address = 203.0.113.2

DNS 개념(A 레코드, CNAME 레코드)

  1. A 레코드
  • A 레코드(Address Record)는 도메인을 IP 주소로 매핑하는 DNS 레코드이다.
  • 네트워크에서 사용되는 가장 기본적인 레코드로, 도메인의 실제 위치를 나타낸다.
  • 예: elastic-redis-001.cache.amazonaws.com → 10.0.0.1
  1. CNAME 레코드
  • CNAME은 Canonical Name Record의 약자로, 도메인 별칭(Alias)을 정의하는 DNS 레코드이다.
  • 하나의 도메인이 다른 도메인을 가리키도록 설정한다.
  • 주로 복잡하거나 자주 변경되는 도메인을 간단하고 쉽게 기억할 수 있는 별칭으로 가리킬 때 사용한다.
  • CNAME 레코드 뒤에는 반드시 A 레코드 또는 AAAA 레코드(IPv6)로 끝난다.

클라이언트 요청 흐름

  1. 클라이언트가 app.example.com에 요청
  2. DNS 서버는 app.example.com의 CNAME 레코드를 확인
  3. elastic-redis-001.cache.amazonaws.com으로 요청 전달
  4. DNS 서버는 A 레코드를 조회하여 최종적으로 IP 주소(10.0.0.1)를 반환

특정 DNS 레코드 타입을 조회하고 싶다면

# dig example.com

; <<>> DiG 9.10.6 <<>> example.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12345
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 2, ADDITIONAL: 1

;; QUESTION SECTION:
;example.com.                   IN      A

;; ANSWER SECTION:
example.com.            3600    IN      A       93.184.216.34

;; AUTHORITY SECTION:
example.com.            172800  IN      NS      b.iana-servers.net.
example.com.            172800  IN      NS      a.iana-servers.net.

;; ADDITIONAL SECTION:
a.iana-servers.net.     172800  IN      A       199.43.135.53
b.iana-servers.net.     172800  IN      A       199.43.133.53

;; Query time: 35 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Dec 20 15:00:00 2024
;; MSG SIZE  rcvd: 154

References

profile
기록을 통한 성장을

0개의 댓글