TECH 으로 돌아가기
TECH HACKER NEWS 오늘 8분 읽기 22 READS

바이트 단위 최적화로 100TB를 절약한 클라우드플레어 DNS 캐시 이야기

바이트 단위 최적화로 100TB를 절약한 클라우드플레어 DNS 캐시 이야기
SOURCE IMAGE · HACKER NEWS

1바이트가 250기가바이트가 되는 세계가 있다. 클라우드플레어의 1.1.1.1, 게이트웨이 DNS, DNS 방화벽, AS112 등을 떠받치는 내부 플랫폼 '빅 파인애플(Big Pineapple)'은 어느 순간에나 2,500억 개가 넘는 DNS 캐시 항목을 메모리에 담고 있다. 이 규모에서는 항목 하나당 단 1바이트를 낭비해도 전체 서버 군집에서 250GB가 증발한다. 클라우드플레어는 Rust로 작성된 캐시 저장 구조에 다섯 차례에 걸쳐 손을 대 항목당 메모리 사용량을 56% 줄였고, 그 결과 서버 군집 전체에서 약 100TB의 메모리를 회수했다. 이는 자사 13세대 서버 130대에 탑재된 RAM과 맞먹는 양이다. 더 주목할 점은 공간을 줄이면서 속도까지 얻었다는 것이다. 삽입 처리량은 43% 늘었고 조회 지연은 19% 줄었다.

캐시는 콜드 스타트 시 비어 있다가 질의가 도착하면서 채워지고, 최대 항목 수에 도달하면 오래되거나 인기 없는 항목을 축출한다. 데이터센터마다 캐시 크기는 다른데, 특히 EDNS 클라이언트 서브넷(ECS)이 쓰이는 곳에서는 같은 질의라도 클라이언트 네트워크에 따라 응답이 달라져 여러 버전을 저장해야 한다. 그만큼 항목 수와 항목당 메모리가 모두 커지므로, 이번 최적화는 ECS 트래픽이 많은 지역일수록 효과가 크다. 각 항목은 무엇을 질의했는지 식별하는 키와, 응답·메타데이터(생성 시각, 히트 카운터, TTL 등)를 담는 값으로 이뤄진 키-값 쌍이다.

자료구조에서 낭비를 걷어내다

첫 두 개선은 Rust 타입 선택의 문제였다. Rust의 Vec는 힙 포인터, 길이, 그리고 용량(capacity) 세 필드를 갖는다. 용량 필드와 미리 잡아둔 여유 공간은 항목을 계속 밀어 넣을 때를 위한 것인데, DNS 응답은 한 번 캐시에 넣으면 다시 수정하지 않는다. 따라서 8바이트짜리 용량 필드도, 여덟 칸을 잡아두고 다섯 개만 채운 힙의 빈 슬롯도 모두 낭비다. 성장할 수 없는 BoxBox로 바꾸면 이 둘이 사라진다. 항목당 이런 필드가 여덟 개였으니 필드당 8바이트, 항목당 64바이트가 절약됐고 전체로는 15TB가 넘었다. 이어 응답의 answer·authority·additional 세 구획을 별도 리스트로 두는 대신 하나의 리스트에 u16(2바이트) 오프셋으로 구획 경계를 표시해 항목당 28바이트를 더 줄였다. 여러 불리언 필드를 비트플래그 하나로 묶자 정렬을 맞추기 위한 패딩까지 함께 사라져, 실제 절감폭은 필드 크기 합보다 더 컸다.

도메인 이름과 레코드 데이터를 다시 설계하다

세 번째는 레코드의 소유자(owner) 이름 처리다. 대부분의 레코드는 소유자가 질의한 도메인과 동일하다. CNAME이 끼면 달라지기도 하지만, 그 예외를 제외하면 소유자 이름을 굳이 저장할 필요가 없다. 클라우드플레어는 소유자가 질의 도메인과 같으면 OptionNone으로 두고, 응답을 조립할 때 이미 손에 쥔 캐시 키에서 도메인을 복원한다. 조회 시점마다 어차피 키가 있으니 힙 할당 없이 이름을 되살리는 것이다. 소유자가 다를 때만 Some에 힙 포인터를 담는다. RFC 1035의 이름 압축을 조회 핫패스에서 따라가는 비용이 크기 때문에, 여기서는 의도적으로 메모리 대신 속도를 택했다.

네 번째는 열거형(enum) 크기 문제였다. Rust의 enum은 언제나 가장 큰 변형(variant) 크기를 차지한다. DNS 레코드 종류를 enum 변형으로 담으면 가변 텍스트 세 개와 도메인, 정수 둘을 가진 NAPTR(136바이트) 탓에 태그와 패딩까지 더해 전체가 144바이트가 된다. 그런데 4바이트면 되는 A 레코드와 16바이트인 AAAA가 트래픽의 80% 이상이라, 대부분의 레코드가 120바이트 넘는 패딩을 헛되이 지고 있었다. 큰 변형만 박싱(boxing)해 힙으로 내보내면 enum은 8바이트 포인터만 남고, A·AAAA는 레코드당 120바이트를 아낀다. 다만 박싱에는 jemalloc이 크기 클래스 단위로 반올림하며 생기는 할당 오버헤드와, 데이터가 힙 여기저기 흩어지며 나빠지는 캐시 지역성이라는 대가가 따랐다.

다섯 번째 개선이 이 두 대가를 정리했다. 파싱된 enum 목록 대신, 각 레코드를 2바이트 길이 접두사 뒤에 원시 바이트로 인코딩해 하나의 Box에 연속으로 담았다. enum 변형 오버헤드와 박싱 할당이 사라지고 데이터가 붙어 있어 CPU 캐시 지역성이 좋아진다. 응답을 만들 때도 A·AAAA·TXT와 DNSSEC 레코드는 인코딩된 바이트를 그대로 복사하면 되고, 이름 압축이 필요한 CNAME·NS·MX·SOA만 파싱한다. 직접 복사가 가능한 레코드가 다수라 조회 경로의 작업이 줄어 지연이 5% 감소했다. 삽입 시에는 재사용되는 스크래치 버퍼에 먼저 쓴 뒤 한 번에 Box로 복사하는 방식으로 레코드별 개별 할당을 없애, 이 변경만으로 삽입 처리량이 13% 올랐다.

실무자가 가져갈 교훈과 한계

프로덕션 롤아웃은 2026년 5월 18일 시작해 7월 6일 전 서비스에 걸쳐 완료됐고, 각 릴리스가 개선을 하나씩 반영하면서 메모리 사용량(p90·p98·p99)이 계단식으로 떨어졌다. 이 사례가 주는 실무적 시사점은 분명하다. Rust에서 컬렉션 타입을 관성적으로 Vec이나 String으로 쓰는 관행, enum이 최대 변형 크기로 팽창하는 성질, 정렬 패딩이 숨기는 낭비는 모두 규모가 커질 때 비로소 청구서로 돌아온다. 반대로 말하면, 불변 데이터를 다룰 때 성장 가능한 타입을 버리고 데이터를 연속 배치하는 것만으로 메모리와 속도를 동시에 개선할 여지가 크다는 뜻이다.

동시에 몇 가지 한계도 짚어둘 만하다. 벤치마크는 A 56%·AAAA 25%·TXT 19%라는 프로덕션 분포를 흉내 낸 근사치일 뿐, 실제 프로세스 메모리는 트래픽 구성, 캐시 점유율, 할당자 상태, 캐시 외부 메모리에 따라 달라진다. 그래서 클라우드플레어도 벤치마크와 별개로 프로덕션 상주 메모리를 실측했다. 또한 원시 바이트 저장 방식은 레코드를 임의 인덱싱할 수 없어 순차 순회가 필요하고, 소유자 이름 생략과 이름 압축 회피는 메모리 대신 속도를 택한 명시적 트레이드오프다. 즉 이 최적화들은 2,500억 항목이라는 특정 규모와 '한 번 쓰고 수정하지 않는' 캐시 특성 위에서 성립한다. 자신의 워크로드에 그대로 옮기기 전에, 어떤 데이터가 정말 불변이며 어떤 지점에서 공간과 속도를 맞바꿀지를 먼저 계산해봐야 한다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://blog.cloudflare.com/dns-cache-memory-optimization-11...
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기
처리 중...