지난주 화요일 새벽 3시 12분, 폰이 울렸다. 사내 슬랙 챗봇이 coredns_cache_misses_total 급증을 감지해서 온콜을 깨웠다. 처음엔 흔한 오탐이겠거니 하고 눈을 감으려다가, 대시보드를 열어보니 정말로 뭔가 이상했다. 클러스터 전체 파드에서 DNS 실패 로그가 초당 수천 건씩 쏟아지고 있었다.
노드 24대, 파드 2100여 개가 돌고 있는 EKS 1.30 클러스터였다. 평소에도 CoreDNS는 조용한 편이라서 크게 신경 쓰던 컴포넌트는 아니었는데, 이날 이후로 우리 팀의 DNS 관측 표준이 완전히 바뀌었다.
처음 본 지표
먼저 CoreDNS 메트릭을 봤다. 평소 P99 응답 시간이 3ms 언저리에서 놀던 게 갑자기 800ms까지 튀어 있었다. 그리고 coredns_dns_responses_total{rcode="NXDOMAIN"}이 평소 대비 40배 정도 폭증하고 있었다.
# 평시
coredns_dns_responses_total{rcode="NXDOMAIN"} → 초당 15~30건
# 장애 시점
coredns_dns_responses_total{rcode="NXDOMAIN"} → 초당 1200~1400건
솔직히 이 숫자만 보고 있으면 원인을 짐작하기 어렵다. NXDOMAIN이 늘어난 건 결과일 뿐, 왜 늘어났는지가 진짜 문제다. 게다가 어떤 파드가 이렇게 많은 실패를 만들어내는지도 로그만으로는 불분명했다.
삽질 1 — 애먼 앱을 의심하다
새벽 3시 반쯤 첫 번째 실수를 했다. 대시보드에서 최근 배포된 서비스가 딱 하나 있었는데, 그놈이 범인이겠거니 하고 롤백을 걸었다. 근데 5분 지나도 NXDOMAIN 그래프는 요지부동. 오히려 좀 더 올라가는 것 같기도 했다. 멘탈이 좀 흔들렸다.
30분을 그렇게 날리고 나서 접근 방식을 바꿨다. "누가 그러는지" 대신 "무엇을 물어보는지"부터 봐야 했다.
CoreDNS 로그 플러그인 켜기
당황해서 처음엔 안 켜져 있던 log 플러그인을 부랴부랴 활성화했다. Corefile에 이 정도 추가했다.
.:53 {
errors
health
ready
log {
class denial
}
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
ttl 30
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
}
log { class denial }은 NXDOMAIN이랑 SERVFAIL 같은 실패 응답만 찍어준다. 전체 쿼리 로깅은 부담이 커서 웬만하면 안 쓰는 게 낫다. 우리는 이날 잠깐만 켰다가 원인 잡고 바로 껐다.
로그를 5분쯤 흘려보내고 grep 몇 번 돌리니까 패턴이 눈에 들어왔다. some-service.default.svc.cluster.local.default.svc.cluster.local 이런 식으로 도메인이 두 번 붙어서 나가는 쿼리가 어마어마했다. 이게 뭐지 싶었다.
진짜 원인 — ndots와 search 도메인의 만남
이걸 이해하려면 파드 안 /etc/resolv.conf를 봐야 한다.
nameserver 10.100.0.10
search default.svc.cluster.local svc.cluster.local cluster.local ap-northeast-2.compute.internal
options ndots:5
ndots:5가 뭐냐면, 쿼리에 점(.)이 5개 미만이면 리졸버가 먼저 search 도메인을 순서대로 다 붙여보고, 그것도 실패하면 그제서야 절대경로로 시도한다는 뜻이다. 쿠버네티스 기본이 이렇게 되어 있다.
문제는 우리 팀에서 최근에 어떤 마이크로서비스가 외부 API를 호출하는데, 그 도메인이 api.example.internal 이런 식으로 점이 2개짜리였다. ndots:5 룰 때문에 리졸버는:
api.example.internal.default.svc.cluster.local→ NXDOMAINapi.example.internal.svc.cluster.local→ NXDOMAINapi.example.internal.cluster.local→ NXDOMAINapi.example.internal.ap-northeast-2.compute.internal→ NXDOMAINapi.example.internal→ 그제서야 성공
이걸 요청 1건마다 5번 물어본다. 심지어 IPv4/IPv6 조회를 따로 하니까 실제로는 10번이다. 이 서비스가 초당 수백 건씩 외부 API를 때리는데 커넥션 풀 재사용이 잘 안 되고 있었고, 결국 NXDOMAIN이 이 지경까지 튀어 오른 거다.
이걸 알고 나서 조금 허탈했다. 사실 이 함정은 CoreDNS 문서나 여러 SRE 포스트에도 자주 언급되는, 나름 유명한 이슈다. 근데 실제로 겪기 전까지는 "그게 나한테 오겠어?" 싶었던 게 문제였다.
이날 밤에 한 것과 다음 날 한 것
새벽에는 두 가지만 했다.
첫째, 문제 파드의 Deployment에 dnsConfig를 넣어서 ndots를 낮췄다.
spec:
template:
spec:
dnsConfig:
options:
- name: ndots
value: "2"
이거 하나만으로도 NXDOMAIN 그래프가 즉시 90% 이상 꺾였다. 마음이 좀 놓였다.
둘째, CoreDNS cache 플러그인의 negative caching TTL을 조금 손봤다. 기본이 사실 꽤 짧아서, 실패 응답이 계속 원본까지 다녀오고 있었다.
cache 30 {
success 3600
denial 9984 15
}
denial 9984 15는 NXDOMAIN 응답을 15초간 최대 9984건 캐시한다는 의미다. 15초를 고른 건 우리 팀 배포 주기랑 시크릿 rotation 주기를 고려해서 절충한 값이다. 여기서 너무 길게 잡으면 도메인이 진짜로 생겼을 때 반영이 늦어져서 다른 문제가 터진다. 우리는 처음에 60초로 갔다가 시크릿 이름 오타를 고쳤는데도 파드가 실패를 계속 리턴하는 걸 보고 15초로 내렸다.
다음 날에는 좀 더 근본적인 것들을 정리했다.
그 이후에 팀에서 정한 것들
일단 관측성부터 다시 짰다. coredns_dns_responses_total{rcode="NXDOMAIN"}에 대한 burn rate 알람을 만들었다. 그동안은 CoreDNS의 pod restart 여부와 latency만 봤지, 실패 응답 비율은 아예 안 보고 있었다. 새벽 3시에 도착한 알람도 사실은 cache miss 알람이었지 NXDOMAIN 자체는 아니었다. 지표가 있어도 알람이 없으면 소용이 없다는 걸 새삼 배웠다.
NodeLocal DNSCache 도입도 이참에 다시 검토했다. 예전에 "우리 규모엔 오버킬"이라고 결론 냈었는데, 이번 사건으로 파드 → CoreDNS 사이의 UDP 홉이 얼마나 취약한 지점인지 실감했다. 지금은 스테이징에서 검증 중이다. 결과는 아직 지켜봐야 알 것 같다.
그리고 ndots 관련 지침을 팀 위키에 정리했다. 규칙은 단순하다. 외부 도메인 호출이 많은 서비스는 배포 스펙에 dnsConfig.options.ndots: "2"를 명시적으로 넣는다. 내부 서비스 호출만 쓰는 파드는 기본값 그대로 둔다. 이걸 강제하는 admission webhook까지 만들지는 고민 중이다. 그 정도까지 갈 만한 빈도인지는 좀 더 지켜봐야 한다.
남은 생각
솔직히 이 사건 자체는 별로 특별하지 않다. ndots:5 함정은 오래된 이야기고, 우리보다 훨씬 크게 데인 팀들이 세상에 널려 있다. 다만 나한테 남은 교훈은 두 가지였다.
하나는, "우리 규모에는 굳이"라고 미뤄둔 것들이 결국 새벽 3시에 청구서로 돌아온다는 것. NodeLocal DNSCache, CoreDNS 상세 지표 알람, ndots 정책. 다 언젠가 해야지 하다가 미뤄둔 것들이었다.
또 하나는, 처음부터 로그를 봤어야 했다는 것. 30분 넘게 애먼 배포를 롤백하고 있던 나 자신이 좀 어이없었다. 지표는 결과만 알려주고, 원인은 로그에 있다. 이건 다음번 장애에도 잊지 말아야겠다.
혹시 비슷한 상황을 겪은 파이 있다면 어떻게 대응했는지 궁금하다. NodeLocal DNSCache 실제 운영해보신 분들의 후기가 특히 궁금하다.
'IT > 기타' 카테고리의 다른 글
| ArgoCD ApplicationSet Progressive Sync가 내부적으로 어떻게 동작하는가 (0) | 2026.07.09 |
|---|---|
| Istio ambient vs sidecar, 우리 팀은 뭘 골랐나 (0) | 2026.07.05 |
| 새벽 3시, CoreDNS NXDOMAIN 폭주로 잠을 못 잤다 (0) | 2026.06.20 |
| 사실 내부적으로는 — Istio ambient mode ztunnel과 waypoint가 동작하는 방식 (0) | 2026.06.19 |
| cert-manager HTTP-01 challenge가 자꾸 timeout 나면 1.17 확인해보세요 (0) | 2026.06.14 |