38년 된 RFC의 복수 — DNS 레코드 순서가 뒤집히자 리눅스가 멈췄다
도서관 서가에 책이 꽂혀 있다. 분류 번호도 맞고, 책도 전부 있다. 다만 꽂힌 순서만 살짝 바뀌었다. 대부분의 사서는 아무 문제 없이 원하는 책을 찾는다. 그런데 한 명의 사서만큼은 반드시 왼쪽부터 순서대로 훑어야만 책을 찾을 수 있다. 이 사서가 하필 도서관에서 가장 많은 대출 요청을 처리하는 사람이라면? 2025년 12월, Cloudflare의 1.1.1.1 리졸버에서 정확히 이런 일이 벌어졌다.
사건 개요 — 메모리 최적화가 부른 나비 효과
Cloudflare 엔지니어들은 1.1.1.1 리졸버의 캐시에서 메모리 사용량을 줄이는 작업을 했다. 기존에는 새 리스트를 생성하고 레코드를 복사하는 방식이었는데, 기존 리스트에 직접 추가하는 방식으로 변경한 것이다. 코드 변경 자체는 기능적으로 완벽했다. 캐시된 데이터의 내용도 정확했다. 딱 하나, 레코드의 순서만 바뀌었다.
2025-12-02 코드 변경 커밋 (캐시 메모리 최적화)
2025-12-10 테스트 환경 배포 — 이상 없음
2026-01-07 프로덕션 롤아웃 시작
2026-01-08 17:40 UTC 서버 90% 배포 완료 → 장애 발생
2026-01-08 18:27 UTC 롤백 시작
2026-01-08 19:55 UTC 롤백 완료 → 서비스 정상화테스트 환경에서는 한 달 가까이 아무 문제가 없었다. 프로덕션에 90%까지 올라간 1월 8일에야 비로소 DNS 확인 실패 리포트가 쏟아지기 시작했다. 총 장애 시간은 약 2시간 15분이었다.
[💡 잠깐! 이 용어는?] DNS 리졸버(Resolver): 도메인 이름(google.com)을 IP 주소(142.250.196.110)로 변환해주는 서버다. 브라우저에 주소를 입력하면 가장 먼저 DNS 리졸버에 질의가 날아간다. Cloudflare의 1.1.1.1은 전 세계에서 가장 빠른 공개 리졸버 중 하나다.
뒤집힌 순서 — CNAME이 뒤로 밀려났다
DNS 응답에는 여러 종류의 레코드가 함께 담긴다. CNAME은 "이 도메인은 저 도메인의 별칭이다"라고 알려주는 레코드이고, A 레코드는 "이 도메인의 실제 IP 주소는 이것이다"라고 알려주는 레코드다. CNAME 체인이 있는 경우, 기존 코드에서는 항상 CNAME을 먼저 나열하고 A 레코드를 뒤에 붙였다.
[기존 응답 — CNAME 선행]
1. CNAME: www.example.com → cdn.example.com
2. CNAME: cdn.example.com → origin.cdn.net
3. A: origin.cdn.net → 198.51.100.1
[변경 후 응답 — A 레코드 선행]
1. A: origin.cdn.net → 198.51.100.1
2. CNAME: www.example.com → cdn.example.com
3. CNAME: cdn.example.com → origin.cdn.net데이터 자체는 동일하다. 세 레코드 모두 빠짐없이 들어있다. 순서만 뒤집혔을 뿐이다. RFC에도 레코드 순서는 중요하지 않다고 되어 있으니, 이론적으로는 문제가 없어야 한다.
[💡 잠깐! 이 용어는?]
CNAME(Canonical Name) 레코드: 도메인의 별칭을 정의하는 DNS 레코드다. www.example.com이 cdn.example.com을 가리키도록 하면, 최종 IP를 찾기 위해 CNAME 체인을 따라가야 한다. 별칭이 별칭을 가리키면 체인이 길어진다.
RFC 1034의 애매한 유산 — "possibly preface"
1987년에 발행된 RFC 1034에는 다음 문구가 있다.
"The response will include the CNAME record and possibly preface any other records with additional CNAME records."
"possibly preface"라는 표현이 핵심이다. 이걸 "CNAME이 반드시 먼저 와야 한다"로 읽을 수도 있고, "CNAME이 앞에 올 수도 있다" 정도로 읽을 수도 있다. 현대 RFC 문서에서 사용하는 MUST, SHOULD, MAY 같은 규범적 키워드가 빠져 있다.
법률에 비유하면 이런 상황이다. 법 조문에 "아마 이렇게 처리하는 것이 바람직할 것이다"라고 적혀 있는데, 한 판사는 이를 강행 규정으로, 다른 판사는 임의 규정으로 해석하는 것이다. 38년 동안 대부분의 구현체가 CNAME을 먼저 보내는 관행을 따랐기 때문에, 이 모호함이 문제로 드러나지 않았을 뿐이다.
반면 동일 타입 레코드(RRset)의 순서에 대해서는 RFC가 명확하게 말한다.
"The order of RRs in a set is not significant, and need not be preserved."
같은 타입의 레코드끼리는 순서가 상관없다. 하지만 서로 다른 타입의 레코드 간 순서 — 특히 CNAME과 A 레코드 사이의 순서 — 에 대해서는 침묵에 가깝다.
진짜 범인 — glibc의 getaddrinfo
모든 DNS 클라이언트가 깨진 것은 아니었다. 순서에 관계없이 레코드를 처리하는 구현체는 아무 문제가 없었다. 문제는 glibc의 getaddrinfo 함수였다.
glibc의 DNS 리졸버는 응답을 위에서 아래로 한 줄씩 읽는다. 지금 찾고 있는 이름을 추적하면서 레코드를 순차 처리하는데, CNAME 체인이 먼저 나와야 "www.example.com → cdn.example.com → origin.cdn.net" 순서로 따라갈 수 있다. A 레코드가 먼저 나오면 "origin.cdn.net의 IP가 198.51.100.1이라고? 나는 www.example.com을 찾고 있는데?"라며 혼란에 빠진다.
| DNS 클라이언트 | 레코드 순서 의존 여부 | 장애 영향 |
|---|---|---|
| glibc getaddrinfo | 의존 | 영향 받음 |
| systemd-resolved | 무관 | 정상 |
| macOS resolver | 무관 | 정상 |
| Windows DNS Client | 무관 | 정상 |
여기서 파급력이 폭발한다. glibc는 리눅스 서버 생태계의 표준 C 라이브러리다. 컨테이너, 가상 머신, 베어메탈 할 것 없이 리눅스 기반 인프라의 대다수가 glibc를 사용한다. 즉, 서버 사이드 인프라의 상당 부분이 영향권에 들어간 것이다.
[💡 잠깐! 이 용어는?]
glibc(GNU C Library): 리눅스에서 가장 널리 쓰이는 C 표준 라이브러리다. getaddrinfo는 도메인 이름을 IP로 변환하는 POSIX 표준 함수로, 거의 모든 네트워크 프로그램이 내부적으로 호출한다. curl, wget, 대부분의 웹 프레임워크가 이 함수를 거친다.
테스트가 놓친 것 — 기능은 맞지만 부수 효과가 틀렸다
Cloudflare는 오랫동안 CNAME을 먼저 배치해 왔다. 하지만 이것이 의도된 동작이었는지 우연한 부수 효과였는지 구분하지 않았다. 메모리 최적화 테스트는 "캐시된 데이터가 정확한가?"를 검증했지, "레코드 순서가 유지되는가?"를 검증하지 않았다.
택배 비유가 적절하다. 상자 안의 물건은 전부 맞다. 파손도 없다. 다만 포장 순서가 바뀌었는데, 자동 분류기가 맨 위 물건을 보고 분류하는 방식이었기 때문에 전체 물류 라인이 멈춰버린 것이다. "내용물이 맞는가?"만 테스트했지 "포장 순서가 맞는가?"는 테스트하지 않았다.
이런 종류의 버그는 테스트하기 가장 까다로운 유형에 속한다. 명세에 없는 암묵적 계약이기 때문이다. 오래된 코드의 부수 효과가 시간이 지나면서 사실상의 인터페이스 계약이 되어버린 케이스다.
Cloudflare의 대응과 후속 조치
Cloudflare는 변경을 즉시 롤백하고 두 가지를 선언했다. 첫째, CNAME 순서를 앞으로 변경하지 않겠다. 둘째, RFC에 CNAME 레코드가 다른 레코드보다 먼저 와야 한다는 명확한 규정을 추가하도록 제안하겠다.
명세의_모호함: RFC의 "possibly preface"는 MUST가 아니었다
암묵적_계약: 수년간 유지된 부수 효과가 사실상의 계약이 될 수 있다
테스트_공백: 기능 정확성뿐 아니라 순서 같은 부수 효과도 검증 대상이다
영향_범위: glibc 기반 리눅스 인프라가 핵심 피해자였다
후속_조치: CNAME 순서 고정 + RFC 개선 제안마무리
38년 전 누군가가 RFC 문서에 "possibly"라고 적은 한 단어가 2025년 인터넷 인프라에 2시간짜리 구멍을 냈다. 명세서에 "순서는 중요하지 않다"고 적혀 있어도, 실제 구현체가 순서에 의존한다면 그것이 사실상의 명세가 된다. 코드를 최적화할 때는 기능의 정확성만 검증하면 안 된다. "이 코드가 지금까지 우연히 보장해온 것은 무엇인가?"까지 질문해야 한다. 부수 효과의 일관성 — 테스트 목록에서 가장 빠지기 쉬운 항목이지만, 빠졌을 때 가장 큰 사고를 치는 항목이다.
참고:
관심 있을 만한 포스트
Cloudflare Code Mode — 2,500개 API를 1,000 토큰에 담는 MCP의 새로운 패턴
Cloudflare가 공개한 Code Mode는 AI 에이전트에게 수천 개의 API 엔드포인트를 단 2개 도구로 제공하는 MCP 서버 설계 패턴이다.
Accept: text/markdown — AI 에이전트가 HTML 대신 마크다운을 받는 시대
Cloudflare가 AI 에이전트를 위해 HTML을 마크다운으로 자동 변환하는 기능의 동작 원리와 의미를 살펴본다.
좀비 TV 400만 대의 35초 — 2025년 DDoS 공격은 어떻게 역대 기록을 갈아치웠나
2025년 DDoS 공격이 전년 대비 두 배 이상 증가하고, 역대 최대 31.4 Tbps 공격이 기록된 Cloudflare Q4 리포트 분석.
뱀의 탈피에서 배운 서버 재시작 — Rust로 커넥션 제로 로스를 구현하는 ecdysis
Cloudflare가 5년간 프로덕션에서 검증한 Rust 무중단 재시작 라이브러리 ecdysis를 오픈소스로 공개했다.
Next.js 블로그 만들기 — GitHub Pages에서 Cloudflare Pages로 이전하기
GitHub Pages의 한계를 넘어 Cloudflare Pages로 블로그를 이전한 과정. 비교, 설정, SEO까지 한 번에 정리.
AI 코딩의 맹점 — Artifacts 없이 에이전트는 기억을 잃는다
PRD, ADR, TDD가 AI 코딩 워크플로우에서 왜 선택이 아닌 필수인지, 실전 구조와 함께 살펴본다.
Next-Translate 3.0 — Turbopack과 App Router를 위한 i18n 재건
1년간 공백 후 돌아온 Next-Translate 3.0이 Turbopack 지원, 비동기 params, App Router 안정화를 한 번에 처리하는 방법.
V8 WasmGC 투기적 최적화 — 가상 메서드를 인라인으로 만드는 법
V8이 WasmGC의 가상 메서드 디스패치에 투기적 인라이닝을 도입해 Dart와 Java 앱에서 최대 8% 성능을 끌어낸 방법.