教程·阅读约 2 分钟·
从 2500 亿条缓存里抠出 100TB 内存:Cloudflare 的五个内存优化手法,每个都能直接抄

从 2500 亿条缓存里抠出 100TB 内存:Cloudflare 的五个内存优化手法,每个都能直接抄

Cloudflare 对 1.1.1.1 的 DNS 缓存做了 5 个存储层优化,每条目占用从 953 字节降到 420 字节,整个机群省下约 100TB 内存,缓存反而更快了。本文逐条拆解这些可复用的 Rust 内存优化技术。

原文来源:How we saved 100 terabytes of memory by optimizing 1.1.1.1's DNS cache — Cloudflare 对 DNS 缓存存储层做了 5 个优化,每条目内存占用砍半、整个机群释放约 100TB 内存,而且缓存更快了。

先看结论。Cloudflare 的 Big Pineapple 平台(背后支撑 1.1.1.1、Gateway DNS、DNS Firewall 等服务)在任何时刻都存着超过 2500 亿条 DNS 缓存条目。这个量级下,每个条目浪费 1 个字节,整个机群就要多花 250GB 内存。

他们对缓存条目的内存布局做了 5 次连续改动,把每条目占用砍掉了 50% 以上,整个机群释放了大约 100TB 内存——相当于 130 台 Gen 13 服务器的内存总量。而且缓存变得更快了:插入吞吐提升 43%,查询延迟下降 19%,并没有拿速度换空间。

这篇文章的价值在于:这 5 个手法全是通用的存储层优化技术,不依赖 Cloudflare 的特殊场景,值得逐个拆开看。

先看缓存长什么样

每个缓存条目是一对 key-value。key 标识查询内容,value 存 DNS 响应本身——answer、authority、additional 三个记录区段,外加创建时间、命中计数、TTL 等元数据。一个典型的 CacheEntry 里有 8 个 VecString 字段。

优化前的基准:每条目净占用 953 字节,分配 1.1KB。测试数据按生产流量分布生成:56% A 记录、25% AAAA、19% TXT,每条目 1-4 条记录。

—— 广告 ——

手法一:Vec<T> 换成 Box<[T]>,砍掉容量字段

Vec<T> 存三个字段:指向堆内存的指针、当前长度、总容量。push 元素时检查长度是否超过容量、必要时重新分配。但 DNS 响应一旦进缓存就再也不会被修改了——容量字段毫无用处,却每个 Vec 白占 8 字节;预分配的多余堆空间也浪费了。

Box<[T]> 创建后不可增长,不需要容量字段,也不预留未来元素的空间。String 同理,Box<str> 去掉容量字段。

每个条目有 8 个 Vec/String 字段,每个换掉省 8 字节,共 64 字节/条目,还消除了 Vec 为增长预留的堆空间。2500 亿条目算下来,这一步就省了超过 15TB。

手法二:少几个列表,就少几个指针

answer、authority、additional 三个区段不用分开存,存成单个列表 + 各段起始偏移就行。每段记录数用 u16 就装得下,所以偏移用 2 字节,而每个独立的 Box<[T]> 需要 8 字节指针 + 8 字节长度。去掉两个列表,换成两个 2 字节偏移,每条目省 28 字节。

这里还有个 Rust 特有的连锁收益:结构体为了对齐会插 padding,并向上取整到对齐倍数。他们顺手把几个布尔字段打包成一个 bitflag,减少了周边 padding,结构体缩小的幅度比布尔字段本身的体积还大。

手法三:丢掉 owner

每条 DNS 记录都有 owner——记录所属的域名。大多数情况下,owner 和查询的域名完全相同(查 example.com 的 A 记录,返回的两条 A 记录 owner 都是 example.com)。只有遇到 CNAME 之类的情况,owner 才会和查询域名不同。

于是把字段改成 Option<Box<Name>>:owner 与查询域名相同时存 None,构建响应时从缓存 key 恢复查询域名,省掉一次堆分配;确实不同时才存完整名字。实际流量里大多数记录的 owner 都与查询域名一致,所以绝大多数记录连 owner 的堆分配都省了。

手法四:enum 变体装箱,别让罕见类型撑大整个枚举

Rust 的 enum 是 sum type,大小永远等于最大变体的大小。如果把每种 DNS 记录类型都做成 enum 变体,NAPTR 有 136 字节(三个变长文本字段、一个域名、两个整数),整个 RecordData enum 含 tag 和 padding 变成 144 字节。

但 A 记录只需要 4 字节,AAAA 只需要 16 字节——而 A 和 AAAA 占了 80% 以上的流量。也就是说,绝大多数记录有 120+ 字节在 padding 上浪费着。

解法是把大的变体装箱:A、AAAA 这种又小又常见的变体内联存储,TXT、NAPTR、SVCB 等大变体移到堆上,enum 只存 8 字节指针。A/AAAA 每条省 120 字节;TXT 这类中等变体也受益,堆分配按实际数据大小而不是按 144 字节取。NAPTR 本身反而多花了一个指针和分配开销——但它罕见,这笔交易划算。

装箱当然有代价:每个装箱变体是一次独立堆分配,分配器按 size class 舍入会浪费(jemalloc 下 40 字节的 MX 记录会舍入到 48 字节);而且数据分散在堆上,破坏内存局部性,读的时候要追指针、可能触发新的 cache line 加载。这直接引出了手法五。

手法五:直接用 wire format 存原始字节

把记录以 2 字节长度前缀 + 原始字节的形式,拼进单个 Box<[u8]> 缓冲区。这消灭了手法四引入的逐变体枚举开销和逐条装箱分配,数据连续排列,CPU 缓存局部性大幅改善。

代价是记录不能再随机索引,只能顺序遍历——但每条目记录数本来就少,影响可以忽略。收益是构建响应时,A、AAAA、TXT 和所有 DNSSEC 记录类型都能直接把字节拷进输出消息,省掉了逐字段序列化回 wire format 的工作;只有包含域名的记录(CNAME、NS、MX、SOA)需要解析做 DNS 名称压缩。由于支持直接拷贝的记录类型占流量绝大多数,查找路径上的工作量大幅减少。

缓冲区也做了复用:用一个跨插入操作存活的 scratchspace 缓冲,前一次写入已经把它撑大,后面很少需要重新分配。基准测试里,单这一步就提升了 13% 的插入吞吐。

结果

指标优化前优化后变化
每条目净占用953 字节420 字节-56%
每条目分配1.1 KB461 字节-58%
缓存插入吞吐625,000 条/秒893,000 条/秒+43%
缓存查询延迟828 ns670 ns-19%

生产环境实测:p99 单实例内存从 9.3GB 降到 5.3GB(-43%),p90 从 6.5GB 降到 3.8GB(-42%)。2026 年 5 月 18 日开始分批上线,7 月 6 日全部服务完成,机群工作集内存总共低了约 100TB。

这五招对你有什么用

这几个手法不挑场景,任何 Rust 项目都能直接对照检查:你的 Vec 存进缓存后就再也不改了,换成 Box<[T]>;你的 enum 里有个罕见的大变体在撑大所有实例,考虑装箱;你的数据结构里有冗余字段,想想能不能在读取时推导出来。真正值得学习的是它们的思考顺序——先量化每种类型的实际分布(80% 的流量是 4 字节的 A 记录),再针对大头做布局优化,最后用基准和生产数据验证每一步,确保没有拿性能换内存。Cloudflare 释放的内存计划重新投进缓存容量,提高命中率、减少上游查询量——省下来的每一字节都在继续产生收益。

分享到
微博Twitter

© 2026 四月

原文链接:https://www.aprilzz.com/tutorials/cloudflare-dns-cache-memory-optimization