IT/기타

ndots:5, 왜 우리 Pod은 DNS 하나 찍는 데 다섯 번을 물어보는가

gfrog 2026. 8. 5. 00:20

ndots:5, 왜 우리 Pod은 DNS 하나 찍는 데 다섯 번을 물어보는가

DNS 지연 얘기를 하면 다들 NodeLocal DNSCache부터 붙이라고 한다. 맞는 얘기다. 근데 그 앞에 왜 이런 일이 벌어지는지 원리를 짚고 넘어가는 사람은 의외로 적더라. 우리 팀도 몇 달 전 P99가 튀는 걸 잡으렬고 노드로컬 캐시부터 붙였는데, 그때는 됐지만 나중에 다른 클러스터에 적용할 때 왜 되는지 설명을 못 해서 좀 창피했다.

그래서 이 글은 ndots가 뭐고, 왜 하필 5이며, 그 값 하나가 어떻게 매 요청마다 5~6번의 DNS lookup을 유발하는지 밑바닥부터 파본다. 최근 Medium 등에서 "ndots:5 latency tax"라는 표현이 다시 돌기 시작한 것도 이유가 있다.

resolv.conf가 실제로 뭘 하는가

Pod에 들어가서 /etc/resolv.conf를 열어보면 대충 이런 모습이다.

search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal
nameserver 10.96.0.10
options ndots:5

여기서 중요한 건 세 줄이 각자 하는 일이 다르다는 것.

search 라인은 "이름이 짧아 보이면 뒤에 이 도메인을 하나씩 붙여서 다시 시도해봐"라는 뜻이다. nameserver는 그 질의를 실제로 보낼 곳(CoreDNS Service ClusterIP)이고, options ndots:5가 문제의 그 값이다.

ndots는 "이름 안에 점이 이만큼 이상 있으면 그건 이미 완전한 이름(FQDN)으로 간주하고 search를 건너뛴다"는 임계값이다. 반대로 말하면 점이 그보다 적으면 무조건 search 도메인을 붙여서 순차적으로 다 물어본다. 이 규칙은 glibc의 res_query / getaddrinfo 계열 함수가 처리한다.

실제로 몇 번 물어보는가

api.stripe.com을 조회한다고 해보자. 점이 두 개다. ndots:5 미만이니 search 도메인을 앞에서부터 붙여서 시도한다. 위 resolv.conf 기준으로 실제 나가는 질의는 이런 순서다.

1. api.stripe.com.default.svc.cluster.local → NXDOMAIN 2. api.stripe.com.svc.cluster.local → NXDOMAIN 3. api.stripe.com.cluster.local → NXDOMAIN 4. api.stripe.com.ap-northeast-2.compute.internal → NXDOMAIN 5. api.stripe.com → 드디어 정답

게다가 IPv6 스택이 켜져 있는 이미지(요즘 대부분의 base image가 그렇다)에서는 A와 AAAA를 각각 물어본다. 그럼 5 × 2 = 10건. 요청 한 번 나갈 때마다.

CoreDNS가 아무리 빨라도 이 10건이 순차적/병렬로 다 왕복해야 한다. 그리고 이 중 4쌍은 반드시 NXDOMAIN이라 캐시도 짧게 잡히기 쉴다. 사실 이 부분이 P99를 튀게 만드는 원흉이다. 평균은 멀쩡한데 특정 시점에 NXDOMAIN 캐시가 만료되면서 확 밀리는 순간이 있다.

왜 하필 5인가

이게 좀 이상한 값이다. 리눅스 glibc의 기본 ndots는 1이다. 그런데 Kubernetes는 왜 5를 박아뒀을까?

찾아보면 결국 Service DNS 이름의 형태 때문이다. my-svc.my-namespace.svc.cluster.local처럼 완전한 서비스 이름은 점이 4개. Headless 서비스의 Pod 이름은 여기에 하나 더 붙어서 점 5개. 즉 클러스터 내부 이름은 점이 4~5개 사이인데, ndots를 그보다 낮게 잡으면 짧게 쓴 서비스 이름(예: my-svc.my-namespace)이 search 없이 그대로 바깥으로 나가버려서 NXDOMAIN을 맞는다. 그래서 클러스터 내부 조회를 편하게 만들기 위해 ndots를 5로 잡아둔 것.

문제는 이게 내부 조회에는 유리하지만 외부 조회에는 세금(latency tax)이 된다는 것. 그리고 요즘 워크로드는 대부분 외부 API를 훨씬 더 많이 부른다.

우회하는 여러 방법과 각각의 부작용

Pod 단위로 ndots를 낮추는 게 흔한 처방이다. dnsConfig.options로 ndots:2를 넣으면 대부분의 외부 도메인이 search를 건너뛴다.

spec:
  dnsConfig:
    options:
      - name: ndots
        value: "2"

근데 이걸 하면 짧게 쓴 서비스 이름은 못 창는다. 다른 네임스페이스의 서비스를 svc-b.ns-b로만 쓰던 코드가 있다면 다 깨진다. 우리 팀도 처음 시도할 때 이걸 몰라서 스테이징에서 한 번 터졌다. 그 뒤로는 신규 Pod에만 ndots:2를 적용하고 기존 서비스는 코드에서 FQDN(svc-b.ns-b.svc.cluster.local)으로 리팩터링하는 식으로 마이그레이션했다.

FQDN 뒤에 점을 붙이는 방법도 있다. api.stripe.com.처럼 trailing dot을 넣으면 resolver는 이걸 절대 이름으로 취급하고 search를 아예 건너뛴다. 애플리케이션에서 URL을 조립할 때 반영해야 해서 코드 변경이 필요한데, 라이브러리에 따라 trailing dot을 지원하지 않는 경우도 있어서 이건 우아하진 않다.

NodeLocal DNSCache는 다른 차원의 해법이다. 각 노드에 DNS 캐시를 두어 CoreDNS까지 가는 왕복 자체를 줄이는 거라, ndots:5로 인한 5회 조회가 여전히 발생해도 대부분 로컬에서 끝난다. 특히 NXDOMAIN 응답을 캐시하는 negative caching이 켜져 있어서 앞의 4번을 매번 밖으로 안 보낸다. 단점은 데몬셋 하나가 추가되고, 로컬 DNS가 죽으면 그 노드의 Pod들이 통째로 DNS 장애를 겪는다는 점. 그래서 노드로컬 캐시를 붙일 때는 반드시 라이브니스 체크와 CoreDNS 폴백 설정을 함께 챙겨야 한다.

그래서 지금 뭘 봐야 하나

CoreDNS 메트릭에서 coredns_dns_response_rcode_count_total{rcode="NXDOMAIN"}을 먼저 본다. 이 값이 NOERROR 대비 4배 근처면 딱 ndots:5의 그림자다. 정상적인 클러스터에서 NXDOMAIN 비율이 지나치게 높다면 대부분 이 문제다.

두 번째로는 응답 시간 히스토그램. coredns_dns_request_duration_seconds 버킷에서 짧은 쪽으로 잘 몰려 있는지 확인. P99가 다른 대역과 극단적으로 벌어져 있으면 negative caching 만료 순간의 부하일 가능성이 높다.

마치며

ndots:5는 나쁜 값이 아니다. Kubernetes의 서비스 이름 규약을 고려하면 합리적인 선택이다. 다만 지금 워크로드가 외부 API 위주로 바뀌었다면, 그 규약이 만들어질 때의 전제와 실제 상황이 어긋난다.

우리 팀은 결국 신규 워크로드는 ndots:2 + FQDN 사용, 레거시는 NodeLocal DNSCache로 커버, 이렇게 두 축으로 정리했다. 완벽하지 않다. Sidecar가 있는 Pod은 dnsConfig가 컨테이너별로 다르게 먹지 않아서 여전히 애매한 케이스가 있고, dnsPolicy와의 상호작용도 문서만 보고서는 헷갈리는 구석이 남아 있다. 여기까지는 좀 더 파봐야 할 것 같다.

혹시 다른 접근으로 잡으신 분 있으면 댓글로 공유 부탁드립니다.