Skip to content

필사 모드: DNS 내부 구조 완전 해부 — 재귀/권한, EDNS0, DNSSEC, DoH/DoT/DoQ, CoreDNS, Happy Eyeballs까지

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

들어가며 — "DNS 문제"라는 말의 무게

장애 회고에서 가장 자주 등장하는 문장:

> "처음엔 앱 문제인 줄 알았는데, DNS였어요."

농담처럼 돌아다니는 말: **"It's always DNS."** 실제로 AWS 2019년 EC2 장애, Akamai 2021년 인터넷 셧다운, Facebook 2021년 6시간 다운 — 전부 DNS가 얽혀 있다.

왜 이렇게 자주 터지는가. 왜 Kubernetes 개발자는 "DNS가 느려요"라는 말을 한 달에 한 번은 듣는가. 이 글에서 해체해본다.

- **DNS 조회 한 번의 풀 경로** — 재귀 리졸버부터 13개 루트까지

- **CoreDNS 내부** — Kubernetes의 숨은 심장

- **Happy Eyeballs** — IPv4/IPv6 경쟁의 예술

- **DNSSEC/DoT/DoH/DoQ** — 보안과 프라이버시의 진화

- **퍼블릭 리졸버 대전** — 1.1.1.1 vs 8.8.8.8 vs 9.9.9.9

- **실전 장애 사례 3개**

- **ndots=5 함정** — Kubernetes의 성능 killer

> 이전 글 [OpenTelemetry 완전 해부](/blog/culture/2026-04-15-opentelemetry-observability-traces-metrics-logs-profiles-otlp-collector-sampling-deep-dive-guide-2025)에서 관측성의 신호를 이야기했다. DNS 지연은 그 신호로 포착하기 가장 어려운 영역 중 하나 — 대부분의 APM이 "getaddrinfo"를 애플리케이션 span으로 잡지 못하기 때문이다.

1. DNS의 본질 — 1983년의 타협

왜 이런 계층 구조인가

1983년 Paul Mockapetris가 DNS를 설계할 때의 환경: ARPANET의 모든 이름-IP 매핑이 **`HOSTS.TXT`라는 한 파일**에 있었다. SRI에서 관리하며 FTP로 배포. 호스트가 늘어나자 완전 불가능해짐.

해법: **분산된 계층 트리**.

. (root)

/ | \ ...

.com .org .kr

/ | \

example mozilla co

/ | \ \

www mail api naver

각 노드가 자기 밑의 네임을 위임받은 **권한 서버(authoritative server)**를 가진다.

두 종류의 서버

- **권한 서버 (Authoritative)** — 특정 영역(zone)의 레코드를 가진 서버. 예: Cloudflare가 `example.com`의 권한 서버

- **재귀 리졸버 (Recursive Resolver)** — 클라이언트 대신 여러 권한 서버를 돌아다니며 답을 찾는 서버. 예: 8.8.8.8

클라이언트가 직접 루트 서버에 질의하지 않는 이유 — 너무 많은 질의가 루트에 몰릴 것. 재귀 리졸버가 캐싱하며 부하를 분산한다.

13개의 루트 서버 (진짜는 수천 개)

루트 서버는 **A.root-servers.net ~ M.root-servers.net**까지 13개다. 이건 이름이 13개라는 뜻이지, 서버가 13대라는 게 아니다. 각 네임 뒤에 **Anycast**로 수백 개의 실제 인스턴스가 전 세계에 분산돼 있다.

"왜 13개?" DNS 패킷의 UDP 최대 크기 512바이트에 모든 루트 서버 IP를 담으려면 13개가 한계였다. 지금은 EDNS0로 더 큰 패킷을 쓸 수 있지만 13개 숫자는 관례로 유지.

2. 조회 한 번의 풀 여정 — www.example.com

`curl https://www.example.com`을 쳤을 때 밀리초 단위로:

T+0ms — 브라우저/OS 캐시

1. 브라우저 자체 캐시 확인 (Chrome은 `chrome://net-internals/#dns`)

2. 없으면 OS 리졸버 (systemd-resolved, nscd 등) 캐시

3. 없으면 `/etc/hosts` 확인

4. 없으면 OS가 `getaddrinfo` 시스템 콜

T+0~5ms — `/etc/resolv.conf` 기반 재귀 리졸버로

nameserver 8.8.8.8

options ndots:5

search default.svc.cluster.local svc.cluster.local cluster.local

OS가 `nameserver`에 지정된 재귀 리졸버에 UDP/53으로 질의.

T+5~200ms — 재귀 리졸버의 일

리졸버에 `www.example.com`의 A 레코드가 캐시에 없다면:

1. **루트 서버에 질의**

- "`.com`은 누구 담당?"

- 루트 서버 응답: "`com`의 권한 서버는 `a.gtld-servers.net` 등"

2. **`.com` 권한 서버에 질의**

- "`example.com`은 누구 담당?"

- 응답: "`example.com`의 권한 서버는 `ns1.example.com` 등"

3. **`example.com` 권한 서버에 질의**

- "`www.example.com`의 A는?"

- 응답: "93.184.216.34"

4. 리졸버가 클라이언트에게 답변 + TTL만큼 캐시

TTL이 300이면 5분간 같은 재귀 리졸버에 오는 질의는 캐시로 응답. **이게 루트 부하 관리의 비결**이다.

T+200ms~ — TCP 연결

IP 받았으니 이제 TCP SYN부터 시작.

3. 레코드 타입 — 단순한 A 레코드만이 아니다

모던 DNS에는 수십 개의 타입이 있다. 중요한 것들:

| 타입 | 용도 | 예시 |

|---|---|---|

| A | IPv4 주소 | 93.184.216.34 |

| AAAA | IPv6 주소 | 2606:2800:220:1::... |

| CNAME | 다른 이름으로의 별칭 | www.example.com → example.com |

| MX | 메일 서버 | mail.example.com 우선순위 10 |

| TXT | 임의 텍스트 (SPF, DKIM, 소유권 검증) | v=spf1 include:_spf.google.com ~all |

| NS | 해당 영역의 네임서버 | ns1.example.com |

| SOA | 영역 시작 | serial, refresh, retry, expire |

| SRV | 서비스 위치 + 포트 | _sip._tcp.example.com |

| CAA | 인증서 발급 허가 | letsencrypt.org만 허용 |

| HTTPS/SVCB | HTTPS 설정 (ALPN, ECH) | h3 ALPN, ech |

| PTR | IP → 이름 (역방향) | 34.216.184.93.in-addr.arpa → ... |

| DNSKEY/DS/RRSIG | DNSSEC 서명 | ... |

**HTTPS/SVCB** (2023 표준화)는 브라우저가 HTTPS 연결 세부사항을 DNS로 미리 받는 신규 표준. HTTP/3와 ECH(Encrypted Client Hello)를 배포하기 위한 핵심 인프라.

4. EDNS0 — 512바이트의 족쇄를 풀다

원래 DNS UDP는 512바이트 제한

RFC 1035(1987) 규정. 그 이상이면 TCP fallback. 느리고 방화벽에서 자주 막힘.

EDNS0 (RFC 6891, 2013 완성)

OPT 레코드로 확장. 클라이언트가 "나 4096바이트 UDP 받을 수 있어"라고 알림. 지금은 **대부분의 리졸버가 기본 1232바이트**(MTU 고려).

EDNS0가 중요한 이유:

- DNSSEC 서명이 들어가면 응답이 커져서 필수

- **ECS (EDNS Client Subnet)** — 리졸버가 클라이언트 네트워크를 권한 서버에 힌트로 전달. CDN이 정확한 지리적 응답 가능

5. DNSSEC — 답변을 위조할 수 없게

문제: 평문 DNS는 위조 가능

전통 DNS는 서명이 없다. 중간자가 "www.google.com은 실은 1.2.3.4야"라고 응답을 주입할 수 있다 (DNS Cache Poisoning, Kaminsky 공격 2008).

DNSSEC의 아이디어

각 영역이 **공개키 서명**으로 레코드를 보증. 체인 오브 트러스트:

Root key → .com key → example.com key → 레코드 서명

(DS) (DS) (RRSIG)

- **DNSKEY** — 공개키

- **RRSIG** — 레코드 서명

- **DS** — 부모가 자식의 키를 해시로 보증

- **NSEC/NSEC3** — "이 이름 존재 안 함"의 부정 증명

왜 널리 못 퍼졌나

- 키 관리 복잡도

- 레코드 크기 증가 (UDP fragmentation 이슈)

- 리졸버 캐시와의 상호작용

- TLD 가입 속도: .gov는 높지만 .com은 약 5%

2024년 기준 도메인의 **약 10%만 DNSSEC 서명**. 그래서 최근 관심은 DNSSEC보다 **DoH/DoT** 쪽으로 이동.

6. DoT / DoH / DoQ — 전송 계층의 혁명

왜 암호화가 필요한가

평문 DNS는:

- ISP가 어떤 사이트를 방문했는지 훔쳐봄

- 국가 차원의 검열(예: DNS 차단)

- 광고 회사의 프로파일링

세 가지 진화

**DoT (DNS over TLS)** — RFC 7858, 2016

- 포트 853

- TCP + TLS

- 전용 포트라 방화벽이 차단 가능

**DoH (DNS over HTTPS)** — RFC 8484, 2018

- 포트 443 HTTPS

- HTTP/2 또는 HTTP/3

- 다른 HTTPS 트래픽과 구분 불가 → 검열 회피에 강함

- 그러나 ISP 입장에서 가시성 상실 → **논란**

**DoQ (DNS over QUIC)** — RFC 9250, 2022

- QUIC (UDP 기반)

- DoT의 지연 + DoH의 은닉성을 둘 다 해결

- 모바일에서 연결 마이그레이션 이득

브라우저와 OS 지원

- Firefox: DoH 기본 ON (미국) via Cloudflare

- Chrome: Secure DNS 설정 ON 가능

- Windows 11: DoH OS 레벨 지원

- Android 9+: DoT 자동

- iOS 14+: DoH/DoT profile

논란 — "중앙집권화"

DoH가 도입되면 ISP가 아닌 **Cloudflare/Google에 DNS를 중앙 집중**. 프라이버시냐 의존성이냐의 딜레마. Firefox는 "TRR (Trusted Recursive Resolver)" 정책으로 파트너 리졸버들을 필터링.

7. CoreDNS — Kubernetes의 DNS

왜 kube-dns에서 CoreDNS로?

Kubernetes 1.11(2018)부터 기본 DNS가 CoreDNS. 이유:

- kube-dns: dnsmasq + kubedns + sidecar 3개 프로세스

- CoreDNS: Go 기반 단일 바이너리, 플러그인 구조, 메모리 적음

플러그인 체인

CoreDNS의 설정 `Corefile`:

.:53 {

errors

health

kubernetes cluster.local in-addr.arpa ip6.arpa {

pods insecure

fallthrough in-addr.arpa ip6.arpa

}

forward . /etc/resolv.conf

cache 30

loop

reload

loadbalance

}

요청이 들어오면 플러그인을 **위에서 아래로** 통과. `kubernetes` 플러그인이 답을 못 주면 `forward`가 업스트림 DNS(예: 1.1.1.1)로 전달.

Kubernetes 내부에서 어떻게 동작하나

- Pod가 `/etc/resolv.conf`에 **CoreDNS의 Service IP**(예: 10.96.0.10)를 갖는다

- Pod가 `mysvc.default.svc.cluster.local`을 질의하면 CoreDNS가 kubernetes 플러그인을 통해 API Server에서 Service IP를 조회 (실제론 informer 캐시)

- 외부 도메인은 `forward`로 노드 `resolv.conf`의 리졸버로 전달

NodeLocal DNSCache — 필수 성능 튜닝

기본 구성은 **매 Pod의 DNS 질의가 CoreDNS Pod로 NAT됨**. kube-proxy의 conntrack 테이블이 터지거나, 질의가 많은 앱은 CoreDNS가 포화.

해결: **NodeLocal DNSCache**. 각 Node에 DNS 캐시 DaemonSet을 띄워 Pod가 127.0.0.1로 질의. 로컬 히트 시 CoreDNS까지 안 감. NAT 우회. conntrack 절약.

Kubernetes 1.18+ 권장 구성. 설치 한 번으로 DNS 지연 P99가 수십 ms에서 1ms로 떨어지는 경우가 흔하다.

8. ndots=5 — Kubernetes의 DNS 성능 함정

증상

Python 앱이 `external-api.com`을 호출. 대부분은 잘 되는데 가끔 지연 + 타임아웃.

원인

Pod의 `/etc/resolv.conf`:

search default.svc.cluster.local svc.cluster.local cluster.local

options ndots:5

`ndots:5`는 **"이름에 점(.)이 5개 미만이면, search 도메인을 먼저 붙여 시도"**.

`external-api.com`은 점이 1개. ndots 5 미만이므로 다음 순서로 질의:

1. `external-api.com.default.svc.cluster.local` → NXDOMAIN

2. `external-api.com.svc.cluster.local` → NXDOMAIN

3. `external-api.com.cluster.local` → NXDOMAIN

4. `external-api.com` → 성공

**질의가 4배**로 늘어난다. CoreDNS 부하, 지연 증가.

해결

방법 1: FQDN 사용 — 끝에 점을 찍기

urlopen("https://external-api.com.") # 마지막 점이 FQDN 표시

방법 2: ndots 낮추기

spec:

dnsConfig:

options:

- name: ndots

value: "1"

방법 3: Pod의 `dnsPolicy: None` + 수동 구성

어느 방법이든 **명시적**으로 해야 한다. 기본값은 Kubernetes DNS 내부 조회를 위한 설계로, 외부 API 호출이 많은 앱에서는 역효과다.

9. Happy Eyeballs — IPv4 vs IPv6의 미학

문제: 이중 스택 지연

IPv4/IPv6 이중 스택 환경에서 브라우저가 IPv6를 먼저 시도했는데 IPv6 경로가 막힘 → 타임아웃까지 수 초 대기 → IPv4 재시도 → UX 악화.

해답: Happy Eyeballs (RFC 8305, 2017)

1. A와 AAAA를 **동시에** 질의

2. AAAA가 먼저 오면 IPv6 연결 시도

3. **50ms 이내**에 IPv6 연결이 안 되면 IPv4도 **병렬로** 시도

4. 먼저 성립하는 걸 사용, 다른 건 폐기

구현은 curl, Chrome, Firefox, macOS, iOS 등 거의 모든 현대 스택에 들어있다.

v2의 개선 (2017)

- 주소 정렬: GUA(Global) 우선, 그다음 IPv4, 그 다음 ULA

- 캐시된 "최근 성공한 연결"을 기억해 다음번엔 바로 시작

이 알고리즘 덕에 IPv6 배포가 UX 손실 없이 진행될 수 있었다.

10. 퍼블릭 리졸버 대전

2018년 이후 "개인용 빠른 DNS" 시장이 열렸다.

주요 플레이어

| 리졸버 | 특징 |

|---|---|

| **1.1.1.1** (Cloudflare + APNIC) | 속도 최강, 24시간 로그 삭제, DoH/DoT 공식 |

| **8.8.8.8** (Google) | 지연 안정적, 로그 48시간 보관 |

| **9.9.9.9** (Quad9) | 악성 도메인 차단, IBM + 글로벌 사이버 협회 |

| **208.67.222.222** (OpenDNS/Cisco) | 필터링 기반 (무료/유료 가족 모드) |

| **94.140.14.14** (AdGuard) | 광고/추적 차단 |

선택 기준

- **지연**: 일반적으로 1.1.1.1이 가장 빠르지만 지역에 따라 다름. `dnsperf`로 실측 권장

- **프라이버시**: Cloudflare가 공개 감사 보고서로 가장 투명. Google은 역사적으로 로그 분석 이슈

- **필터링**: 멀웨어 차단이 필요하면 9.9.9.9

ISP vs 퍼블릭

많은 ISP가 DNS를 **광고에 활용**한다 (예: NXDOMAIN을 자기 검색 페이지로 리다이렉트). 퍼블릭 리졸버는 기술적으로 중립적이지만, 중앙집중의 우려가 있다.

11. 실전 장애 사례 3개

사례 1: AWS 2019년 Route 53 이벤트 (DNSSEC 관련)

Route 53이 일시적으로 DNSSEC 서명 체인 오류 → 특정 영역 조회 실패 → 거기 의존하던 서비스 연쇄 장애. 교훈: **DNSSEC 활성화는 모니터링과 함께**.

사례 2: Facebook 2021년 10월 6시간 다운

BGP 라우팅 실수로 페이스북의 **권한 DNS 서버가 인터넷에서 사라짐**. 외부에서 facebook.com을 조회하면 "권한 서버 응답 없음". 내부 직원도 페이스북 내부 도구가 DNS에 의존해서 복구도 지연.

교훈:

- DNS 서버를 **주력 제품과 같은 네트워크**에 두지 말기

- 비상 접근 경로 준비

- 장애 복구 체계에 DNS 독립성 고려

사례 3: Akamai 2021년 7월 1시간 셧다운

Akamai Edge DNS의 소프트웨어 버그로 전 세계 DNS 조회 실패. 뉴욕증권거래소, 맥도날드, 영국항공 등 1000+개 고객사 장애. 교훈: **DNS 공급자 다중화** (Route 53 + Cloudflare + NS1 등 2~3개 병행)를 대규모 서비스는 고려한다.

12. Zone 파일과 동적 DNS

Zone 파일의 고전 포맷

$TTL 3600

$ORIGIN example.com.

@ IN SOA ns1.example.com. admin.example.com. (

2026041501 ; serial

3600 ; refresh

600 ; retry

604800 ; expire

3600 ) ; minimum

@ IN NS ns1.example.com.

@ IN A 93.184.216.34

www IN A 93.184.216.34

mail IN A 93.184.216.35

@ IN MX 10 mail.example.com.

여전히 많은 권한 서버가 이 포맷으로 영역을 로드.

동적 DNS (DDNS)

- **nsupdate** (RFC 2136) — DNS 메시지로 레코드 추가/삭제

- **API 기반** — Route 53, Cloudflare API

- 동적 IP를 가진 홈서버/IoT 기기에서 활용

ExternalDNS (Kubernetes)

Kubernetes Service/Ingress의 외부 호스트를 **자동으로 DNS 공급자에 레코드로 생성**. Route 53/Cloudflare/GCP Cloud DNS 지원. GitOps 환경에서 Ingress hostname 관리의 정석.

13. 모니터링 — DNS의 보이지 않는 지연 잡기

메트릭

- Query rate (QPS)

- 응답 코드별 분포 (NOERROR, NXDOMAIN, SERVFAIL, REFUSED)

- 지연 히스토그램 (P50, P99)

- 캐시 hit ratio

로그

CoreDNS의 `log` 플러그인:

[INFO] 10.244.0.5:37321 - 12345 "A IN example.com.default.svc.cluster.local. udp 65 false 512" NXDOMAIN qr,aa,rd 158 0.000234s

`NXDOMAIN`이 많은 이름 → ndots 함정 의심.

도구

- **dig** — 표준. `dig +trace`로 루트부터 경로 보기

- **dnsperf** — 벤치마크

- **dnstap** — 구조화된 DNS 로그 포맷

- **dnsviz.net** — DNSSEC 체인 시각화

14. DNS 설계 실무 체크리스트 12가지

1. **TTL을 설계하라** — 짧으면 부하, 길면 배포 시 스위칭 지연. 배포 전 TTL 미리 줄이기

2. **NS 레코드 공급자 2개 이상** — 단일 공급자 장애 대비

3. **DNSSEC를 켤 거면 모니터링 필수** — 키 롤오버 실수가 전체 다운

4. **CNAME 루프 조심** — CNAME 체인이 깊으면 타임아웃

5. **SPF/DKIM/DMARC** — 이메일 스푸핑 방지. 요즘 TXT 레코드 필수

6. **CAA 레코드** — 인증서 발급 가능한 CA 제한

7. **Wildcard의 위험** — `*.example.com`이 예상 외 서브도메인까지 잡음

8. **ECS(EDNS Client Subnet) 정책** — 리졸버가 클라이언트 IP 힌트를 권한 서버로 전달. CDN엔 유익, 프라이버시엔 논란

9. **Kubernetes는 NodeLocal DNSCache 필수** — 대규모에서 특히

10. **앱에서 외부 도메인은 FQDN(뒤에 점) 쓰기** — ndots 함정 회피

11. **DoH/DoT 도입 전 내부 관측성 계획** — 평문 DNS에 의존하던 IDS가 먹통

12. **`/etc/hosts` 의존 지양** — 배포 환경마다 다른 예외의 원흉

다음 글 예고 — HTTP/3와 QUIC의 세계

DNS는 UDP 기반이지만 전통적으로 간단한 UDP만 썼다. 지금 웹의 최전선은 **UDP 위에 쌓은 새 전송 프로토콜 QUIC**이다. 다음 글에서는:

- **왜 HTTP/2가 아직 한계인가** — HOL blocking의 그림자

- **QUIC의 0-RTT와 1-RTT** 핸드셰이크

- Stream multiplexing — TCP에서 불가능했던 것

- Connection Migration — 와이파이에서 LTE로 끊김 없이

- **HTTP/3의 채택률** — Cloudflare, Google, Facebook의 실측

- **WebTransport** — QUIC 위의 양방향 메시징

- TCP BBR과 QUIC 혼잡 제어의 관계

- QUIC의 비용 — 암호화 부담과 중간 장비 문제

1996년 HTTP/1.0 이후 30년, 전송 계층의 근본적 전환점을 짚어본다.

> **"QUIC은 TCP+TLS+HTTP/2의 장점만 추리고 TCP의 근본 문제를 우회한 사실상의 TCP 후계자다."**

현재 단락 (1/228)

장애 회고에서 가장 자주 등장하는 문장:

작성 글자: 0원문 글자: 8,946작성 단락: 0/228