왜 DNS가 장애 전파의 통로가 되는가

DNS는 단순한 이름 조회처럼 보이지만, 실제로는 여러 계층의 캐시(리졸버, OS, 애플리케이션 런타임)가 겹쳐 있는 분산 시스템이다. 이 구조 때문에 문제가 생긴다. 장애가 났을 때 새 IP로 트래픽을 옮기려 해도 캐시에 남은 낡은 레코드 때문에 전환이 지연되고, 반대로 캐시가 짧으면 권위 서버에 부하가 몰려 그 자체가 장애 원인이 된다. TTL(Time To Live)은 이 두 위험 사이를 조율하는 유일한 손잡이다.

특히 마이크로서비스 환경에서는 한 서비스의 DNS 응답 지연이나 stale 레코드가 상류 호출자로 연쇄 전파된다. TTL 설계를 방치하면 장애 반경(blast radius)이 예측 불가능해진다.

TTL이 실제로 동작하는 지점

흔한 오해는 "TTL을 60초로 낮췄으니 60초 안에 전환된다"는 것이다. 실제로는 각 캐시 계층이 독립적으로 TTL을 카운트하고, 일부 퍼블릭 리졸버는 최소/최대 TTL을 강제로 재작성한다. JVM처럼 자체 캐시를 갖는 런타임은 DNS TTL을 무시하고 무한 캐시를 하기도 한다.

# 실제 남은 TTL 확인 — 연속 조회 시 값이 줄어드는지 본다
dig +noall +answer api.internal.example.com
# api.internal.example.com. 42 IN A 10.0.3.11
#                          ^^ 남은 TTL(초). 캐시 계층에 따라 값이 다르다

# 권위 서버에 직접 물어 원본 TTL 확인
dig @ns1.example.com api.internal.example.com +noall +answer

레코드 유형별 TTL 전략

모든 레코드에 같은 TTL을 쓰면 안 된다. 자주 바뀌는 것과 안정적인 것을 분리한다.

대상권장 TTL이유
장애 조치용 A/AAAA (LB 앞단)30~60초빠른 전환 필요
안정적 CNAME (CDN 별칭)300~3600초변경 드묾, 부하 절감
MX / TXT3600초 이상거의 불변
변경 예정 레코드사전에 60초로 하향전환창 확보

핵심은 "평상시 긴 TTL, 변경 직전 하향"이다. 마이그레이션 하루 전에 TTL을 낮춰 두면, 전환 시점에 낡은 캐시가 거의 남지 않는다.

애플리케이션 캐시가 만드는 함정

인프라 TTL을 아무리 잘 잡아도 애플리케이션 런타임이 무시하면 소용없다. JVM은 networkaddress.cache.ttl 기본값이 보안 매니저 유무에 따라 무한이 될 수 있고, Python·Go의 커넥션 풀은 한 번 해석한 IP를 재사용한다.

# requests 세션이 커넥션을 재사용하면 DNS 재조회가 안 일어난다.
# 명시적으로 조회 결과를 짧게 캐시하고 만료 시 재해석
import socket, time

class TTLDNSCache:
    def __init__(self, ttl=30):
        self.ttl = ttl
        self._cache = {}

    def resolve(self, host):
        now = time.monotonic()
        entry = self._cache.get(host)
        if entry and now - entry[1] < self.ttl:
            return entry[0]
        ip = socket.gethostbyname(host)
        self._cache[host] = (ip, now)
        return ip

dns = TTLDNSCache(ttl=30)
print(dns.resolve("api.internal.example.com"))

장애 전파를 줄이는 방어적 설계

TTL만으로는 부족하다. stale 캐시를 오히려 무기로 쓰는 serve-stale(RFC 8767)를 켜면, 권위 서버가 죽어도 만료된 레코드로 잠시 응답을 이어가 완전 중단을 막는다. 로컬 캐싱 리졸버(Unbound, CoreDNS)를 각 노드에 두면 상류 DNS 장애의 영향을 흡수한다.

# CoreDNS: 캐시 + serve-stale로 상류 장애 흡수
.:53 {
    cache {
        success 9984 30      # 최대 엔트리, 최소 TTL 30초
        denial 9984 5
        serve_stale 1h immediate  # 만료 후 1시간까지 stale 응답 허용
    }
    forward . 10.0.0.2 10.0.0.3 {
        policy sequential
        health_check 5s
    }
    loop
    log
}

변경 작업 시의 운영 절차

레코드 변경은 절차로 통제한다. ① 변경 24시간 전 TTL을 60초로 하향 → ② 기존 TTL이 완전히 만료될 때까지 대기 → ③ 실제 레코드 변경 → ④ 안정화 확인 후 TTL 원복. 이 순서를 지키지 않고 긴 TTL 상태에서 바로 바꾸면, 일부 사용자는 몇 시간 동안 옛 서버로 붙는다.

주의점

첫째, TTL을 무작정 낮추면 권위 서버·리졸버 부하와 조회 지연이 늘어 그 자체가 병목이 된다. 30초 미만은 특별한 이유가 없으면 피한다. 둘째, 퍼블릭 리졸버는 TTL을 재작성하므로 내가 설정한 값이 그대로 지켜진다고 가정하지 말고 dig로 실측한다. 셋째, DNS 기반 failover는 캐시 특성상 즉시성이 없다. 초 단위 전환이 필요하면 DNS가 아니라 로드밸런서·애니캐스트·헬스체크 계층에서 처리해야 한다. DNS는 "느리지만 광역적인" 전환 도구라는 성격을 전제로 설계하는 것이 안전하다.