Cloudflare 优化 DNS 缓存省下 100TB 内存
· 4 min read
Cloudflare 通过以下 5 个具体的优化步骤,将 DNS 缓存条目的平均内存占用降低了 56%:
- 使用
Box<[T]>和Box<str>替换Vec<T>和String
- 原理:
Vec和String在 Rust 中包含指针、长度和容量(capacity)三个字段。但 DNS 响应写入缓存后便不再修改,无需预留扩容空间,capacity字段变得多余。 - 效果:将不需要动态扩容的字段替换为固定大小的
Box类型后,每个字段节省了 8 字节的容量信息开销,同时移除了堆内存上的未利用空闲空间。单此一项就为全网节省了超过 15 TB 的内存。
- 合并多列表并使用偏移量索引(Fewer lists, fewer pointers)
- 原理:原先将答案(Answer)、授权(Authority)和附加(Additional)三部分分别存储在独立列表中(每个列表占用指针和长度开销)。优化后合并为一个单一列表,并使用
u16(2 字节)类型的偏移量(Offset)来标记各部分的起始位置。 - 效果:减少了两个列表指针和长度字段(各 8+8 字节),替换为 2 字节的偏移量,直接省去 28 字节/条。同时减少了结构体对齐所需的填充(Padding)。
- 按需隐式恢复域名所有者(Dropping the owner)
- 原理:绝大多数 DNS 记录的所属域名(Owner)与被查询域名完全一致。因此在绝大多数情况下直接抹去记录中的 owner 字段,在读取生成响应时,直接从 Cache Key 中隐式复用查询域名;仅在存在 CNAME 等 owner 不一致的情况下才在堆上显式存储完整域名。
- 效果:绝大多数缓存记录无需再在堆上为 owner 字段分配额外内存。
- 避免 Enum 类型的大 Variant 空间浪费(Boxing the variants)
- 原理:Rust 中的
enum占用空间取决于其最大的变体(Variant)。此前RecordData枚举因为包含最大的NAPTR类型(136 字节),导致整条枚举加上 Tag 和对齐占用高达 144 字节。而占流量 80% 以上的A和AAAA记录(分别仅需 4 字节和 16 字节)因此浪费了超过 120 字节的 Padding。 - 效果:将
NAPTR、TXT等大体量或不常见的变体使用Box包装放至堆内存,使enum本身尺寸大幅缩小,大幅减少了常用记录(A/AAAA)的内存浪费。
- 记录数据改用 Wire Format(字节流)连续存储
- 原理:放弃将记录解析为一个个单独 boxed enum 的方式,改为将解析后的记录数据直接序列化为 Raw Bytes(
Box<[u8]>),前面带上 2 字节长度前缀,打包连续存储在单块内存中。 - 效果:移除了所有的 enum 额外开销和逐个变体 Box 带来的堆内存分配开销/碎片,并提高了 CPU 缓存局部性(Memory Locality),使插入吞吐量提升了 43%,查询延迟降低了 19%。
优化项排序:
最能节省内存的操作:由上至下
- 避免 Enum 类型的大 Variant 空间浪费(Boxing the variants / Enum Sizing)
- 使用
Box<[T]>与Box<str>替换Vec<T>与String(The cost of capacity) - 按需隐式恢复域名所有者(Dropping the owner)
- 合并多列表并使用偏移量索引(Fewer lists, fewer pointers)
- 记录数据改用 Wire Format 字节流连续存储(Storing records in wire format)
Conclusion
我想起了一个前辈对我说过的话:如果你都用 Long 来代替 Integer 的话,你的程序运行起来内存占用就是别人的两倍。
在当时我认为,一个程序 一次设计,永久使用 是最棒的写法,现在想想,如果这个程序被全世界所使用,过度设计带来的内存浪费也许也是一种失败的写法。
ref: