优化1.1.1.1 DNS缓存以节省100TB内存
摘要
Cloudflare优化了其1.1.1.1 DNS缓存,将内存使用量减少超过50%,节省了100TB内存,并通过优化的Rust数据结构提升了性能。
暂无内容
查看缓存全文
缓存时间: 2026/08/27 18:21
# 我们如何通过优化1.1.1.1的DNS缓存节省100太字节内存
来源:https://blog.cloudflare.com/dns-cache-memory-optimization-1111/
Big Pineapple(https://blog.cloudflare.com/big-pineapple-intro/)是1\.1\.1\.1(https://www.cloudflare.com/learning/dns/what-is-1.1.1.1/)、Gateway DNS(https://www.cloudflare.com/products/gateway/)、DNS Firewall(https://www.cloudflare.com/dns/dns-firewall/)、AS112(https://blog.cloudflare.com/the-as112-project/)以及多项其他Cloudflare DNS服务的底层平台,它在任何时刻都存储着超过2500亿条DNS缓存条目。在这种规模下,即使每条缓存浪费一个字节,整个集群也会消耗超过250吉字节内存。通过五次针对缓存条目内存存储方式的迭代优化,我们将每条缓存的内存占用降低了50%以上。这些优化在整个集群中释放了大约100太字节的内存,相当于130台Gen 13服务器(https://blog.cloudflare.com/gen13-config/)的RAM总量。同时缓存性能也得到提升:插入吞吐量提高43%,查询延迟降低19%,这得益于更少的内存分配操作和更优的内存局部性——我们并未用速度换取空间。
## 我们缓存什么
Big Pineapple在冷启动时缓存为空。随着DNS查询到达,缓存逐渐填充直至达到最大条目数,此时系统会逐出较旧或较不常用的条目以腾出空间。具体缓存大小因数据中心而异。当使用EDNS客户端子网(ECS)(https://en.wikipedia.org/wiki/EDNS_Client_Subnet)时,权威服务器会根据客户端网络返回不同结果,因此我们需要为相同查询缓存多个版本。这不仅增加了条目数量,还提升了每条内存占用,使本文所述优化在ECS密集场景下效果尤为显著。
缓存中的每个条目都是键值对。键标识被查询的内容:
```rust
pub struct CacheKey {
qname: Name,
qtype: Rtype,
authenticated: bool,
tag: Vec,
}
```
值存储DNS响应本身:应答部分、授权部分和附加部分,以及创建时间、命中计数器和生存时间(TTL)等元数据。
```rust
pub struct CacheEntry {
timestamp: UnixTimeStamp,
pub inception: Instant,
pub ttl: Ttl,
pub hits: u32,
pub answers: Vec,
pub authority: Vec,
pub additional: Vec,
pub errors: Vec,
...
}
```
这两个结构体都存在改进空间。多个字段使用了在条目存储后并不需要的、带有额外开销的类型。
## 内存使用基准测试
为衡量每次变更的影响,我们使用模拟生产流量分布的随机生成条目进行基准测试:56% `A`记录、25% `AAAA`记录、19% `TXT`记录。每条缓存包含1到4条记录。`TXT`记录在测试中代表所有非`A`/`AAAA`记录类型,其大小在64到224字节间随机生成,接近可变长度记录类型的平均响应大小。
我们使用自定义内存分配器(https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html)(封装Rust的`System`分配器(https://doc.rust-lang.org/std/alloc/struct.System.html))跟踪内存使用,记录每条缓存的分配次数和大小。除内存外,我们还测量整个缓存流程的插入吞吐量和查询延迟,确保内存节省不会以性能为代价。这些输入近似生产环境而非完全复现。进程内存还受流量混合、缓存占用率、分配器状态及缓存外内存使用影响。因此我们在发布过程中测量了生产实例的常驻内存。
## 容量字段的开销
`Vec`存储三个字段:指向堆分配数据的指针、当前长度和总容量。插入项目时,`Vec`会检查长度是否超过容量,并在需要时重新分配。若有空间则直接追加项目并递增长度。

但DNS响应存入缓存后便不再修改。容量字段失去作用,但仍为每个`Vec`占用8字节。堆空间的过度分配也被浪费——一个容量为8但仅存储5个项目的`Vec`会在堆上留下3个未使用槽位。

使用`Box<[T]>`可同时解决这两个问题。它在创建后无法增长,因此无需容量字段或为未来元素预留空间。同样适用于`String`——`Box`移除了它的容量字段。每个缓存条目包含8个`Vec`和`String`字段。用`Box<[T]>`和`Box`替换它们,每字段节省8字节,每条目节省64字节。这还消除了`Vec`为增长预留的额外堆内存。结合超过2500亿条缓存条目,总节省超过15太字节。
## 减少列表与指针
我们无需将应答、授权和附加部分存储在独立列表中,可存储单个列表并用偏移量标识各部分起始位置。由于DNS每部分记录数适配`u16`,可用`u16`(2字节)存储偏移量,而每个独立的`Box<[T]>`需要8字节指针和8字节长度。

此操作移除了两个列表(各含8字节指针和8字节长度),替换为两个2字节偏移量,每条目节省28字节。节省的字节数并不总是等于各字段移除的字节数。Rust会插入填充以满足对齐要求(https://doc.rust-lang.org/nomicon/repr-rust.html),并将结构体大小向上取整为对齐值的倍数。移除小字段可能消除额外填充。例如,我们将多个布尔字段打包为单个位标志(https://docs.rs/bitflags/latest/bitflags/),减少了周围填充,使结构体缩小幅度超过各布尔字段尺寸之和。
## 移除所有者字段
每条DNS记录都有所有者(即记录所属域名)。通常所有者与被查询域名相同。例如,查询`example.com A`返回两条具有相同所有者的记录:
```
$ dig example.com A
;; ANSWER SECTION:
example.com. 300 IN A 198.51.100.1
example.com. 300 IN A 198.51.100.2
```
但涉及`CNAME`时,记录所有者可能与查询域名不同:
```
$ dig example.com A
;; ANSWER SECTION:
example.com. 300 IN CNAME cdn.example.com.
cdn.example.com. 300 IN A 198.51.100.1
cdn.example.com. 300 IN A 198.51.100.2
```
DNS传输格式使用RFC 1035(https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.4)定义的名称压缩处理重复所有者。后续出现的所有者存储2字节指针指向首次出现的位置。如`www.example.com`可编码为`www`加指向`example.com`已出现位置的指针。这在传输层效果良好,但缓存中我们存储完整所有者名称。缓存查询时遍历压缩指针在热路径上代价高昂,因此我们用内存换取速度。
然而大多数记录的所有者与查询域名相同。对于这些记录,我们可完全移除所有者字段,在读取时推断。当所有者不同时(如`CNAME`后的`A`记录),我们存储完整名称。
```rust
pub struct Record {
owner: Option<Name>,
class: Class,
ttl: Ttl,
rtype: Rtype,
data: RecordData,
}
```
当`owner`为`None`时,响应构建时从缓存键恢复查询域名,避免堆分配。这意味着记录不再是自包含的,但缓存键在每次查询时都可用。当所有者不同时,`Some`存储指向堆上完整名称的指针。

实践中,大多数缓存记录的所有者与查询域名相同,因此大部分记录无需为所有者字段进行堆分配。
## 枚举大小优化
Rust枚举是和类型(https://en.wikipedia.org/wiki/Algebraic_data_type):每个变体可携带不同数据,但枚举始终等于最大变体的大小。
```rust
pub enum Option<T> {
Some(T),
None,
}
```
`Option`要么是`Some`(持有值),要么是`None`(不持有值)。两种变体占用相同内存。枚举存储指示活动变体的标签,后接足以容纳最大变体数据的空间。当变体为`None`时,该空间未被使用。
对于记录数据,自然的做法是将每种DNS记录类型存储为枚举变体:
```rust
pub enum RecordData {
A(Ipv4Addr),
Aaaa(Ipv6Addr),
Txt(Txt),
Naptr(Naptr),
Svcb(Svcb),
// ...
}
```
但枚举始终等于最大变体的大小。本例中是136字节的`NAPTR`——它存储三个可变长度文本字段、一个域名和两个整数。因此包含变体标签和填充的完整枚举为144字节。

`A`记录仅需4字节,`AAAA`记录需16字节。`A`和`AAAA`占我们流量的80%以上,因此大多数记录因填充浪费超过120字节。单条缓存可存储多条记录,浪费迅速累积。
### 为变体装箱
为解决此问题,我们可为枚举的较大变体装箱,将其移至独立堆分配。枚举存储指向堆的8字节指针,数据在堆上仅占用实际所需大小。
```rust
pub enum RecordData {
// 小型常见变体内联存储
A(Ipv4Addr),
Aaaa(Ipv6Addr),
// 大型变体存储在堆上
Txt(Box<Txt>),
Naptr(Box<Naptr>),
Svcb(Box<Svcb>),
// ...
}
```
对于`A`和`AAAA`记录,每条节省120字节。较小变体类型如`TXT`和`CNAME`也受益:它们仍占24字节枚举空间,但堆分配按实际数据大小而非填充至144字节。最大变体`NAPTR`实际成本略增,需支付堆指针和分配开销。但`NAPTR`记录在实践中罕见,此权衡值得。

### 装箱的成本
装箱有两个代价:首先是分配器开销。每个装箱变体成为独立堆分配,分配器向上取整至最近大小类别。Big Pineapple使用jemalloc(https://jemalloc.net/),一种专为多线程、分配密集型工作负载设计的分配器。jemalloc将相似大小的分配分组到固定大小箱中。`TXT`记录请求32字节恰好适配32字节箱,无浪费;但`MX`记录请求40字节,向上取整至48字节,浪费8字节。其次是内存局部性差。未装箱时,缓存条目的记录枚举值位于单个连续分配中。装箱后,每个装箱变体的数据位于独立堆区域。读取时需跟随指针,若指针指向远离条目其余部分的位置,CPU需获取新缓存行。数百万条缓存条目下,装箱数据散布在堆中而非紧密排列。

单看任一成本都不致命,但如下一节所示,消除两者可带来内存使用和查询延迟的可测量改善。
## 以传输格式存储记录
显而易见的下一步是以传输格式存储完整DNS响应,每次查询时仅修补客户端相关字段(如消息ID)。但这存在缺点:DNSSEC记录仅在客户端设置DO(DNSSEC OK)(https://www.cloudflare.com/learning/dns/dnssec/how-dnssec-works/)标志时包含。存储完整传输格式消息意味着要么缓存两个变体(带DNSSEC和不带DNSSEC),要么从已构建的消息中过滤它们。此外每次查询解析完整消息也有成本,而我们刚描述的枚举方法通过存储已解析记录避免了此开销。
作为折中,我们仅将记录数据存储为原始字节,缓存条目其余部分保持结构化字段。我们不存储已解析枚举变体列表,而是将记录存储为单个`Box<[u8]>`,其中每条记录编码为2字节长度前缀加原始字节。

这消除了前次优化的每变体枚举开销和装箱堆分配。数据变为连续紧凑存储,改善了CPU缓存局部性。代价是记录无法随机索引,必须顺序遍历缓冲区。这增加了如轮询(https://www.cloudflare.com/learning/dns/glossary/round-robin-dns/)旋转`A`/`AAAA`记录等功能的复杂性,但由于每条记录数量少,开销可忽略。
从缓存记录构建DNS响应时,大多数记录类型可直接从缓冲区复制到传出消息。此前每条已解析记录需逐字段序列化回DNS传输格式。新布局通过直接复制编码字节,为`A`、`AAAA`、`TXT`和所有DNSSEC记录类型跳过了此操作。仅包含域名的记录(如`CNAME`、`NS`、`MX`和`SOA`)仍需解析以应用DNS名称压缩。由于支持直接复制的记录占我们流量的绝大多数,此变更减少了查询路径的工作量。结合改进的内存局部性,我们的基准测试显示缓存查询延迟降低5%。
构建记录数据缓冲区时,我们写入可跨缓存插入复用的临时缓冲区。由于先前的写入已使其增长,缓冲区很少需要重新分配。记录大小不一,因此直到序列化完成才能确定精确缓冲区大小。记录进入临时缓冲区后,我们分配`Box<[u8]>`并通过`memcpy`将数据复制其中。这用单次记录数据分配替换了每条装箱记录的单独分配,也避免了收缩`Vec`时的浪费(分配器可能无法回收原始分配的未使用尾部)。在我们的基准测试中,仅此变更就使缓存插入吞吐量提高13%。
## 优化成果
生产环境测量展示了每条节省如何转化为整个进程的常驻内存。下图显示Big Pineapple实例的p90、p98和p99内存使用情况。第一条虚线标记2026年5月18日开始发布,第二条标记7月6日完成所有服务发布。每次发布引入一种或多种上述优化,因此内存使用呈阶梯式下降。每次发布时,重启的实例从空缓存开始,随缓存填充消耗更多内存。因此稳定平台期比初始低谷更能反映稳态内存使用。

各百分位实例内存使用均下降。在p99,内存从9.3 GB降至5.3 GB,常驻内存减少43%。在p90,内存从6.5 GB降至3.8 GB,减少42%。缓存更满的实例获得最大绝对节省。我们的基准测试显示,这五项优化使每条内存占用从953字节降至420字节,减少56%。每条分配从1.1 KB降至461字节,减少58%。
发布稳定后,整个集群的总工作内存降低了约100太字节。性能也得到提升:缓存插入吞吐量提高43%,查询延迟降低19%。
| 指标 | 优化前 | 优化后 | 变化 |
|------|--------|--------|------|
| 每条净占用 | 953字节 | 420字节 | -56% |
| 每条分配 | 1.1 KB | 461字节 | -58% |
| 缓存插入吞吐量 | 625,000条目/秒 | 893,000条目/秒 | +43% |
| 缓存查询延迟 | 828 ns | 670 ns | -19% |
相似文章
@southpolesteve: Zod 4.5 带来了内存占用的大幅下降。如果你正在使用 @Cloudflare Workers,你应该更新!
Zod 4.5 实现了方法记忆化,以大幅减少内存占用,相比之前的版本,堆内存保留量减少了高达9.8倍。
@VincentLogic: 一个程序员突发奇想: DNS 解析器会把域名记几天—— 那能不能拿来存文件? ↓ 他找到了全网 390 万个开放 DNS 解析器 把文件切碎 撒遍整个互联网 没有硬盘 没有数据库 没有云 文件活在无数台服务器的缓存里 而那些服务器根本不知…
一个程序员利用全网390万个开放DNS解析器的缓存来存储文件碎片,创建了一个名为dnsfs的荒诞开源项目,文件随着缓存过期而消失。
@Honcia13: 很多人不知道,Cloudflare 官方出了一个免费“梯子”,装上就能用! 1.1.1.1 + WARP,本质上是 Cloudflare 用自己的全球网络帮你加密和加速所有流量。 核心优势: 所有流量全程加密,运营商看不到你在访问什么 基…
Cloudflare 推出的免费 1.1.1.1 + WARP 服务,通过加密和优化网络连接提升速度和隐私保护,支持全平台,无需注册即可使用。
@ayushagarwal027: Cloudflare 花了 6 周追踪 hyper Rust HTTP 库中的一个 bug。修复只需 4 行代码。症状:图片响应……
Cloudflare 工程师花了六周时间调试 hyper Rust HTTP 库中的一个竞态条件,该问题导致大图片响应中出现静默数据截断。根本原因是在 flush 期间,一个 `let _ =` 丢弃了 `Poll::Pending` 信号;修复方式是一处四行代码的修改,现已合并到上游。
@_avichawla: Cloudflare如何在不离开Postgres的情况下将查询时间缩短35倍:他们的Postgres表达到数十亿行,而每次时间范围查询开始变慢。
Cloudflare使用TimescaleDB(Tiger Cloud)在数十亿行的Postgres表上将查询性能提升了35倍,并借助Claude Code和Tiger CLI构建了一个实时地震仪表板。