Cloudflare, 1.1.1.1 DNS 캐시 구조 최적화로 메모리 100TB 절감
- Cloudflare의 Big Pineapple은 2,500억 개가 넘는 DNS 캐시 항목을 유지하며, 항목당 1바이트 낭비도 전체 플릿에서 250GB 이상이 됨
- 캐시 항목 저장 방식을 다섯 차례 바꿔 항목당 메모리를 50% 이상 줄이고, 전체에서 약 100TB를 확보함. Gen 13 서버 130대의 RAM 규모임
- 할당 횟수 감소와 메모리 지역성 개선으로 삽입 처리량은 43% 증가했고 조회 지연 시간은 19% 감소함
- 수정하지 않는 DNS 응답의
Vec<T>와String을Box<[T]>및Box<str>로 바꿔 용량 필드와 과잉 할당을 제거함. 항목마다 8개 필드에서 64바이트를 절감해 15TB 이상을 아낌 - 벤치마크는 운영 트래픽을 근사해 A 56%, AAAA 25%, TXT 19%로 채운 캐시를 사용하고, 배포 중에는 실제 인스턴스의 상주 메모리도 측정함
Hacker News 의견들
처음 설계할 때 Vec 용량 필드 같은 자명한 낭비를 잡는 리뷰가 없었나 싶음
돌아가고 있었으니 아무도 굳이 들여다보지 않은 거겠지
예전에는 성급한 최적화라고 했겠지만, 메모리 가격이 최대 10배까지 오른 지금은 큰 메모리 사용량을 다시 볼 만함
초기에는 얼마나 커질지, DNS 레코드를 얼마나 들고 있을지 모르니 최적화가 대개 값어치 없었을 듯함. 100TB를 쓰게 된 뒤에야 마이그레이션과 폴백까지 설계하면서 고칠 이유가 생김
사소한 최적화를 설계 단계에서 다 논의하면 시간만 버림. 필요해지면 운영 중인 시스템이 결국 알려줌
롭 파이크 규칙대로 병목은 측정한 뒤에 고쳐야 함. 작은 n에서 복잡한 알고리즘과 자료구조는 상수 비용과 버그만 늘리기 쉬움
Box<[T]>는 그렇게 널리 알려진 최적화도 아니고, 보통 Vec가 아주 많이 들어가진 않음. 여기처럼 항목 수가 수천억 개면 차이가 나는 거지
이런 이유로 시스템 프로그래밍은 계속 중요함. 다만 C 관점에서는 레코드 데이터를 CacheEntry 뒤에 붙여 별도 할당을 없앨 수도 있지 않나 싶음
Rust에서도 동적 크기 타입으로 그런 배치는 기술적으로 가능하지만, 언어의 다른 부분과 잘 맞지 않고 구현이 까다로움. Rust Nomicon도 custom DST는 아직 덜 다듬어진 기능이라고 적어둠
CacheEntry를 고정 크기 원소의 슬라이스나 배열에 넣는 구조라면, 크기가 제각각인 레코드 데이터를 바로 뒤에 붙일 수 없음
Rust는 이런 메모리 배치 트릭에는 불편한 편임. arena도 제대로 쓰기 어렵고, Cloudflare가 메모리 제약 프로젝트에 Zig를 쓰기 시작한 이유가 이런 쪽일 수 있음
데이터베이스 레코드처럼 가변 길이 필드를 연속 메모리에 패킹하는 방식을 언어가 지원하면 좋겠음. Cloudflare는 그걸 수작업으로 구현한 셈임
MaraDNS 블랙리스트에서 항목마다 malloc()할 때는 237MB였는데, 큰 블록 하나를 할당해 순회하게 바꾸니 9.5MB가 됐음
레코드를 길이 접두사와 원시 바이트로 저장하는 건 TLV, 즉 tag-length-value 계열임. 네트워크 프로토콜에 흔하고, 알 수 없는 태그를 건너뛰기에도 좋음
캐시가 2,500억 항목이면 정말 캐시인가 싶음. 80 대 20을 가정하면 1조 2,500억 레코드쯤 되는 건지, 여러 캐시보다 인메모리 DB가 나은지 궁금함
1.1.1.1은 내부 데이터셋 앞단이 아니라 재귀 리졸버임. 캐시 미스면 제3자가 운영하는 authoritative nameserver를 조회해야 하고, DNS 전체 레코드를 미리 모아 직접 제공할 수는 없음. 원본 응답은 1ms에서 2초 이상까지 들쭉날쭉함