Cloudflare công bố một bài deep-dive ngày 27 tháng 8 về 5 tối ưu memory-layout cho Big Pineapple, DNS cache viết bằng Rust đứng sau 1.1.1.1, DNS Firewall, và các dịch vụ DNS khác của họ. Con số nổi bật: bộ nhớ mỗi entry giảm từ 953 byte xuống 420 byte — cắt 56% — và với hơn 250 tỷ entry được cache trên toàn fleet, con số này giải phóng khoảng 100TB RAM. Đi kèm là hiệu ứng phụ: throughput insert tăng từ 625K lên 893K entry/giây, và latency lookup giảm từ 828ns xuống 670ns. Không phải lỗi đánh máy — TB chứ không phải GB, và lợi ích hiệu năng đến “miễn phí” cùng lợi ích bộ nhớ.
Hầu hết chúng ta không vận hành DNS cache ở lưu lượng của Cloudflare. Nhưng tôi đọc những bài như thế này giống cách đọc internal của query planner: không phải vì sẽ vận hành ở quy mô đó, mà vì các kỹ thuật dùng lại được trực tiếp trong bất kỳ service Rust hoặc C++ hot-path nào giữ hàng triệu object nhỏ, sống lâu trong bộ nhớ — cache, session store, in-memory index. Tôi đi qua bài viết và map từng kỹ thuật với vấn đề đã thực sự gặp trong production.
Các kỹ thuật, và nơi chúng generalize
1. Sắp xếp lại field trong struct để đóng gói (packing). Rust (giống C) không đảm bảo thứ tự field khớp thứ tự khai báo, nhưng struct định nghĩa ngây thơ vẫn lãng phí byte cho padding khi các field kích thước khác nhau xen kẽ nhau. Một struct như:
struct DnsEntry {
ttl: u32, // 4 byte
flags: bool, // 1 byte + 3 padding
record_type: u16, // 2 byte + 2 padding
expiry: u64, // 8 byte
}
sắp lại theo kích thước giảm dần loại bỏ hầu hết padding đó:
struct DnsEntry {
expiry: u64, // 8 byte
ttl: u32, // 4 byte
record_type: u16, // 2 byte
flags: bool, // 1 byte + 1 padding
}
Riêng cái này thường lời 10-20% trên struct có field độ rộng hỗn hợp, và không tốn gì cả — #[repr(Rust)] đã tự sắp lại cho type không phải #[repr(C)] ở compiler gần đây, nhưng nhiều team vẫn pin #[repr(C)] vì lý do FFI hoặc serialization và trả “thuế” padding mà không nhận ra. Điều đầu tiên tôi kiểm tra trong service Rust của chính mình sau khi đọc bài này: struct nào đang pin repr(C) không cần thiết.
2. Arena / bump allocation cho batch sống ngắn. Thay vì heap-allocate từng cache entry riêng lẻ (và trả overhead allocator + fragmentation cho mỗi allocation), Big Pineapple nhóm entry vào arena được allocate hàng loạt và free cùng lúc. Đây là ý tưởng giống bumpalo trong hệ sinh thái Rust hay slab allocator trong C. Bài học generalize được: nếu bạn có object với lifetime tương quan — một batch record cùng hết hạn, hay mọi request trong một connection — arena allocation biến N lần malloc/free nhỏ thành một lần allocate hàng loạt và một lần free hàng loạt. Tôi từng dùng pattern này trong một parser scoped theo request; lợi ích ít về footprint bộ nhớ hơn mà chủ yếu loại bỏ allocator lock contention khi tải cao.
3. String interning cho giá trị lặp lại. DNS record lặp lại domain suffix và record type liên tục. Interning — lưu một bản canonical và tham chiếu bằng một integer ID nhỏ thay vì duplicate string — là wisdom cũ, nhưng dễ bị bỏ qua khi làm nhanh. Nếu bạn đang lưu enum hướng người dùng, category tag, hay hostname lặp lại dưới dạng String trong một cache hot-path, đây gần như là tiền miễn phí. Một intern table HashMap<String, u32> cộng tham chiếu u32 thay vì clone String là một refactor cơ học với lợi ích lớn ở quy mô.
4. Bitpacking flag và enum nhỏ. Boolean flag và enum nhỏ (record type, cache state, priority) được đóng gói vào bitfield thay vì byte/word đầy đủ. Crate bitflags của Rust hoặc bitmask u8 thủ công làm được điều này. Nhỏ trên mỗi entry, nhưng nhân với hàng tỷ entry, nó cộng dồn — đây chính xác là câu chuyện “56% không phải một ý tưởng lớn, mà là năm ý tưởng nhỏ cộng dồn” mà Cloudflare kể.
5. Boxed slice kích thước cố định thay vì container có thể tăng trưởng. Lợi ích đơn lẻ lớn nhất mà Cloudflare nhắc đến (hơn 15TB trong tổng 100TB) là thay Vec<T> và String bằng boxed slice kích thước cố định (Box<[T]>, Box<str>) cho cache entry. Vec và String mang theo field capacity bên cạnh length vì được thiết kế để tăng trưởng — nhưng một DNS record, sau khi vào cache, không bao giờ tăng trưởng nữa. Trả tiền cho capacity dư thừa trên 250 tỷ entry bất biến là lãng phí thuần túy. Bài học generalize được: bất cứ khi nào bạn lưu một collection biết trước là bất biến sau khi khởi tạo, Box<[T]> (qua .into_boxed_slice()) hoặc Box<str> (qua .into_boxed_str()) loại bỏ field capacity miễn phí. Đây là bản năng data-splitting hot/cold kinh điển từ data-oriented design, áp dụng ở cấp độ type thay vì tách struct thủ công.
Phạm vi thành thật
Không kỹ thuật nào riêng lẻ là mới — đều là systems programming giáo khoa. Điều có giá trị trong bài viết của Cloudflare là sự kết hợp và đo lường: năm trick quen thuộc, áp dụng có chủ đích lên một cache high-cardinality, với con số before/after thật (56%, ~100TB toàn fleet) thay vì một “chúng tôi đã tối ưu bộ nhớ” mơ hồ. Đó là phần đáng học cho team của bạn: chọn một cấu trúc in-memory hot-path, đo overhead mỗi entry bằng heap profiler (heaptrack, dhat, hoặc thậm chí kiểm tra sanity std::mem::size_of), rồi áp dụng năm kỹ thuật trên theo thứ tự công sức: sắp lại struct và bitpacking tốn một buổi chiều, arena allocation và chuyển sang boxed slice là một refactor thật sự. Đừng refactor trước; đo trước, nếu không bạn đang tối ưu một struct mà chẳng ai giữ một tỷ bản copy.
Nguồn: Cloudflare: How we saved 100 terabytes of memory by optimizing 1.1.1.1’s DNS cache