优化 1.1.1.1 的 DNS 缓存,省下 100TB 内存

查看原文 HN 讨论

文章摘要

Cloudflare 工程师 Sebastiaan Neuteboom 在 2026 年 8 月 27 日发布的这篇工程博客,讲的是一件听起来枯燥、算起账来惊人的事:他们对 1.1.1.1 背后的 DNS 平台 Big Pineapple 做了五处缓存内存布局优化,把每条缓存条目的内存占用砍掉一半以上,全球机队因此释放了大约 100TB 内存——相当于 130 台 Gen 13 服务器的全部 RAM。更难得的是速度也变快了:插入吞吐提升 43%,查找延迟下降 19%,没有拿性能换空间。

背景是规模:Big Pineapple 支撑 1.1.1.1、Gateway DNS、DNS Firewall、AS112 等服务,任意时刻在机队中保存超过 2500 亿条 DNS 缓存条目。在这个量级下,每条浪费 1 个字节,全机队就要多付出 250GB 内存。启用 EDNS Client Subnet(ECS)的场景里,同一个查询会因客户端网段不同缓存出多个版本,条目数和单条内存占用都会放大,所以 ECS 密集的机房收益最明显。

缓存的 key 是 CacheKey(域名、查询类型、是否已认证、tag),value 是 CacheEntry(时间戳、TTL、命中计数,以及 answer / authority / additional 三段记录列表)。作者按顺序讲了五步优化:

第一步,砍掉 capacity 字段。 Rust 的 Vec<T> 存三个字段:堆指针、长度、容量。但 DNS 响应一旦写进缓存就再也不会被修改,容量字段毫无用处,却要占 8 字节;而且 Vec 为增长预留的多余堆空间也全是浪费。换成不可增长的 Box<[T]>(字符串换成 Box<str>)两个问题一起解决。每条条目有 8 个这样的字段,光结构体本身就省 64 字节,加上堆上的冗余,按 2500 亿条算超过 15TB。

第二步,三段列表合并成一段。 与其为 answer / authority / additional 各存一个列表(每个都是 8 字节指针 + 8 字节长度),不如存一个列表加上两个分段偏移量。DNS 每段的记录数放得进 u16,两个 2 字节偏移替换掉两组 16 字节,每条省 28 字节。作者顺带提醒:这类节省不能简单按字段大小相加,因为 Rust 会插入对齐 padding 并把结构体大小向上取整到对齐倍数——他们把几个 bool 打包成 bitflag 之后,结构体缩小的幅度反而超过了那几个布尔值本身的大小。

第三步,丢掉 owner。 每条 DNS 记录都有一个 owner(记录所属域名),绝大多数情况下它和被查询的域名完全一样,只有涉及 CNAME 时才会不同。DNS 线格式用 RFC 1035 的名称压缩指针来处理重复,但查找热路径上跟随压缩指针代价太高。他们的折中是把 owner 改成 Option<Box<Name>>:相同时存 None,读取时从 cache key 里还原,省掉一次堆分配;不同时才存完整名字。

第四步,给大的枚举变体装箱。 Rust 枚举的大小等于最大变体的大小。RecordData 里最大的是 NAPTR(136 字节),算上 tag 和 padding 整个枚举变成 144 字节——而 A 记录只需要 4 字节、AAAA 只需要 16 字节,这两类却占了超过 80% 的流量,也就是说大多数记录白白浪费 120 多字节。把大变体改成 Box<...>,枚举只存 8 字节指针,A/AAAA 每条省 120 字节。代价是罕见的 NAPTR 反而多付一点,但这笔交易划算。

第五步(也是最有意思的一步),记录数据直接以线格式存原始字节。 装箱本身带来两个新成本:一是分配器开销——Big Pineapple 用 jemalloc,它按固定尺寸的 bin 分组,TXT 请求 32 字节正好落进 32 字节 bin 不浪费,但 MX 请求 40 字节要向上取整到 48,白丢 8 字节;二是内存局部性变差,装箱后每条数据散落在堆各处,读取要跟指针、可能触发新的 cache line 加载。彻底存整条线格式响应也不行(DNSSEC 记录只在客户端设置 DO 标志时才返回,要么缓存两份要么每次过滤,且每次查找都要重新解析整条消息)。最终方案是中间路线:其余字段仍保持结构化,只把记录数据压成一个 Box<[u8]>,每条记录用「2 字节长度前缀 + 原始字节」编码。这样既消除了枚举 padding 和逐条堆分配,数据又连续排列改善了 CPU 缓存局部性。代价是不能随机索引、只能顺序遍历(影响 A/AAAA 的轮转),但每条条目的记录数很少,成本可以忽略。额外收获是构造响应时,A、AAAA、TXT 和所有 DNSSEC 记录可以直接把编码好的字节拷进出站消息,跳过逐字段重新序列化;只有 CNAME、NS、MX、SOA 这类含域名的记录还需要解析以应用名称压缩。仅这一项就让查找延迟降了 5%。写入侧则复用一个跨插入持久存在的 scratchspace 缓冲区,序列化完再一次性 memcpyBox<[u8]>,单这一项把插入吞吐提高了 13%。

结果:基准测试中每条条目净占用从 953 字节降到 420 字节(-56%),每条分配量从 1.1KB 降到 461 字节(-58%),插入吞吐从 62.5 万条/秒升到 89.3 万条/秒(+43%),查找延迟从 828ns 降到 670ns(-19%)。生产环境上,rollout 从 2026 年 5 月 18 日开始、7 月 6 日在所有服务上完成,p99 单实例内存从 9.3GB 降到 5.3GB(-43%),p90 从 6.5GB 降到 3.8GB(-42%),全机队 working set 合计低了约 100TB。Cloudflare 表示接下来会把省出来的内存重新投入扩大缓存容量,从而提高命中率、减少对上游权威服务器的查询量。

HN 评论精华

这条帖子拿到 905 分、100 多条评论。讨论有三条主线:为什么这么明显的优化拖到 2026 年才做、Rust 在这类系统编程场景下的表现,以及一些同类经验分享。