Cloudflare 对 1.1.1.1 使用的 Big Pineapple DNS 缓存做了五项 Rust 层面的内存优化,将单条缓存记录的内存占用降低了 56%。当同一种数据结构在整个服务集群中被复制数十亿次时,这次看似局部的改造最终释放了约 100 TB 内存。
来源摘要没有列出五项优化的具体实现,因此下面不会猜测其内部字段或代码。我们更值得拆解的是这种优化为什么能形成如此大的收益,以及在 Rust 缓存、索引和常驻内存服务中,可以怎样实践同类分析。
真正的成本是“单条开销 × 条目数 × 副本数”
缓存结构的内存成本不能只看一条记录:
total_memory ≈ entry_count × bytes_per_entry × replica_count
+ hash_table_overhead
+ allocator_overhead
+ unused_capacity
假设某个条目从 160 字节缩减到 70 字节,每条只节省 90 字节。单看微不足道,但 10 亿条记录就是约 90 GB;再乘以进程、机器和区域副本,收益很快进入 TB 级。
这也是 56% 这个数字比“删除了几个字段”更重要的原因。它通常意味着团队审查了完整的内存布局,而不只是业务载荷:结构体填充、指针宽度、堆分配、容器容量和哈希表装载率都可能包含在账单里。
Rust 类型看起来很小,运行时布局未必小
Rust 会按照字段的对齐要求在结构体中插入 padding。与此同时,String、Vec<T> 等类型本身还保存指针、长度和容量;在 64 位平台上,它们通常各占三个机器字,这还不包括堆上的内容。
对高基数 DNS 缓存,可以重点检查五类成本。以下是通用审计清单,并非对 Big Pineapple 五项改动的逐条复原:
- 字段宽度:过期时间是否真的需要
u64,状态值是否需要完整整数。 - 容器元数据:不可变数据是否仍在使用带额外 capacity 的
String或Vec<T>。 - 重复分配:一个条目是否包含多个独立堆对象,能否改成连续存储、切片或驻留数据。
- 枚举和可选值:
Option、标签和负缓存状态的组合是否产生了意外的 padding。 - 表级开销:哈希桶、低装载率、过度预留容量和分片元数据是否抵消了条目优化。
字段顺序也值得测量。把高对齐字段集中排列有时能减少结构体尾部或字段间的空洞,但编译器布局和类型语义都可能变化,不能依赖肉眼估算。
可以这样实践:先做一个可运行的布局实验
下面是一个简化示例,用于比较两种 DNS 缓存条目表示。它不代表 1.1.1.1 的实际数据结构,也没有覆盖完整 DNS 记录类型;它只演示字段宽度和容器选择如何影响结构体本体。
将代码保存为 layout.rs,然后直接用 Rust 编译器运行:
use std::mem::{align_of, size_of};
struct EntryWide {
name: String,
wire_answer: Vec<u8>,
expires_at_unix_ms: u64,
response_code: u32,
authoritative: bool,
}
struct EntryCompact {
name: Box<str>,
wire_answer: Box<[u8]>,
expires_after_s: u32,
flags: u8,
}
fn report<T>(name: &str) {
println!(
"{name:>12}: size = {:>3} bytes, align = {} bytes",
size_of::<T>(),
align_of::<T>()
);
}
fn main() {
report::<EntryWide>("wide");
report::<EntryCompact>("compact");
let entries = 1_000_000_000_u64;
let saved = size_of::<EntryWide>() - size_of::<EntryCompact>();
let gib = saved as f64 * entries as f64 / 1024_f64.powi(3);
println!("struct-only saving at {entries} entries: {gib:.2} GiB");
}
rustc -O layout.rs
./layout
Box<str> 和 Box<[u8]> 没有可增长容器的 capacity 字段,因此结构体本体通常更小,也不会保留未使用容量。代价是更新时不能原地扩容,往往需要重新分配。对于写入后不再修改、读取频繁的缓存值,这种交换可能合理;对于持续追加或修改的数据,它可能增加复制成本。
这个程序只测量 size_of::<T>(),不包含字符串内容、响应字节、哈希表桶和分配器元数据。它适合发现布局问题,不足以证明生产环境节省了多少内存。
从类型实验走到生产测量
可靠的优化流程至少需要三层数据:
- 类型层:使用
size_of、align_of和布局工具检查结构体、枚举与 padding。 - 分配层:记录对象数量、分配大小分布、容器的
len/capacity比值以及分配器驻留内存。 - 服务层:观察缓存命中率、淘汰率、请求延迟、CPU 使用率和进程 RSS。
内存下降不等于优化完成。紧凑表示可能引入额外解码、整数溢出边界、时间范围限制或更多复制。例如,把毫秒时间戳改成 u32 相对秒数之前,必须定义时间基准、最大 TTL、回绕行为和升级兼容性。把多次分配合并为连续缓冲区,也可能让单字段更新变得昂贵。
测试时还应固定真实流量分布。DNS 名称长度、记录集合大小、负缓存比例和 TTL 分布都会改变平均条目成本。只用几个手写样例,很容易得到漂亮但无法外推的结果。
采用这类优化前的检查表
可以按以下顺序推进:
- 用生产采样计算每类条目的数量与实际分配成本。
- 优先处理高频类型中的固定开销,而不是只优化少数超大对象。
- 每次只改变一个布局决策,分别测量结构体、堆分配和整表 RSS。
- 为时间范围、长度截断、枚举编码和旧数据兼容性增加边界测试。
- 同时验证命中率、尾延迟和 CPU,避免用计算量换取不可接受的内存下降。
- 在少量实例上灰度,确认分配器释放的内存确实回到可用池或操作系统。
Big Pineapple 的结果说明了一条朴素但经常被忽略的工程规律:在超大规模常驻缓存里,数据布局就是系统架构。单条记录省下几十字节,乘上全网条目和副本后,就可能比一次大规模扩容更有价值。