무슨 변화인가
Cloudflare는 1.1.1.1 DNS 캐시의 메모리 배치를 최적화해 전체 플릿 기준 약 100TB의 메모리를 절감했다고 밝혔다. 대상은 1.1.1.1, Gateway DNS, DNS Firewall, AS112 등 여러 DNS 서비스를 떠받치는 Big Pineapple 플랫폼이다. 이 플랫폼은 어느 시점이든 2,500억 개가 넘는 DNS 캐시 항목을 저장하므로, 항목 하나에서 1바이트만 줄여도 전체로는 250GB가 넘는 차이가 난다.
이번 글에서 중요한 지점은 “Rust로 만들었다”가 아니라, 대규모 캐시에서 자료구조 선택이 비용과 지연 시간에 동시에 영향을 준다는 점이다. Cloudflare는 캐시 항목 저장 방식을 다섯 차례에 걸쳐 바꾸면서 항목당 메모리 사용량을 50% 넘게 줄였고, 동시에 삽입 처리량은 43% 늘고 조회 지연 시간은 19% 줄었다고 설명한다.
개발자가 먼저 봐야 할 핵심
이 사례는 특정 서비스 운영자의 내부 최적화이지만, 개인 개발자와 연구자, 인프라 팀에도 적용 가능한 판단 기준을 준다. 캐시, 큐, 인덱스, 세션 저장소처럼 항목 수가 계속 늘어나는 구조에서는 작은 필드 하나가 전체 메모리 비용으로 증폭된다. 특히 DNS처럼 읽기 경로가 뜨겁고 항목 수가 큰 시스템에서는 “저장 후 다시 바뀌지 않는 데이터인가”, “필드가 실제로 필요한가”, “중복 정보를 추론할 수 있는가”를 먼저 확인해야 한다.
Cloudflare의 캐시는 콜드 스타트 때 비어 있다가 DNS 질의가 들어오면서 채워진다. 최대 항목 수에 도달하면 오래되었거나 덜 인기 있는 항목을 제거해 공간을 만든다. 여기에 EDNS Client Subnet이 들어가면 같은 질의라도 클라이언트 네트워크에 따라 권한 서버가 다른 답을 줄 수 있어, 동일 질의의 여러 버전을 캐시에 보관하게 된다. 즉 캐시 항목 수뿐 아니라 항목 하나가 차지하는 메모리도 운영 지역과 트래픽 성격에 따라 달라진다.
첫 번째 교훈: 저장 후 변하지 않는 값에는 성장 가능한 컨테이너가 과하다
원문에서 가장 직관적인 최적화는 Vec와 String을 Box 기반 저장 방식으로 바꾼 것이다. Rust의 Vec는 포인터, 길이, 용량을 가진다. 데이터를 계속 추가할 수 있어야 하므로 현재 길이보다 더 큰 용량을 미리 잡을 수 있고, 그 용량 정보도 구조체 안에 남는다. 하지만 DNS 응답이 캐시에 저장된 뒤 더 이상 수정되지 않는다면 이 “앞으로 커질 가능성”은 필요 없는 비용이 된다.
Cloudflare는 각 캐시 항목에 Vec와 String 계열 필드가 8개 있었고, 이를 Box와 Box 기반 문자열 저장으로 바꾸면 필드당 8바이트, 항목당 64바이트를 줄일 수 있다고 설명한다. 여기에 Vec가 미래 확장을 위해 잡아둔 초과 힙 공간까지 줄어든다. 2,500억 개가 넘는 항목 규모에서는 이 변화 하나만으로도 15TB가 넘는 절감 효과가 된다.
개발자가 자기 코드에 적용할 때는 단순히 “Vec를 없애자”가 아니라 데이터의 생명주기를 먼저 봐야 한다. 생성 중에는 Vec가 자연스럽고, 저장 이후에는 고정 크기 슬라이스나 박스형 배열이 더 맞을 수 있다. 반대로 저장 이후에도 항목을 추가하거나 정렬하거나 재작성한다면 성장 가능한 컨테이너가 여전히 필요할 수 있다.
두 번째 교훈: 여러 리스트를 하나로 합치고 경계만 저장할 수 있다
DNS 응답은 answer, authority, additional 같은 여러 섹션으로 나뉜다. 처음 설계에서는 각 섹션을 별도 리스트로 보관하는 방식이 이해하기 쉽다. 하지만 리스트가 여러 개면 각 리스트마다 포인터와 길이 정보가 붙는다. Cloudflare는 세 섹션을 하나의 리스트로 저장하고, 각 섹션의 시작 위치를 오프셋으로 기록하는 방식을 사용했다.
원문에 따르면 DNS 레코드 개수는 u16 범위에 담을 수 있어, 별도 Box 두 개가 들고 있던 포인터와 길이 대신 2바이트 오프셋 두 개를 저장할 수 있었다. 이 변경으로 항목당 28바이트를 줄였다. 또 Rust 구조체의 정렬과 패딩 때문에, 작은 필드를 제거했을 때 실제 구조체 크기는 그 필드 크기보다 더 크게 줄어들 수 있다. 여러 boolean 값을 bitflag로 묶은 것도 이런 효과와 연결된다.
이 접근은 로그 이벤트, 검색 결과, 프로토콜 메시지, 컴파일러 토큰 스트림처럼 “논리적으로는 여러 구간이지만 물리적으로는 연속 배열로 둘 수 있는 데이터”에 응용할 수 있다. 다만 오프셋 방식은 인덱스 계산이 잘못되면 버그가 더 찾기 어려워지므로, 경계 조건 테스트가 필요하다.
세 번째 교훈: 반복되는 소유자 정보는 필요할 때만 저장한다
DNS 레코드에는 owner, 즉 해당 레코드가 속한 도메인이 있다. 많은 경우 owner는 사용자가 질의한 도메인과 같다. 예를 들어 example.com의 A 레코드를 물으면 answer 섹션의 레코드 owner도 example.com인 경우가 흔하다. 하지만 CNAME이 섞이면 질의한 이름과 실제 A 레코드 owner가 달라질 수 있다.
DNS wire format은 RFC 1035의 이름 압축으로 반복되는 도메인 이름을 줄일 수 있다. 그러나 Cloudflare의 캐시는 조회 경로에서 압축 포인터를 따라가는 비용을 피하기 위해 레코드마다 전체 owner를 저장해 왔다. 이번 최적화에서는 owner가 질의 도메인과 같으면 저장하지 않고, 읽을 때 질의 도메인에서 추론한다. CNAME 뒤의 A 레코드처럼 owner가 다를 때만 전체 이름을 저장한다.
이 방식은 데이터베이스 정규화와 비슷하지만, 무조건 중복 제거를 하자는 뜻은 아니다. 뜨거운 조회 경로에서 매번 조인이나 포인터 추적이 필요해지면 지연 시간이 늘 수 있다. Cloudflare 사례의 핵심은 대부분의 항목에서 안전하게 추론 가능한 중복만 제거했다는 점이다.
벤치마크를 볼 때 확인할 기준
Cloudflare는 무작위로 생성한 캐시 항목으로 벤치마크를 구성하되, 실제 운영 트래픽 분포에 가깝게 맞췄다고 설명한다. 분포는 A 레코드 56%, AAAA 25%, TXT 19%였고, 각 항목에는 1개에서 4개의 레코드가 들어갔다. TXT는 A와 AAAA가 아닌 가변 길이 레코드 유형의 대체값으로 사용했고, 크기는 64바이트에서 224바이트 사이로 무작위화했다.
또 Rust의 System allocator를 감싼 커스텀 allocator로 항목당 할당 횟수와 크기를 측정했고, 메모리와 함께 삽입 처리량 및 조회 지연 시간도 봤다. 원문은 이 입력이 운영 환경을 완전히 재현한 것은 아니며, 프로세스 메모리는 트래픽 구성, 캐시 점유율, allocator 상태, 캐시 바깥의 메모리 사용량에도 영향을 받는다고 밝힌다. 그래서 롤아웃 과정에서 실제 프로덕션 인스턴스의 resident memory도 측정했다.
| 확인 항목 | Cloudflare 사례에서 본 기준 | 내 시스템에 적용할 때 볼 점 |
|---|---|---|
| 항목 수 | 2,500억 개 이상 | 항목당 몇 바이트가 전체 비용으로 얼마나 커지는지 계산 |
| 데이터 변경 여부 | 캐시 저장 후 DNS 응답은 수정되지 않음 | 생성 단계와 저장 후 단계를 분리해 자료구조 선택 |
| 중복 필드 | owner가 질의 도메인과 같으면 저장 생략 | 항상 같은 값인지, 예외가 있는지 먼저 검증 |
| 성능 검증 | 메모리뿐 아니라 삽입 처리량과 조회 지연 시간 측정 | 절감 효과가 지연 시간 악화로 바뀌지 않는지 확인 |
내 코드에서 점검할 구체적인 위치
- 캐시 엔트리 구조체: Vec, String, HashMap처럼 성장 가능한 필드가 저장 이후에도 필요한지 확인한다.
- 응답 섹션 저장 방식: 논리 구간이 여러 개라도 물리적으로 하나의 배열과 오프셋으로 표현할 수 있는지 본다.
- boolean 필드: 여러 플래그가 구조체 패딩을 키우고 있다면 bitflag로 묶을 여지가 있는지 확인한다.
- 반복 문자열: 대부분 동일한 도메인, 경로, 사용자 식별자, 네임스페이스를 매 항목에 저장하고 있는지 점검한다.
- 벤치마크 입력: 실제 트래픽 분포와 너무 다른 샘플로 측정하고 있지 않은지 확인한다.
바로 적용하기 전에 필요한 검증
이 사례를 보고 곧바로 모든 Vec를 Box로 바꾸는 것은 위험하다. 먼저 “저장 이후 불변”이라는 조건이 코드와 운영 흐름에서 실제로 맞는지 확인해야 한다. 캐시 갱신, TTL 재계산, hit counter 증가, 오류 기록 추가처럼 항목 내부가 바뀌는 경로가 있다면 필드별로 분리해서 판단해야 한다.
또 메모리 절감은 단독 지표가 아니다. Cloudflare는 더 적은 할당과 나은 메모리 지역성 덕분에 속도도 개선됐다고 설명하지만, 다른 시스템에서는 반대로 역직렬화 비용이나 오프셋 계산 비용이 늘 수 있다. 따라서 최소한 항목당 메모리, 총 resident memory, 삽입 처리량, p50과 p99 조회 지연 시간, 롤백 가능성을 함께 측정해야 한다.
개인 사용자와 연구자에게 주는 의미
개인 프로젝트나 연구 코드에서도 캐시가 작을 때는 이런 최적화가 과해 보일 수 있다. 하지만 데이터셋이 커지거나 장시간 실행되는 서비스가 되면 상황이 달라진다. 크롤러의 URL 캐시, 임베딩 검색 인덱스, 로컬 DNS 프록시, 로그 분석 파이프라인, 테스트 결과 저장소처럼 항목 수가 많고 구조가 반복되는 곳에서는 같은 원칙이 유효하다.
반면 항목 수가 적고 병목이 네트워크나 디스크라면 자료구조 미세 최적화보다 관측 지표를 먼저 갖추는 편이 낫다. 이 글의 실용적인 적용 기준은 “항목 하나의 절감량”이 아니라 “항목 수와 반복 빈도를 곱했을 때 의미 있는 운영 비용으로 바뀌는가”다.
개발팀에서 검토할 순서
- 가장 큰 캐시 또는 장기 보관 인메모리 구조체를 찾는다.
- 항목 하나의 실제 크기와 할당 횟수를 측정한다.
- 저장 후 변하지 않는 필드와 계속 바뀌는 필드를 분리한다.
- 중복 저장된 문자열, 리스트, 플래그, 메타데이터를 표시한다.
- 대표 트래픽 분포로 벤치마크를 만든다.
- 메모리 절감과 조회 지연 시간을 함께 비교한다.
- 프로덕션 일부 구간에서 resident memory를 확인한 뒤 확대한다.
출처와 검증
이 글은 Cloudflare Blog가 2026년 8월 27일 공개한 글을 바탕으로, 개발자가 자기 시스템에 적용할 때 확인해야 할 기준을 정리한 것이다. 수치와 사례는 Cloudflare가 공개한 범위 안에서만 사용했다.
원문: https://blog.cloudflare.com/dns-cache-memory-optimization-1111/