IT/기타

새벽 3시, CoreDNS NXDOMAIN 폭주로 잠을 못 잤다

gfrog 2026. 6. 20. 00:47
SMALL

 

처음엔 외부 API를 의심했다

새벽 3시 12분. 슬랙 알림이 한 번에 17개 쌓였다. P99 외부 API 호출 latency가 평소 80ms에서 4초까지 튀었다. 처음엔 SaaS 벤더 쪽 장애인가 싶었는데, 우리 쪽 다른 서비스들도 다 같이 느려지고 있었다. 멘탈이 한번 나가고, 노트북을 켰다.

결론부터 말하면 CoreDNS NXDOMAIN 폭주였다. 솔직히 DNS 문제는 항상 의심해야 한다는 걸 머리로는 알았는데, 실제로 새벽에 당해보니 또 한 번 새겼다. 우리 팀이 운영하는 EKS 클러스터, 노드 38대, 워크로드 약 400개 정도 되는 규모다.

알림 내용은 단순했다. httpClient.get 호출 P99가 폭증 중이라는 거. 외부 결제 SaaS 호출이 대부분이라 벤더 status 페이지부터 봤다. 다 초록불. 그러면 우리 쪽이다.

먼저 한 행동은 노드 로드 확인. CPU도 메모리도 멀쩡했다. 네트워크 PPS도 평소 수준. 그런데 kubectl get pods -A 자체가 평소보다 2-3초 느린 느낌이 들었다. 이때 DNS를 의심했어야 했는데, 나는 API 서버부터 봤다. 약 7분 정도 헛짚었다.

CoreDNS 파드 로그를 보니까 이런 게 끝없이 찍히고 있었다.

[INFO] 10.0.32.18:53124 - 4827 "A IN payments-api.vendor.com.default.svc.cluster.local. udp 91 false 1232" NXDOMAIN
[INFO] 10.0.32.18:53124 - 4828 "A IN payments-api.vendor.com.svc.cluster.local. udp 83 false 1232" NXDOMAIN
[INFO] 10.0.32.18:53124 - 4829 "A IN payments-api.vendor.com.cluster.local. udp 79 false 1232" NXDOMAIN
[INFO] 10.0.32.18:53124 - 4830 "A IN payments-api.vendor.com.ap-northeast-2.compute.internal. udp 97 false 1232" NXDOMAIN

같은 도메인을 search path 다 거치면서 4번씩 묻고 있었다. 게다가 IPv6도 있으니까 AAAA까지 합하면 한 번 호출에 최소 8번. 새벽 트래픽 늘면서 호출 빈도가 평소의 1.5배쯤이 됐는데, 거기에 곱하기 8을 하니 CoreDNS가 못 버틴 거다.

ndots:5의 함정

근데 사실 이건 새로운 문제가 아니다. Kubernetes 기본 ndots:5 정책 때문에 도메인에 점이 5개 미만이면 search domain을 다 시도해본다. payments-api.vendor.com은 점이 2개니까 search list를 한 바퀴 다 돌고서야 직접 쿼리한다.

평소에는 CoreDNS 캐시가 막아주는데, 어제 벤더 쪽에서 도메인 구조를 살짝 바꿨다. payments-api.vendor.compayments-api-v2.vendor.com. 우리 코드 일부는 환경변수로 도메인을 받는데, 일부 파드는 재시작 전이라 옛날 도메인을 들고 있었다. 옛날 도메인은 NXDOMAIN. CoreDNS 캐시는 NXDOMAIN을 길게 캐시하지 않는다(기본 30초).

그래서 매번 search path 8번씩 풀로 돌고, 다 NXDOMAIN, 캐시 짧고, 트래픽 늘고… CoreDNS 파드 2개로는 못 막았다.

임시 처방, 그리고 잘못 쓴 옵션

새벽 3시 30분. 일단 응급 처치로 CoreDNS 레플리카를 2→6으로 늘렸다. 약간 좋아졌지만 근본 해결은 아니다. 옛 도메인을 쓰는 워크로드를 찾아서 강제 재시작했다. 이걸 다 끝내고 보니 4시 10분.

여기서 한번 더 삽질했다. 캐시를 더 길게 해두자 싶어서 CoreDNS 설정에 serve_stale을 켰다.

cache 30 {
    success 9984 30
    denial 9984 5
    serve_stale 1h immediate
}

이게 함정이었다. serve_stale은 캐시가 만료돼도 stale 응답을 반환해주는 옵션인데, NXDOMAIN도 stale로 들고 있는다. 벤더가 새로 도메인을 살려도 우리 CoreDNS는 한 시간 동안 NXDOMAIN을 반환할 수 있다는 뜻이다. 깃허브 이슈(coredns/coredns#3586)에 같은 케이스가 올라와 있다. 다행히 모니터링 그래프 보다가 빠르게 알아채고 끄긴 했는데, 5분 정도는 일부 서비스가 막혔다.

진짜 처방

아침에 정신 차리고 했던 게 세 가지다.

첫째, ndots를 1로 낮췄다. 외부 도메인은 어차피 점이 다 들어가 있으니까 굳이 search list를 돌릴 필요가 없다. 클러스터 내부 서비스는 어차피 FQDN으로 쓰거나, 점 없는 짧은 이름이면 search path 한 번이면 된다.

# Deployment dnsConfig
spec:
  dnsConfig:
    options:
    - name: ndots
      value: "1"

전체 클러스터에 적용하려면 CoreDNS 앞단에서 NodeLocal DNSCache를 두는 편이 깔끔하다. Pod별로 dnsConfig를 일일이 손대고 싶지 않다면 default Corefile을 패치하는 방법도 있는데, 우리는 일단 결제 같은 critical path 워크로드부터 dnsConfig를 박았다.

둘째, NodeLocal DNSCache를 도입했다. 이게 사실 작년 운영 회의에서 "언젠가 해야지"로 미뤄둔 거였다. 노드마다 캐시를 두니까 CoreDNS로 가는 트래픽 자체가 줄어든다. 우리 환경에서는 CoreDNS QPS가 약 70% 줄었다.

# nodelocaldns 데몬셋, 핵심 부분만
clusterDNS: 169.254.20.10
forwardPlugin:
  upstreams:
    - "/etc/resolv.conf"

셋째, autopath 플러그인 검토. CoreDNS의 autopath는 클라이언트 search path를 서버 쪽에서 처리해주는 기능이다. 잘 쓰면 search domain을 안 돌리고도 정답을 찾아준다. 단점은 CoreDNS의 메모리 사용량이 좀 늘고, pod IP를 watch해야 한다. 우리는 도입 전에 staging에서 한 달 정도 더 봐야 할 것 같다.

모니터링이 약했다

이번 사건의 진짜 교훈은 DNS 모니터링이 약했다는 거다. CoreDNS metrics는 이미 노출돼 있었는데, 우리는 그냥 CPU/메모리만 봤다. 다음 메트릭은 알람을 걸어둘 가치가 있다.

  • coredns_dns_responses_total{rcode="NXDOMAIN"} — NXDOMAIN 비율 갑자기 늘면 의심
  • coredns_dns_request_duration_seconds — p99 latency 50ms 넘으면 경고
  • coredns_forward_request_duration_seconds — 업스트림 forward latency
  • coredns_cache_misses_total / coredns_cache_hits_total — hit ratio 60% 이하면 경고

특히 첫 번째 NXDOMAIN 비율은 정말 빠른 사인이다. 평소 우리는 NXDOMAIN이 전체의 2% 정도였는데, 사건 시간엔 28%까지 튀었다. 이거 알람만 걸어뒀어도 한 시간은 일찍 알아챘다.

그래서

DNS는 약하다. 그리고 Kubernetes의 ndots:5는 외부 도메인 호출 많은 클러스터에서는 거의 트랩이다. 처음부터 알았어야 했는데, 운영하면서 큰 사고 없이 지나가니까 손을 안 댔던 부분이었다.

지금 우리 클러스터는 ndots:1 + NodeLocal DNSCache 조합으로 돌아간다. 일주일 지났는데 DNS 관련 알람이 0건이다. 진작에 할 걸 그랬다.

혹시 비슷한 경험 있는 분, 또는 autopath 운영 중인 분 있으면 댓글로 후기 좀 부탁드린다. 우리는 도입 고민 중이라 케이스가 궁금하다.

BIG