IT/기타

CoreDNS NXDOMAIN 캐시가 만든 30초 지옥, ndots:5의 그림자

gfrog 2026. 8. 19. 03:20

CoreDNS NXDOMAIN 캐시가 만든 30초 지옥, ndots:5의 그림자

지난주 화요일 새벽 4시쯤, 온콜 알림이 울렸다. checkout-service 의 외부 결제사 API 콜 P99 레이턴시가 갑자기 5초, 10초를 찍고 있었다. 눈이 번쩍 떠졌다. 결제 흐름이라 반쯤 정신이 나간 채로 노트북을 열었다.

결론부터 말하면 범인은 CoreDNS의 NXDOMAIN 네거티브 캐시였다. 그런데 이게 그냥 캐시 문제가 아니라, ndots:5랑 얽혀서 아주 재미없게 터졌다. 몇 시간 동안 헤맨 이야기를 정리해둔다.

처음엔 결제사 문제인 줄 알았다

첫 반응은 당연히 "결제사 쪽에서 뭐 하나" 였다. 상태 페이지 열어봤다. 정상. 결제사 담당자한테 슬랙 DM 날렸다. "저희 쪽 아무 이슈 없는데요."

그럼 우리 클러스터 문제다. 근데 CPU도 널널하고 노드 메모리도 여유 있고 HPA는 안 튀었다. 뭐지.

kubectl exec 로 파드에 들어가서 그냥 time curl 을 몇 번 쳐봤다. 어떤 요청은 0.3초, 어떤 요청은 5.4초. 정확히 5초 언저리에서 자꾸 걸린다. 이 5초라는 숫자가 눈에 걸렸다. 아, 저거 어디서 본 숫자다. glibc resolver 타임아웃.

DNS를 의심하기 시작

dig +stats external.payment-provider.com @코어DNS-IP 로 직접 쿼리해봤다. 대부분 20ms 안쪽. 근데 어떤 서브도메인은 계속 NXDOMAIN이 돌아온다. 이상하다. 얘는 어제까지 잘 됐던 도메인인데.

여기서 조금 실마리가 보였다. 결제사가 최근에 새 서브도메인 하나를 추가했다고 어제 오후에 공지 메일이 왔었다. 나는 그거 그냥 스팸함으로 밀어놨었다 (반성한다).

파드 안에서 getent hosts 로 새 서브도메인 조회하면 5초 타임아웃 나고 NXDOMAIN. 노드에서 직접 조회하면 정상. 그럼 이건 확실히 CoreDNS 레이어의 문제다.

NXDOMAIN이 캐시에 눌러앉아 있었다

CoreDNS 파드 로그를 켜봤다. 로그 레벨을 debug로 잠깐 올려서 새 서브도메인 조회 흐름을 봤다.

[DEBUG] plugin/cache: NXDOMAIN response served from cache

이 로그가 계속 찍히고 있었다. 문제는, 결제사가 서브도메인을 등록하기 *직전에* 우리 앱이 그 도메인을 미리 조회한 적이 있었다는 거였다. 어떤 서비스가 컨피그맵을 미리 로드하면서 도메인을 리졸빙했고, 그때 아직 등록 전이었으니까 정당하게 NXDOMAIN이 돌아왔던 거다.

문제는 CoreDNS의 cache 플러그인이 이 NXDOMAIN을 기본갑대로 캐시에 박아두고 있었다. denial TTL은 우리 Corefile 상에서 명시가 없어서 기본값으로 동작 중이었다.

. {
    cache 30
    # denial은 별도 지정 안 함
    ...
}

cache 30 만 써두면 positive와 negative가 같이 30초 캐시된다고 대충 알고 있었다. 근데 실제로 확인해보니 우리 버전에서는 denial 관련 옵션의 상속 관계가 좀 애매했다. 명시적으로 cache { success 30 denial 30 } 처럼 나눠 쓰지 않으면 예상 밖의 값이 걸리는 경우가 있다.

여기서 ndots:5가 기름을 부었다

한 발 더 들어가 보면, 문제를 5초 지옥으로 키운 진짜 범인은 ndots였다.

우리 파드의 /etc/resolv.conf 는 이렇게 생겼다:

search default.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

external.payment-provider.com 을 조회하면 도트가 2개니까 ndots:5 미만이라, 리졸버는 이걸 상대경로로 취급해서 search 리스트를 먼저 순회한다.

1. external.payment-provider.com.default.svc.cluster.local → NXDOMAIN (캐시됨) 2. external.payment-provider.com.svc.cluster.local → NXDOMAIN (캐시됨) 3. external.payment-provider.com.cluster.local → NXDOMAIN (캐시됨) 4. external.payment-provider.com → 여기서 결제사 실제 IP 리턴돼야 하는데 이전에 실패했던 게 캐시에 있음

정상 상황이면 앞의 세 개는 캐시 히트라서 빠르게 넘어가고 마지막 정답 쿼리 하나만 나가야 한다. 근데 문제의 네 번째 조회가 예전 NXDOMAIN 캐시에 걸려버리니까, 앱은 "이 도메인 없다"는 답을 받고 재시도했다. 그리고 재시도할 때마다 glibc는 A와 AAAA를 병렬로 뿌리는데, 이게 conntrack이랑 붙으면서 그 유명한 5초 UDP 타임아웃 케이스에 걸리기도 했다.

한마디로: 캐시된 오래된 NXDOMAIN + ndots:5의 search 폭탄 + IPv6 병렬 조회 이슈까지 겹치면서 P99가 5초를 넘어가고 있었다.

응급조치와 진짜 수정

새벽이라 정공법은 미뤄뒀다. 응급조치는 두 단계였다.

첫째, CoreDNS 파드 재시작으로 캐시 즉시 비우기. 원시적이지만 이게 제일 빨랐다.

kubectl -n kube-system rollout restart deploy/coredns

30초 후 P99가 정상으로 돌아왔다. 일단 서비스는 살렸다.

둘째, 근본 수정을 위해 Corefile을 손봤다.

. {
    errors
    health {
       lameduck 5s
    }
    ready
    kubernetes cluster.local in-addr.arpa ip6.arpa {
       pods insecure
       fallthrough in-addr.arpa ip6.arpa
       ttl 30
    }
    prometheus :9153
    forward . /etc/resolv.conf {
       max_concurrent 1000
    }
    cache 30 {
       success 9984 30
       denial 9984 5
       prefetch 3 60s 10%
    }
    loop
    reload
    loadbalance
}

핵심 변경은 denial 을 5초로 짧게 줄인 거다. 새로 등록된 도메인이 최악의 경우에도 5초 안에는 반영된다. 그리고 prefetch 로 인기 있는 positive 응답은 미리 리프레시 되도록 했다.

여기서 짚어둘 게 있는데, denial TTL을 무작정 짧게 잡는 것도 위험하다. NXDOMAIN 캐시를 짧게 두면 존재하지 않는 도메인을 계속 반복 조회하는 앱이 있을 때 CoreDNS 부하가 튄다. 우리 팀에서는 5초 정도가 새 도메인 등록 반영 지연과 부하 사이의 균형점이라고 합의했다. 이건 트래픽 패턴에 따라 다르다.

ndots는 어떻게 할 것인가

Corefile만 고치고 끝낼 뻔했는데, 팀 내부 논의 끝에 앱 쪽에도 손을 대기로 했다. 외부 도메인을 자주 부르는 서비스에는 파드 스펙에 dnsConfig 로 ndots를 낮추자는 결론이 났다.

dnsConfig:
  options:
  - name: ndots
    value: "1"

이걸 넣으면 external 도메인은 search 리스트 순회 없이 바로 절대 도메인으로 조회된다. 클러스터 내부 서비스 이름(짧은 이름)을 쓰는 곳에는 부작용이 있을 수 있어서 서비스별로 판단이 필요하다. 우리는 결제, 알림, 외부 크롤링 관련 서비스만 우선 적용했다.

이걸로 끝일까? 사실 아직 검증 중이다. NodeLocal DNSCache 도입 이야기가 다시 나왔는데, 이건 예전에 한 번 가토하다가 미뤄뒀다. 다음 스프린트에서 재검토해보려고 한다.

남는 교훈

일단 CoreDNS의 negative caching은 "안전한 기본값"으로 방치할 대상이 아니었다. 대부분의 클러스터가 그냥 cache 30 만 써두는데, 이게 아주 잘 도는 것 같아 보여도 사실은 새 도메인 등록 지연이라는 함정을 깔고 있다. 서브도메인을 자주 추가하는 서드파티 의존이 있으면 특히 그렇다.

두 번째로, ndots:5는 여전히 골치다. 매년 kubecon에서 누군가는 ndots 얘기를 다시 꺼낸다. 왜 아직도 기본값이 5냐는 얘기도 매번 나온다. 우리는 이번 기회에 dnsConfig 표준안을 팀 내부에 문서화하기로 했다.

마지막으로, 결제사 공지 메일은 앞으로 스팸함에 안 밀어놓겠다. 그게 사실 제일 큰 교훈인지도 모르겠다.

혹시 비슷하게 NXDOMAIN 캐시로 사고 나신 분 있으면 어떻게 튜닝했는지 댓글이나 DM 남겨주시면 감사하겠다.


태그: CoreDNS, Kubernetes, DNS, ndots, 트러블슈팅, SRE