CoreDNS 때문에 새벽에 페이지 받은 이야기

지난주 화요일 새벽 3시 12분. 폰이 울렸다. 결제 서비스에서 upstream timeout이 폭발하고 있다는 페이지. 잠결에 노트북 열었을 때 나는 진짜 결제 API가 죽은 줄 알았다.
처음 본 지표
Grafana 대시보드를 열어보니 결제 API 자체는 멀쩡했다. P99 레이턴시가 평소 80ms에서 4초로 튀었을 뿐, 컨테이너는 다 살아있고 로그에도 별다른 에러가 없었다. 근데 트래픽은 확실히 흐르지 못하고 있었다. 뭔가 이상해서 istio access log를 뒤져보니 upstream 접속 자체가 안 되는 케이스가 계속 찍히고 있었다.
노드 자체 리소스는 여유로웠다. CPU 40%, 메모리는 60%대. 그런데 왜?
정신이 살짝 들어서 kube-system 네임스페이스를 봤다. CoreDNS 파드 로그가 도배되어 있었다. [ERROR] plugin/errors: 2 payments-api.svc.cluster.local. A: read udp: i/o timeout. 뭐야 이거.
진짜 원인은 훨씬 밑에 있었다
일단 급한 불부터 껐다. CoreDNS 레플리카를 2개에서 6개로 올렸다. 그러니까 5분쯤 지나서 지표가 서서히 정상으로 돌아왔다. 파드가 부족해서 뻗은 건가 싶었지만 그게 근본 원인일 리 없었다. 어제까지만 해도 잘 굴러가던 클러스터인데.
아침 회의 미룬 다음에 팀 채널에 상황 공유하고 원인 분석을 시작했다. 그런데 이게 생각보다 복잡했다.
CoreDNS 메트릭을 보니 coredns_dns_requests_total QPS가 평소 대비 8배가 튀어 있었다. 그런데 클러스터 트래픽 자체는 30% 정도만 늘었다. 어떻게 DNS 쿼리만 이렇게 폭증할 수가 있지?
여기서 눈치챘어야 했는데, 사실 나는 ndots를 완전히 잊고 있었다.
ndots가 뭐길래
Kubernetes 클러스터 안에서 파드가 뜰 때 /etc/resolv.conf가 자동으로 세팅된다. 대충 이런 식이다.
search my-ns.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5
ndots:5가 뭐냐면, 도메인에 점이 5개 미만이면 로컬 도메인으로 간주해서 search list를 순서대로 다 붙여본다는 뜻이다. 예를 들어 파드가 redis.prod.example.com을 조회한다고 치자. 점이 3개니까 ndots:5 조건에 걸린다. 그러면 CoreDNS는 이렇게 순서대로 물어본다.
redis.prod.example.com.my-ns.svc.cluster.local— 없음, NXDOMAINredis.prod.example.com.svc.cluster.local— 없음, NXDOMAINredis.prod.example.com.cluster.local— 없음, NXDOMAINredis.prod.example.com— 여기서 겨우 응답
즉 외부 도메인 하나 찍는데 쿼리 4번이 발생한다. 이게 IPv4와 IPv6 조합이면 8번이다. 진짜다.
우리 결제 서비스는 마침 어제부터 외부 PG사 신규 엔드포인트를 추가로 조회하기 시작했다. 트래픽이 조금 늘어난 게 이 부분이었고, 근데 각 요청마다 DNS 쿼리는 8배로 증폭되고 있었다. 여기다 CoreDNS 파드 2개가 감당해야 하는 QPS는 계산기 때리기도 무섭더라.
대응하면서 배운 것들
임시로 파드만 늘려놓고 근본 대응을 논의했다. 몇 가지 선택지가 있었다.
첫째, ndots를 낮춘다. 파드 스펙에 dnsConfig로 ndots:2 정도로 낮추면 외부 도메인 조회할 때 불필요한 search list 붙이기가 사라진다. 단점은 클러스터 내부 짧은 서비스 이름 (redis 같은) 조회할 때 실패한다. 우리 팀은 FQDN 쓰는 컨벤션이 이미 있어서 이건 부담이 적었다.
둘째, NodeLocal DNSCache를 붙인다. 각 노드에 DaemonSet으로 캐시 프록시를 띄우고, 파드는 iptables로 로컬 노드의 캐시를 먼저 찌르게 하는 방식. CoreDNS 부담이 확 줄고 응답 시간도 좋아진다. 다만 도입할 때 CNI 설정 손봐야 하고 iptables 규칙이 하나 더 늘어난다.
셋째, autopath 플러그인. CoreDNS에 autopath를 켜면 CoreDNS가 파드 네임스페이스를 알아서 인지해서 search list 순회를 한 번의 응답으로 끝내준다. 라운드트립 5번이 1번이 된다. 매력적이긴 한데, autopath는 pods verified 옵션이 필요해서 CoreDNS가 모든 Pod 변경을 감시해야 한다. 결국 CoreDNS 메모리 사용량이 확 늘고 kube-apiserver 부하도 조금 오른다. 큰 클러스터에서는 이 오버헤드가 은근 크다는 얘기를 들었다.
우리는 결국 첫째 + 둘째 조합을 택했다. FQDN 쿼리하는 결제 서비스는 ndots:2로 내리고, 클러스터 전반에는 NodeLocal DNSCache를 올렸다. autopath는 마지막까지 고민했지만, 이번 케이스처럼 외부 도메인이 많은 워크로드에서는 ndots 조정이 훨씬 직관적이고 안전하다고 결론냈다.
지금 와서 다시 보면
사실 이 문제는 예방할 수 있었다. CoreDNS 대시보드에 request rate와 error rate 알람을 걸어놨다면 새벽 페이지 오기 전에 눈치챘을 것이다. 우리 팀은 CoreDNS를 인프라 컴포넌트로만 취급했지 애플리케이션급으로 지켜보지 않았다.
지금은 이런 것들을 모니터링한다.
coredns_dns_requests_total— QPS 급증 감지coredns_dns_responses_total{rcode="NXDOMAIN"}— NXDOMAIN 비율. 이게 튀면 ndots 문제일 확률이 높다coredns_forward_healthcheck_failures_total— upstream 문제 조기 감지- CoreDNS 파드 CPU/메모리 (특히 캐시 사용량)
새벽 페이지 한 번 받으니까 확실하게 배웠다. Kubernetes에서 DNS는 인프라가 아니라 애플리케이션이다. 성능 이슈 나면 상단 서비스가 다 뻗는다.
혹시 다른 팀은 어떤 조합으로 쓰시는지 궁금하네요. 특히 autopath 실제 프로덕션에 안정적으로 돌리고 계신 분 있으면 후기 좀 부탁드립니다.