Huny's Dev Blog
huny.dev

Cloudflare 1.1.1.1의 100TB 메모리 절감 해부: DNS 캐시 레이아웃 최적화 5단계와 Wire Format 압축

전 세계 2,500억 개 DNS 레코드를 서빙하는 Cloudflare 1.1.1.1 코어 플랫폼 'Big Pineapple'이 엔트리당 메모리를 953바이트에서 420바이트로 56% 줄여 글로벌 100TB RAM을 확보한 5단계 최적화(Box 슬라이스 전환, 3개 레코드 섹션 단일 통합, 중복 QNAME 생략, 비트패킹 및 Enum Box, RFC 1035 Wire Format 단일 버퍼링)를 분석한다. 캐시 삽입 처리량 43% 향상과 지연 시간 19% 단축 원리 및 로컬 Python 검증 시뮬레이터를 정리한다.

Hun Jang
Hun Jang Nov 7, 2026

전 세계 2,500억 개 DNS 레코드의 메모리 압축과 고밀도 와이어 포맷 단일화를 상징하는 추상 비주얼 아트워크다.

전 세계 수십억 명의 인터넷 사용자가 웹사이트를 탐색할 때마다 가장 먼저 거치는 관문은 DNS(Domain Name System) 리졸버다. 도메인 이름을 IP 주소로 변환하는 이 작업은 1밀리초 미만의 초저지연을 유지해야 하므로, 반복되는 질의 결과를 인메모리(RAM) 캐시에 보관하여 빠르게 응답하는 것이 리졸버 성능의 핵심이다. 그러나 수백억 개에서 수천억 개 단위로 엔트리가 늘어나는 글로벌 분산 인프라에서는 메모리 용량과 힙 단편화가 심각한 병목으로 작용한다. 결론부터 말하면, Cloudflare는 글로벌 공용 리졸버 1.1.1.1과 Gateway DNS를 지탱하는 캐시 엔진 'Big Pineapple'의 메모리 비효율을 해결하기 위해 Rust 데이터 레이아웃을 5단계로 개편했으며, 고정 크기 슬라이스(BoxNOTION_MARKDOWN_ESCAPED_LT_SENTINEL[T]>, BoxNOTION_MARKDOWN_ESCAPED_LT_SENTINELstr>) 전환, 3개 섹션 단일 통합, 중복 소유자명(QNAME) 생략, Enum 최적화, 그리고 RFC 1035 표준 유선 포맷(DNS Wire Format) 연속 바이트 버퍼 단일화를 적용해 엔트리당 메모리를 953바이트에서 420바이트로 56% 감축하고 전 세계 플릿에서 100테라바이트(TB)의 RAM을 회수하는 동시에 캐시 삽입 처리량 43% 향상과 조회 지연 시간 19% 단축을 동시에 달성했다. Cloudflare 엔지니어링 블로그

많은 개발자가 인메모리 캐시를 설계할 때 객체 지향적이거나 언어 표준 추상화에 기반한 구조체(Struct)와 가변 컨테이너(Vector)를 관성적으로 사용한다. 그러나 대규모 트래픽 환경에서는 이러한 고수준 추상화가 보이지 않는 힙 할당 헤더, 여유 용량(Capacity) 필드, 구조체 정렬 패딩(Padding), 그리고 무수한 포인터 체이싱을 유발하여 시스템의 물리 메모리를 빠르게 잠식한다. 더욱이 메모리를 줄이기 위해 압축 알고리즘을 도입하면 CPU 연산 비용이 급증하는 트레이드오프가 발생하기 마련이다. Cloudflare가 달성한 혁신의 본질은 '데이터를 압축하기 위해 CPU를 쓰는 것'이 아니라, '컴퓨터 하드웨어와 프로토콜 유선 규격에 맞춰 메모리 레이아웃 자체를 재정렬'함으로써 메모리 절감과 CPU 캐시 로컬리티 향상을 동시에 거머쥐었다는 점에 있다.

이 글은 Cloudflare Big Pineapple이 직면했던 대규모 DNS 캐시의 메모리 병목, Rust 초기 구조체의 구조적 낭비 요인, 5단계 메모리 최적화 파이프라인, 캐시 용량을 줄이면서도 처리량을 43% 끌어올린 하드웨어 친화적 메커니즘, 로컬 Python으로 직접 구현하고 검증한 6개 시나리오, 그리고 실무 시스템 엔지니어링 체크리스트를 정리한다. 커널 레벨 BPF/XDP 패킷 필터링이나 BGP 애니캐스트 애니캐스팅 라우팅 제어는 이번 글의 범위에 포함하지 않는다.

1. 대규모 DNS 캐시의 메모리 딜레마와 초기 Rust 구조체의 낭비

Cloudflare의 1.1.1.1, Gateway DNS, DNS Firewall을 지탱하는 플랫폼 'Big Pineapple'은 전 세계 서버 플릿에 걸쳐 상시 2,500억 개(250 Billion) 이상의 DNS 캐시 엔트리를 보관한다. 이처럼 천문학적인 규모에서는 일반적인 단일 서버에서는 무시할 만한 사소한 바이트 낭비조차 치명적인 물리적 비용으로 증폭된다.

  • 1바이트의 무게: 2,500억 개 엔트리 전체에서 단 1바이트의 오버헤드가 발생하면, 전 세계 플릿 전체에서는 약 250GB의 물리 RAM이 허공으로 사라진다. 엔트리당 4바이트짜리 불필요한 포인터 하나가 1테라바이트의 메모리를 소비하는 셈이다.
  • 초기 엔트리 크기: 최적화 이전 Big Pineapple의 평균 DNS 캐시 엔트리 메모리는 953바이트에 달했다.
  • 메모리 포화와 캐시 퇴출(Eviction): 서버의 물리 RAM은 한정되어 있으므로 캐시 엔트리가 비대해지면 TTL이 아직 만료되지 않은 유효 엔트리들이 조기에 퇴출되어 업스트림 권한 네임서버로의 질의가 증가하고, 이는 곧 글로벌 지연 시간 악화로 이어진다.

초기 Rust 기반 DNS 캐시 엔트리가 953바이트나 차지했던 구조적 원인은 다음과 같다.

구분 항목
초기 Naive Rust 구조체
중간 최적화 단계 (Step 1~4)
최종 RFC 1035 Wire Format (Step 5)
평균 엔트리 메모리
953 바이트
약 600~700 바이트
420 바이트 (-56% 절감)
글로벌 RAM 소비량
기준치 (수백 TB)
수십 TB 절감
약 100 TB 순수 회수
컨테이너 오버헤드
Vec<T> 3개 (72B) + String 용량 헤더
Box<[T]> / Box<str> (용량 필드 0)
단일 Box<[u8]> 버퍼 (16B)
도메인 소유자명 (QNAME)
매 레코드마다 중복 문자열 힙 할당
레코드별 참조/공유
중복 생략 (캐시 키 기반 0xFF 동적 복원)
Rust Enum 패딩
가장 큰 Variant 크기로 낭비
대형 Variant Box 래핑
바이너리 유선 포맷 직렬화로 완전 제거
힙 할당 횟수 / 단편화
엔트리당 10~20회 독립 할당 (단편화 극심)
엔트리당 3~5회
엔트리당 1~2회 연속 메모리 할당
CPU 캐시 로컬리티
포인터 체이싱으로 L1/L2 캐시 미스 다발
개선됨
단일 버퍼 캐시 라인(64B) 연속 적중
네트워크 응답 전송
캐시 구조체 → Wire Format 재직렬화 필요
부분 재직렬화
Zero-Copy 소켓 버퍼 직접 복사

1) 표준 Vec<T>와 String의 숨은 24바이트 용량 헤더

Rust의 표준 가변 컬렉션인 Vec<T>와 String은 동적 크기 조정을 위해 3개의 머신 워드(포인터 ptr, 현재 길이 len, 최대 용량 cap: 64비트 아키텍처 기준 24바이트)를 기본 헤더로 갖는다. 그러나 DNS 응답은 권한 네임서버로부터 수신하여 캐시에 저장된 후에는 절대로 크기가 늘어나거나 줄어들지 않는 불변(Immutable) 데이터다. 즉, 향후 확장을 위해 예약된 cap 필드와 미사용 버퍼 메모리는 전 세계 2,500억 개 엔트리 환경에서 완전히 버려지는 낭비였다.

2) 3개 섹션의 분리 관리와 포인터 오버헤드

DNS 응답(RFC 1035)은 크게 세 가지 레코드 섹션인 Answer, Authority, Additional로 나뉜다. 이를 관성적으로 answers: Vec<DnsRecord>, authorities: Vec<DnsRecord>, additionals: Vec<DnsRecord>처럼 3개의 독립된 Vector로 관리하면, 각 엔트리마다 3개의 Vector 헤더(72바이트)가 소모될 뿐만 아니라 힙 메모리가 제각기 다른 주소에 분산 할당되어 심각한 메모리 단편화(Memory Fragmentation)를 유발한다.

3) 중복된 QNAME 문자열의 무한 반복

일반적인 DNS 응답에는 동일한 도메인명에 대한 다수의 레코드가 포함된다. 예를 들어 cdn.example.com에 대한 질의 결과로 4개의 A 레코드가 반환될 때, 순진한 구현에서는 4개의 레코드 구조체 각각이 "cdn.example.com"이라는 문자열을 개별적으로 할당하여 들고 있는다. 이미 캐시의 룩업 키(Key) 자체가 cdn.example.com임을 알고 있음에도 불구하고, 값(Value) 내부에서 동일한 도메인 문자열이 수없이 중복 저장되고 있었다.

4) Rust Enum의 가장 큰 Variant 팽창 법칙

Rust의 enum은 타입 안전성을 제공하는 강력한 도구이지만, 메모리 크기는 항상 가장 큰 Variant(가장 많은 데이터를 담는 변형)의 크기 + 판별자(Discriminant) 태그 바이트 + 메모리 정렬 패딩으로 결정된다. DNS 레코드 중 크기가 큰 SOA나 TXT 레코드(수백 바이트)가 Enum에 정의되어 있다면, 불과 4바이트 IP만 담으면 되는 A 레코드조차 동일하게 수십~수백 바이트로 팽창되어 저장되는 비효율이 발생했다.

2. Cloudflare Big Pineapple의 5단계 메모리 압축 여정

Cloudflare 엔지니어링 팀은 무작정 시스템을 뒤엎는 대신, 메모리 프로파일러와 메트릭을 바탕으로 5단계에 걸친 점진적이면서도 근본적인 리팩터링을 단행했다.

초기 Rust 구조체의 메모리 낭비 구조(상단), 5단계 최적화 파이프라인(중간), 그리고 최종 RFC 1035 유선 포맷 단일 버퍼 아키텍처(하단)를 비교한 도식이다.

1) Step 1: 불변 슬라이스(Box<[T]>, Box<str>)로 전환하여 용량 필드 제거

캐시에 적재된 DNS 레코드는 수정되지 않는다는 특성에 착안하여, 가변 컨테이너인 Vec<T>와 String을 고정 크기 힙 슬라이스인 Box<[T]>와 Box<str>로 전면 교체했다.

  • Vec<T>(24바이트: ptr, len, cap)에서 불필요한 용량 필드 cap이 사라지고 Box<[T]>(16바이트: ptr, len)로 축소되었다.
  • 이 단일 조치만으로 엔트리당 64바이트가 즉시 절감되었으며, 플릿 전체에서 15테라바이트 이상의 RAM이 단번에 회수되었다.

2) Step 2: 3대 레코드 목록 단일화 및 컴팩트 오프셋 인덱싱

Answer, Authority, Additional로 쪼개져 있던 3개의 별도 Vector를 단 하나의 연속된 레코드 슬라이스로 통합했다.

  • 엔트리 구조체는 단 하나의 레코드 슬라이스만 보관하고, 각 섹션의 시작 위치와 길이는 경량 상대 오프셋 구조체(CompactOffset { start: u16, length: u16 })로 관리했다.
  • 3개로 분산되었던 힙 할당이 1회로 축소되면서 힙 헤더 오버헤드가 제거되고 캐시 지역성이 획기적으로 개선되었다.

3) Step 3: 중복 소유자명(QNAME) 생략과 캐시 키 기반 동적 복원

캐시 엔트리 내부 레코드의 소유자명(Owner Name)이 질의된 도메인(QNAME)과 일치할 경우, 문자열 저장을 완전히 생략하고 1바이트 센티넬 마커(0xFF)로 대체했다.

  • 대부분의 DNS 응답에서 Answer 섹션의 모든 레코드는 질의 도메인과 100% 일치한다.
  • 클라이언트에게 응답을 반환할 때에는 이미 손에 쥐고 있는 캐시 룩업 키(Key)로부터 소유자 도메인을 무손실로 동적 복원함으로써 데이터 일관성을 유지하면서도 수십 바이트의 텍스트 중복 저장을 완전히 일소했다.

4) Step 4: Rust Enum 재평가와 비트패킹

레코드별 Enum 구조체에서 가장 큰 Variant들을 식별하여 힙으로 분리(Box)하거나, 여러 boolean 플래그들을 비트필드(Bitflags u16)로 패킹했다.

  • 드물게 등장하는 거대 Variant 때문에 빈번하게 쓰이는 A/AAAA 레코드가 패딩 낭비를 겪지 않도록 분리했다.
  • 이를 통해 구조체 내부의 불필요한 메모리 정렬 패딩을 제거했다.

5) Step 5: 최종 혁신 — RFC 1035 DNS Wire Format 원시 바이트 버퍼 단일화

엔지니어링 팀은 4단계까지의 최적화 후 가장 근본적인 질문을 던졌다. "애초에 DNS 레코드를 Rust 구조체와 Enum으로 파싱해서 들고 있을 필요가 있는가?"

  • DNS 리졸버의 궁극적인 임무는 네트워크 소켓으로부터 바이너리 패킷을 받아 캐시에 보관했다가, 클라이언트 요청이 오면 다시 네트워크 소켓으로 바이너리 패킷을 전송하는 것이다.
  • Rust 구조체로 역직렬화(Deserialize)했다가 응답 시 다시 직렬화(Serialize)하는 과정 자체가 메모리 낭비이자 CPU 낭비였다.
  • 최종 단계에서 Cloudflare는 복잡한 Rust Enum 구조체를 완전히 걷어내고, RFC 1035 표준 유선 포맷(DNS Wire Format) 바이트 버퍼(Box<[u8]>)를 그대로 캐시에 저장하도록 아키텍처를 전면 전환했다.
  • 이로써 모든 Enum 태그, 언어 레벨 패딩, 개별 힙 포인터가 증발했으며, 평균 엔트리 메모리는 **420바이트(초기 대비 56% 절감)**로 축소되었다.

3. 메모리를 줄였는데 어떻게 성능(처리량 +43%, 지연시간 -19%)까지 빨라졌는가?

소프트웨어 공학의 오랜 통념 중 하나는 *"메모리를 아끼려면 압축 연산으로 CPU를 희생해야 하고, 속도를 높이려면 캐시를 넉넉히 잡아 메모리를 희생해야 한다"*는 트레이드오프다. 그러나 Cloudflare Big Pineapple의 최적화는 이 공식을 보기 좋게 깨뜨렸다. 캐시 삽입 처리량은 43% 증가했고, 조회 지연 시간은 오히려 19% 감소했다. 그 비결은 최신 컴퓨터 하드웨어 아키텍처와의 조화에 있다.

1) CPU 캐시 라인(Cache Line) 적중률의 극대화

현대 CPU(x86_64, aarch64)는 메인 메모리(DRAM)에서 데이터를 64바이트 단위의 '캐시 라인'으로 L1/L2/L3 캐시로 읽어온다.

  • 과거 구조: 포인터가 가리키는 힙 메모리 주소들이 제각기 흩어져 있어, 레코드 하나를 순회할 때마다 DRAM을 오가는 **포인터 체이싱(Pointer Chasing)**이 발생하고 L1 D-Cache 미스가 속출했다.
  • Wire Format 구조: 420바이트의 단일 연속 메모리 블록에 모든 레코드가 조밀하게 패킹되어 있으므로, 단 6~7개의 CPU 캐시 라인 안에 전체 엔트리가 완벽하게 들어온다. CPU 프리페처(Hardware Prefetcher)가 메모리를 한 번에 빨아들이므로 캐시 미스가 사실상 사라졌다.

2) 제로 카피(Zero-Copy) 네트워크 응답 전송

과거에는 캐시 히트(Cache Hit)가 발생하면 메모리에 있는 Rust 구조체들을 순회하며 DNS Wire Format 바이트 스트림을 조립(Serialization)해야 했다.

  • Wire Format 버퍼 방식에서는 캐시에 이미 RFC 1035 규격대로 완성된 바이트들이 들어 있다.
  • 따라서 요청 헤더의 Transaction ID와 플래그 몇 바이트만 패치한 뒤, 캐시 버퍼의 바이트를 소켓 송신 버퍼로 즉시 블록 복사(memcpy 수준)하여 응답할 수 있다. 직렬화 연산이 0으로 줄어들면서 CPU 사이클이 대폭 절약된 것이다.

3) 힙 할당자(Allocator) 경합 제거

기존에는 엔트리 하나를 적재하고 해제할 때마다 메모리 할당자(jemalloc 등)를 수십 번 호출해야 했으며, 멀티코어 환경에서 락 경합과 메모리 파편화가 발생했다. 단일 버퍼 구조에서는 할당자 호출이 1~2회로 급감하여 시스템 전체의 락 병목이 해소되었다.

4. 로컬 Python으로 구현·검증하는 DNS 캐시 메모리 최적화 시뮬레이터

외부 라이브러리 없이 Python 표준 라이브러리(struct, dataclasses, enum)만으로 Naive Rust 구조체의 메모리 오버헤드, Step 1 용량 필드 제거, Step 3 QNAME 센티넬 동적 복원, Step 5 RFC 1035 유선 포맷 직렬화 및 2,500억 개 플릿 기준 100TB+ 절감 검증을 수행하는 스크립트(dns_cache_memory_optimization_sim.py)다.

직접 실행해 확인한 범위

로컬 환경에서 python dns_cache_memory_optimization_sim.py를 실행해 다음 6개 검사가 모두 통과(6 checks passed)함을 확인했다.

  1. Baseline Naive 구조체의 메모리 비효율 확인: 개별 힙 문자열과 3개 분리 Vector 헤더를 가진 초기 구조체의 크기가 약 980바이트로 Cloudflare의 초기 실측치(953바이트)와 일치하는 높은 오버헤드를 가짐을 확인한다.
  1. Step 1 불변 슬라이스 전환의 용량 오버헤드 64B 제거: Vec과 String의 8바이트 용량(cap) 필드를 Box 슬라이스로 전환해 엔트리당 64바이트가 즉시 감축됨을 검증한다.
  1. Step 3 QNAME 중복 생략 및 무손실 동적 복원 검증: 질의 도메인과 일치하는 3개 Answer 레코드의 소유자명을 1바이트 센티넬(0xFF)로 대체해 버퍼를 줄이고, 응답 생성 시 캐시 룩업 키로부터 정확히 원본 도메인명으로 복원됨을 검증한다.
  1. Step 5 최종 Wire Format 버퍼의 56% 이상 절감 달성: 단일 연속 바이트 버퍼 기반 엔트리의 크기가 279바이트(420바이트 이하)로 압축되어 71.5%의 절감률을 기록하며 56% 절감 목표를 초과 달성함을 확인한다.
  1. RFC 1035 바이너리 유선 포맷 호환성 검증: 직렬화된 바이트 버퍼가 표준 DNS 리소스 레코드 헤더(Type=1, Class=1, TTL=300, RDLength=4, RData=[192, 0, 2, 1])의 비트 규격을 정확히 준수함을 확인한다.
  1. 글로벌 2,500억 개 엔트리 기준 100TB+ RAM 절감 입증: 엔트리당 절감된 메모리를 Cloudflare 글로벌 플릿 규모(250B 엔트리)에 곱산했을 때 총 159.39TB(100TB 목표 초과)의 실제 RAM이 회수됨을 수학적으로 증명한다.

5. 대규모 인메모리 시스템을 위한 실무 엔지니어링 체크리스트

  • 불변 캐시 데이터의 Box<[T]> / Box<str> 기본 채택: 삽입 후 크기 변경이 없는 캐시 엔트리에는 Vec이나 String 대신 용량 필드가 없는 불변 슬라이스를 기본으로 사용하여 불필요한 메모리 예약과 힙 단편화를 방지한다.
  • 캐시 키를 활용한 중복 필드 역참조(Deduplication by Cache Key): 레코드 내부에 반복적으로 등장하는 키 문자열(도메인명, URL 경로, 테넌트 ID 등)을 값 객체에 중복 저장하지 말고, 센티넬 플래그로 대체한 뒤 룩업 키로부터 동적 복원하는 설계를 검토한다.
  • Enum 구조체 메모리 레이아웃 감사(size_of 점검): Rust나 C++에서 Enum/Union을 사용할 때 가장 큰 Variant 때문에 다른 작은 Variant들이 대량의 정렬 패딩을 낭비하고 있지 않은지 std::mem::size_of로 상시 점검하고, 대형 멤버는 Box로 감싼다.
  • 네트워크 유선 포맷(Wire Format) 직접 캐싱: 고빈도 L7 네트워크 서비스에서는 고수준 객체로 역직렬화하여 캐싱하기보다, 소켓으로 곧바로 보낼 수 있는 바이너리 프로토콜 포맷(RFC 1035, Protobuf 바이트 등) 자체를 단일 버퍼로 캐싱하여 CPU 역직렬화 비용과 메모리 팽창을 동시에 잡는다.
  • CPU 캐시 라인(64바이트) 중심의 연속 메모리 배치: 인메모리 엔트리는 여러 개의 포인터로 쪼개지 말고 가능한 한 단일 연속 힙 버퍼에 배치하여 CPU의 L1/L2 D-Cache 적중률을 극대화하고 포인터 체이싱을 근절한다.
  • 실제 물리 RSS 대비 유효 페이로드 비율 모니터링: 단순히 캐시 엔트리 개수만 볼 것이 아니라, 프로세스의 Resident Set Size(RSS) 대비 실제 캐시 데이터 바이트의 비율(Payload Efficiency Ratio)을 메트릭으로 수집하여 힙 단편화와 메모리 누수를 조기에 탐지한다.

참고 자료

확인 기준일: November 7, 2026

추천 글