Cloudflare 1.1.1.1 如何通过重构 DNS Cache 内存布局节省 100 TB 内存 by ChatGPT

从 ​Vec<Record>、Rust enum、heap allocation,一路优化到连续 DNS wire-format buffer:这是一个非常典型的“大规模系统中,数据表示本身就是性能”的案例。

2026 年 8 月,Cloudflare 公开了其 DNS 平台 Big Pineapple 的一次缓存优化。

Big Pineapple 是 Cloudflare ​1.1.1.1​、Gateway DNS、DNS Firewall 等 DNS 服务背后的基础平台。在任意时刻,它的整个 fleet 中会保存超过 2500 亿条 DNS cache entry。

这意味着一个非常夸张的放大效应:

250,000,000,000 entries × 1 byte
≈ 250 GB

每条记录哪怕浪费 1 byte,全网就是 250 GB。

Cloudflare 最终通过连续 5 轮内存表示优化,将 benchmark 中单条 cache entry 的净内存占用:

953 bytes
   ↓
420 bytes

下降约 56% 。

与此同时:

指标{: style=“text-align: right;"}优化前{: style=“text-align: right;"}优化后{: style=“text-align: right;"}变化
Per-entry footprint{: style=“text-align: right;"}953 B{: style=“text-align: right;"}420 B{: style=“text-align: right;”}-56%
Per-entry allocation{: style=“text-align: right;"}1.1 KB{: style=“text-align: right;"}461 B{: style=“text-align: right;”}-58%
Insert throughput{: style=“text-align: right;"}625K/s{: style=“text-align: right;"}893K/s{: style=“text-align: right;”}+43%
Lookup latency{: style=“text-align: right;"}828 ns{: style=“text-align: right;"}670 ns{: style=“text-align: right;”}-19%

实际生产环境中,整个 fleet 的 working-set memory 最终下降了约 100 TB。

这次优化最值得研究的地方,不是某一个 Rust 技巧,而是 Cloudflare 最终得到的几个设计结论:

Mutable Object
      ↓
Immutable Object
      ↓
Compact Object
      ↓
Flat Object
      ↓
Byte-oriented Representation

换句话说:

当一个对象被写入缓存以后几乎不会再发生修改时,最适合它的内存表示,往往不再是业务代码中最好操作的对象模型。


1. Big Pineapple 的 Cache 在什么位置?#

首先需要理解这不是一个普通的:

HashMap<String, DNSResponse>

Big Pineapple 是完整的 recursive DNS resolver。

一个查询大致经过:

示意图

Big Pineapple 的缓存使用 ARC 一类 cache replacement 结构,而不是简单 KV;同一数据中心中的节点还会通过 consistent hashing 协同,提高整体 cache hit ratio。

对于 recursive DNS 来说,这非常重要。

Cache hit 可能在亚毫秒范围完成,而真正进行 recursive lookup 则可能涉及:

root
 ↓
TLD
 ↓
authoritative NS
 ↓
possibly CNAME
 ↓
another authoritative NS
 ↓
最终答案

因此:

更多 cache capacity
        ↓
更高 cache hit ratio
        ↓
更少 upstream DNS query
        ↓
更低 latency
        ↓
更低网络与 CPU 成本

所以对于 DNS resolver:

Memory efficiency 本身就是 performance optimization。


2. 原来的 CacheEntry 长什么样?#

Cloudflare 给出的简化结构大致是:

pub struct CacheKey {
    qname: Name,
    qtype: Rtype,
    authenticated: bool,
    tag: Vec<u8>,
}

pub struct CacheEntry {
    timestamp: UnixTimeStamp,
    inception: Instant,
    ttl: Ttl,
    hits: u32,

    answers: Vec<Record>,
    authority: Vec<Record>,
    additional: Vec<Record>,

    errors: Vec<ExtendedError>,
    ...
}

从业务建模角度,这非常自然。

DNS Response 原本就包含:

Answer
Authority
Additional

三个 section。

因此程序员很容易写成:

CacheEntry
├── Vec<Record> answers
├── Vec<Record> authority
├── Vec<Record> additional
└── Vec<ExtendedError> errors

问题在于:

“符合业务对象模型”并不等于“符合缓存存储模型”。

它最终形成的是一个 object graph:

示意图

这里会同时出现四种浪费:

  1. container metadata;
  2. unused capacity;
  3. struct alignment / padding;
  4. 多次 heap allocation 带来的 allocator overhead 和 pointer chasing。

Cloudflare 的五轮优化,本质就是逐层消灭这四类成本。


3. 第一刀:​Vec<T>​ → ​Box<[T]>#

这是最直接的一步。

一个 ​Vec<T> 必须支持:

vec.push(...)

因此它不能只知道“数据在哪里”和“现在有几个元素”。

还必须知道:

capacity

典型 64-bit 环境中可以把它概念化为:

Vec<T>

┌─────────────────┐
│ ptr        8 B   │
├─────────────────┤
│ len        8 B   │
├─────────────────┤
│ capacity   8 B   │
└─────────────────┘

        24 B

而缓存中的 DNS response 有一个关键性质:

写入完成后不会再 append record。

也就是说:

Vec 的动态增长能力

在构建阶段:有价值
进入 cache 后:完全没有价值

所以 Cloudflare 将它冻结成:

Box<[Record]>

概念上:

示意图

Cloudflare 的 cache entry 中一共有多个 ​Vec​ / ​String 类型字段。

将 8 个此类字段改为对应的:

Box<[T]>
Box<str>

仅 container metadata 就能减少:

8 fields × 8 bytes capacity
= 64 bytes / cache entry

2500 亿条 entry 放大以后,仅这一类优化就达到 15 TB 以上。

但这里更重要的是一个设计原则:

Build Mutable, Store Immutable#

业务对象的生命周期其实可以拆成两个阶段:

Construction Phase             Serving Phase
──────────────────             ─────────────
需要 push                       只读
需要 resize                     immutable
需要 temporary buffer           hot lookup
需要方便修改                    高密度存储

没有必要要求两个阶段使用同一种 representation。

完全可以:

Vec<Record>
    ↓
build
    ↓
Box<[Record]>
    ↓
cache

这也是数据库、搜索引擎、编译器和高性能缓存里非常常见的设计。


4. 第二刀:三个 List 其实可以只有一个#

现在还有:

answers: Box<[Record]>
authority: Box<[Record]>
additional: Box<[Record]>

问题是:

为什么一定需要三块 heap allocation?

DNS 的这三个 section 本质上只是:

一串 Record 的逻辑分区

于是可以改成:

records

0                                  N
│                                  │
▼                                  ▼

[ Answer ][ Answer ][ Authority ][ Additional ][ Additional ]
│                  │             │
0                  authority     additional
                   offset        offset

于是数据结构从:

pointer + len
pointer + len
pointer + len

变成:

one pointer + one len
+
authority_offset
+
additional_offset

D2 表示如下:

示意图

因为一个 DNS response 中 record 数量足够小,section offset 可以使用 ​u16。

Cloudflare 算下来,这一步每个 entry 又减少了约 28 bytes。

这里还出现了一个 C/C++/Rust 底层优化中特别容易忽略的问题:

Padding#

假设:

struct Foo {
    a: u64,
    b: bool,
    c: u64,
}

它未必是:

8 + 1 + 8 = 17 bytes

CPU alignment 可能让它实际变成:

24 bytes

Rust 默认布局同样需要满足字段 alignment,因此一个字段被删除以后,减少的不一定只有这个字段本身的大小,还可能顺便消灭 padding。

Cloudflare 也顺手把多个 bool 收进 bitflags。

例如:

Before

authenticated: bool
dnssec: bool
stale: bool
...

After

flags: u8

bit 0 authenticated
bit 1 dnssec
bit 2 stale
...

所以:

优化 struct 时,应该观察 ​size_of::<T>(),而不是简单把字段尺寸相加。


5. 第三刀:不要重复保存 Record Owner#

DNS record 一般长这样:

example.com.   300   IN   A   198.51.100.1

其中:

example.com.

就是 record owner。

如果查询本身就是:

QNAME = example.com
QTYPE = A

那么很多 response 实际是:

CacheKey:
    example.com

Record #1:
    owner = example.com

Record #2:
    owner = example.com

Record #3:
    owner = example.com

显然出现了重复。

而更复杂的 CNAME 情况则可能是:

example.com
    ↓ CNAME
cdn.example.com
    ↓ A
198.51.100.1

所以不能永远删除 owner。

Cloudflare 使用的思路是:

struct Record {
    owner: Option<Box<Name>>,
    ...
}

含义发生变化:

None
    =
owner == CacheKey.qname

Some(name)
    =
owner != CacheKey.qname

于是:

示意图

这是一种很漂亮的:

Context-dependent compression#

以前:

Record 是 self-contained

现在:

Record + CacheKey
才能完整还原数据

单独看,这是更差的 abstraction。

但是从系统角度却更好。

因为 lookup 时:

CacheKey 本来就一定存在

因此没有必要让 Record 为了“理论上的独立性”,重复携带 QNAME。

DNS wire format 本身其实也做类似的事情。

RFC 1035 定义了 DNS name compression:重复 domain name 可以通过 pointer 引用 DNS message 中已经出现的名字,而不是重复发送整个名称。

Cloudflare 没有直接在 cache 中使用 DNS compression pointer,因为 hot lookup path 上追踪这些 pointer 会增加处理成本;他们采用了一个更加适合 cache 的折中:

最常见情况
owner == qname
    ↓
完全不存

特殊情况
owner != qname
    ↓
完整保存

也就是:

Optimize the common case, encode the exception.


6. 第四刀:Rust Enum 的“大 Variant 税”#

到这里,还有一个非常大的问题。

DNS 有很多 record type:

A
AAAA
CNAME
TXT
MX
NS
SOA
NAPTR
SVCB
DNSKEY
RRSIG
...

最自然的 Rust 写法当然是:

enum RecordData {
    A(Ipv4Addr),
    Aaaa(Ipv6Addr),
    Txt(Txt),
    Naptr(Naptr),
    Svcb(Svcb),
    ...
}

但是 sum type 有一个关键特性:

enum size
≈
max(all variant size)
+
discriminant
+
alignment

Cloudflare 当时最大的 variant 是:

NAPTR ≈ 136 bytes

最终整个 ​RecordData enum 达到约:

144 bytes

于是一个 IPv4 地址虽然只有:

4 bytes

放进去后仍然要占:

~144 bytes

可以把它想象成:

示意图

这里最恐怖的是:

最稀有的数据类型,决定了最常见数据类型的内存大小。

而 Cloudflare 的 A + AAAA 占流量绝大多数。


7. 第一个解决方案:Box 大 Variant#

可以把:

Naptr(Naptr)

改成:

Naptr(Box<Naptr>)

于是 enum 里面不再放:

136 bytes NAPTR

只需要放:

8-byte pointer

例如:

enum RecordData {
    // hot + small
    A(Ipv4Addr),
    Aaaa(Ipv6Addr),

    // cold + large
    Txt(Box<Txt>),
    Naptr(Box<Naptr>),
    Svcb(Box<Svcb>),
}

于是 enum 本身可以从:

144 bytes

缩小到大约:

24 bytes

对于 A / AAAA,这一下就可以减少大约 120 bytes / record 级别的浪费。

这其实是一种:

Hot / Cold Representation Splitting#

Common + Small
    ↓
inline

Rare + Large
    ↓
heap

这个模式在很多高性能系统里都会出现:

small string optimization
small vector optimization
inline metadata
cold side table
tagged pointer
out-of-line large payload

但是 Cloudflare 很快发现:

Boxing 并不是终点。


8. 为什么 Box 又产生了新的问题?#

现在结构变成:

CacheEntry
   │
   └── Record[]
          │
          ├── A inline
          ├── AAAA inline
          │
          ├── ptr ───────→ TXT
          │
          ├── ptr ─────────────→ MX
          │
          └── ptr ─────────────────────→ NAPTR

问题一:

allocator size class#

Big Pineapple 使用 jemalloc。

allocator 通常不会精确按照:

request 40 bytes

就真的分配 40 bytes。

它可能对应到某个 size class:

40 bytes
   ↓
48-byte allocation class

多出的:

8 bytes

就是 internal fragmentation。

当:

allocation 数量 × 数十亿

时,它就不再是小问题。Cloudflare 在文章中专门举了 ​MX 等 record 被 allocator size class 向上取整的例子。


问题二:

Pointer Chasing#

考虑:

CPU 正在读取 CacheEntry

Cache line #1
┌─────────────────────────┐
│ metadata                │
│ Record A                │
│ Record AAAA             │
│ ptr --------------------┼─────────────┐
└─────────────────────────┘             │
                                        ▼
                                unrelated heap region
                                ┌───────────────────┐
                                │ TXT payload       │
                                └───────────────────┘

CPU 无法保证:

Record
RecordData
Name
TXT
MX
...

都在附近。

所以 lookup 过程中可能不断发生:

load pointer
    ↓
follow pointer
    ↓
new cache line
    ↓
possibly cache miss
    ↓
wait for memory

这就是:

pointer chasing

对象模型越“漂亮”,有时候内存访问反而越散。


9. 最关键的一步:不要再存 Rust Object,直接存 Bytes#

Cloudflare 最终问了一个很重要的问题:

我们为什么一定要在 cache 中保存 parsed Record object?

DNS 数据最后本来就需要被重新编码成:

DNS wire format

于是一个自然方案是:

直接把整个 DNS Response packet 缓存下来

Cache hit 时:

memcpy(packet)
    ↓
patch message ID
    ↓
send

听起来非常完美。

但是这里又有两个问题。


9.1 DNSSEC DO bit#

客户端是否请求 DNSSEC record,取决于 EDNS 中的:

DO = DNSSEC OK

如果直接 cache 完整 response:

Response with DNSSEC
Response without DNSSEC

可能就需要保存两个版本。

否则 lookup 时还得重新解析完整 packet,然后删除:

RRSIG
DNSKEY
NSEC
...

这违背了优化目标。


9.2 有些 Record 需要重新做 Name Compression#

DNS wire message 中:

CNAME
NS
MX
SOA

等 record 包含 domain name。

最终输出 packet 时,DNS message compression 与:

这个 name 在最终 packet 的什么位置

有关。

所以这些记录不能简单地无脑 memcpy。


10. Cloudflare 最终选择:Hybrid Representation#

最终不是:

Structured Rust Objects

也不是:

Whole DNS Packet

而是中间路线:

Structured metadata
+
wire-format RecordData bytes

Cloudflare 将 records 编码成一个连续:

Box<[u8]>

内部类似:

┌─────────────┬─────────────────────┐
│ len: u16    │ record bytes        │
├─────────────┼─────────────────────┤
│ len: u16    │ record bytes        │
├─────────────┼─────────────────────┤
│ len: u16    │ record bytes        │
├─────────────┼─────────────────────┤
│ ...         │ ...                 │
└─────────────┴─────────────────────┘

整体演进可以总结为:

示意图

这实际上是整个优化最核心的一步。


11. Insert Path 到底怎么实现?#

Cloudflare 没有公开最终完整的 production struct,因此下面是根据文章重建的概念模型,不是其源码逐字还原。

可以把最终 entry 理解成:

struct CacheEntry {
    metadata: CacheMetadata,

    // Logical section boundaries / record metadata.
    sections: SectionMetadata,

    // Immutable compact representation.
    records: Box<[u8]>,
}

而插入过程大致变成:

示意图

这里有一个容易忽略、但非常漂亮的优化:

Scratch Buffer#

如果每次 insert 都这样:

let mut buf = Vec::new();

serialize(&mut buf);

let data = buf.into_boxed_slice();

仍然可能发生:

allocate
grow
reallocate
grow
...

并且 allocator 不一定真的能从 ​Vec shrink 中收回尾部空间。

Cloudflare 使用一个会在多次 cache insertion 之间复用的:

scratchspace buffer

第一次:

capacity = 0
→ 128
→ 256
→ 512
→ ...

之后多数请求:

capacity 已经足够

只需要:

clear
serialize

而无需重新分配。

最终知道准确长度以后:

scratch buffer
      ↓
exact Box<[u8]>
      ↓
memcpy once

最终 cache entry 因而只保留刚好需要的 bytes。

这一变化单独就让 Cloudflare benchmark 中 cache insertion throughput 又提高约 13% 。


12. Lookup Path 为什么反而更快?#

通常听到:

把 parsed object 改成 raw bytes

第一反应可能是:

那不是要重新 parse?

但这里恰恰相反。

很多 DNS record:

A
AAAA
TXT
DNSSEC records
...

其 record data 可以直接从 cache buffer:

memcpy

到最终 DNS response。

于是过去:

Parsed Rust Record
        ↓
match RecordData enum
        ↓
read fields
        ↓
serialize field
        ↓
serialize field
        ↓
serialize field
        ↓
wire format

变成:

Cached wire-format bytes
        ↓
memcpy
        ↓
wire format

只有包含 domain name 的:

CNAME
NS
MX
SOA
...

仍然需要解析,因为最终 packet 构建时需要重新进行 DNS name compression。

整个 lookup path 可以表示为:

示意图

这里出现了一个重要变化:

Random access
      ↓
Sequential scan

原来的:

records[index]

变得没那么方便。

现在需要:

read u16 length
advance length bytes
read next u16
...

尤其 round-robin A/AAAA rotation 的实现会更复杂。

但 Cloudflare 做出的判断是:

每个 DNS cache entry 的 record 数量本身很少

因此:

O(1) random access

并没有真正重要到值得付出 object graph 的内存和 cache-locality 成本。

这是一个很经典的工程取舍:

Big-O 相同甚至更差,并不代表实际 CPU performance 更差。


13. 为什么连续 Buffer 会让 CPU 更快?#

现代 CPU 读取内存并不是一个 byte 一个 byte 地取。

而是以 cache line 为基本单位,例如常见:

64 bytes

如果数据是:

Record 1
Record 2
Record 3
Record 4

连续排列:

RAM

┌──────────────── Cache Line ────────────────┐
│ Record1 │ Record2 │ Record3 │ part Record4│
└────────────────────────────────────────────┘

CPU 读取 Record1 时,很可能顺便已经把 Record2 和 Record3 带进 L1/L2 cache。

这种访问方式非常接近:

for record in records {
    process(record)
}

CPU hardware prefetcher 也非常喜欢。

反过来,如果是:

Record
   ↓ pointer
Heap Object

Record
   ↓ pointer
Heap Object

Record
   ↓ pointer
Heap Object

访问模式就可能变成:

CacheEntry
    │
    ├── L1 miss
    │
    └── pointer
          ↓
        Heap page A
          │
          └── pointer
                ↓
              Heap page F

这会同时增加:

cache miss
TLB pressure
pointer dependency
allocator metadata
memory fragmentation

所以:

减少内存占用与提升性能,在这种 workload 下其实是同一个问题。

Cloudflare 最终的 lookup latency:

828 ns
  ↓
670 ns

下降约:

19%

其中最后的 wire-format representation 本身就在 benchmark 中进一步改善了 lookup latency。


14. 五轮优化,其实是在逐渐消灭「对象」#

把整个过程重新抽象一下:

示意图

可以发现 Cloudflare 一开始优化的是:

field

后来优化的是:

struct

再后来优化:

allocation

最终优化的是:

representation

这是性能工程中非常典型的层次:

Micro Optimization
        ↓
Data Structure Optimization
        ↓
Memory Layout Optimization
        ↓
Representation Redesign

最后一层往往收益最大。


15. Cloudflare 是怎么测出来的?#

还有一点非常值得学习:

Cloudflare 并不是:

改代码
→ 看 RSS
→ 感觉省内存了

而是同时维护两套测量。

Micro Benchmark#

他们构造了接近生产流量分布的数据:

A      56%
AAAA   25%
TXT    19%

每个 entry:

1 ~ 4 records

TXT 大小随机分布在:

64 ~ 224 bytes

并通过自定义 allocator wrapper 统计:

allocation count
allocation size
bytes / cache entry

同时测量:

insert throughput
lookup latency

从而避免出现:

内存少了 30%
但是 CPU 慢了 50%

这种“优化”。


Production RSS#

Micro benchmark 无法反映:

allocator fragmentation
真实 query distribution
ECS cache variants
cache occupancy
其他 process memory

因此 Cloudflare 最后又通过真实生产实例的 resident memory 验证。

上线阶段从:

2026-05-18

逐步持续到:

2026-07-06

生产环境中:

p99 instance memory

9.3 GB
   ↓
5.3 GB

约下降:

43%

p90:

6.5 GB
   ↓
3.8 GB

约下降:

42%

这也解释了为什么 benchmark 的:

-56%

不会原样反映成 process RSS 的 ​-56%。

因为:

RSS
=
DNS Cache
+
runtime
+
allocator
+
network buffers
+
Wasm
+
other application state
+
...

Cache 只是整个 process memory 的一部分。


16. 为什么 100 TB 并不夸张?#

这是整个案例最容易低估的地方。

假设:

250 billion entries

每条只节省:

100 bytes

就是:

250,000,000,000 × 100
=
25,000,000,000,000 bytes
≈
25 TB

而 Cloudflare benchmark 中:

953 B
→
420 B

差值达到:

533 B / entry

当然:

benchmark entry count
≠
fleet 中所有 entry 的实际分布

所以不能简单计算:

533 × 250B

作为生产节省量。

Cloudflare 最终使用生产 working set 得出的数字约为:

100 TB

这仍然说明一个非常重要的问题:

Hyperscale systems 改变了“值得优化”的定义。

在一个:

10,000 entries

的应用里,节省:

64 bytes / entry

毫无意义。

总共:

640 KB

但在:

250 billion entries

时:

64 bytes

就是十几 TB。


17. 这套设计真正值得复用的 7 个原则#

Cloudflare 这次优化其实可以提炼成七个更通用的系统设计原则。

1. 数据进入 immutable 生命周期以后,representation 应该改变#

不要长期保存:

builder representation

应该:

Mutable Builder
      ↓
freeze
      ↓
Compact Immutable Representation

典型场景:

cache
index
snapshot
AST
model weights
routing table
configuration

2. 不要为不可能发生的 mutation 付费#

如果一个 collection 永远不会:

push
insert
grow

那么:

capacity

就是纯 metadata tax。


3. Common case 应该决定 layout#

不要让:

rare NAPTR

决定:

every A record

的大小。

应该反过来:

A / AAAA
决定 hot representation

NAPTR / uncommon type
承担 exception cost

即:

Optimize common path
Degrade rare path gracefully

4. Context 可以替代数据#

如果:

record.owner
==
cache_key.qname

那么:

record.owner

不是信息,只是重复。

这和数据库 normalization、dictionary encoding、column compression 的基本思想类似。


5. Allocation Count 和 Allocation Bytes 同样重要#

两个方案即使:

payload bytes

差不多,也可能因为:

10 allocations
vs
1 allocation

表现完全不同。

原因包括:

allocator metadata
size class rounding
fragmentation
cache locality
pointer chasing

6. Serialization Format 也可以是 Runtime Format#

传统设计通常:

Wire Format
    ↓ parse
Object Model
    ↓ serialize
Wire Format

Cloudflare 最后变成:

Wire Format
    ↓ partial normalization
Cache-oriented Binary Format
    ↓ mostly memcpy
Wire Format

这里减少的不是一个函数调用。

而是:

parse
object construction
heap allocation
enum dispatch
field serialization

整条 pipeline。


7. “更抽象”不一定“更高级”#

传统软件工程可能倾向:

Record {
    owner: Name,
    ttl: TTL,
    data: RecordData
}

因为它:

self-contained
type-safe
easy to manipulate
easy to reason about

而 Cloudflare 最终的:

metadata
+
offset
+
flags
+
length-prefixed bytes

明显更加低级。

但对于 cache:

read-heavy
immutable
billions of objects
latency-sensitive

后一种 representation 恰恰更加正确。

所以:

好的 abstraction 必须与生命周期和 workload 对齐。

而不是单纯追求对象模型漂亮。


18. 从系统层面看,这其实是在构造一个 Cache-specific IR#

如果进一步抽象,我认为 Cloudflare 最终实际上设计出了一个:

DNS Cache IR

也就是:

Network Wire Representation
            ↓
Parser
            ↓
Rich Runtime Representation
            ↓
Cache Normalizer
            ↓
Compact Cache IR
            ↓
Response Builder
            ↓
Network Wire Representation

可以画成:

示意图

这个视角比单纯理解为:

Rust 内存优化

更有价值。

Cloudflare 实际做的是:

为 DNS cache workload 设计专用的数据中间表示。

它既不是:

网络协议表示

也不是:

业务对象表示

而是:

Serving Representation

19. 这对普通后端开发有什么意义?#

可能有人会觉得:

2500 亿条缓存
跟普通 SaaS 没关系。

其实设计原则非常通用。

例如你的系统中有:

LLM Context Cache
Embedding Cache
Prompt Cache
Feature Cache
Routing Rules
Authorization Policy
Session Snapshot
Agent Memory
Search Index

如果这些数据:

写一次
读很多次
几乎不修改
数量巨大

那么都应该问一次:

当前在内存里保存的是:

业务最方便操作的 Representation

还是:

Serving 最适合读取的 Representation?

尤其对于:

Rust
C++
Go
Java

这类长期运行的 backend service,更应该观察:

bytes / object
allocations / object
pointers / object
cache lines / lookup
serialization cost / lookup

而不能只观察:

CPU%
RSS
QPS

20. 最后总结#

Cloudflare 这次 100 TB 内存优化,如果只看表面,可以总结成:

Vec → Box
enum → Box
Record → bytes

但这会错过最重要的部分。

真正的演进其实是:

方便编程的数据结构
        ↓
符合生命周期的数据结构
        ↓
符合真实数据分布的数据结构
        ↓
符合 allocator 的数据结构
        ↓
符合 CPU memory hierarchy 的数据结构
        ↓
符合实际 serving path 的数据表示

最终:

953 B → 420 B

不仅省下了内存。

还让:

Insert
625K/s → 893K/s

Lookup
828ns → 670ns

同时变快。

这可能是这篇文章最值得记住的一句话:

在足够大的系统里,Data Representation 本身就是 Architecture。

当一个对象会存在几十亿、几百亿甚至几千亿份时,决定系统成本的往往已经不是:

用了什么数据库
用了什么缓存框架
用了什么网络协议

而是更加基础的问题:

这个对象到底由多少 bytes 构成?

里面有几个 pointer?

会触发几次 allocation?

CPU 需要跨多少 cache line 才能把它读完?

有没有保存根本不需要保存的信息?

Cloudflare 的答案最终非常接近高性能系统最朴素的一条原则:

Store less.
Allocate less.
Chase fewer pointers.
Keep hot data contiguous.
Do less work on the hot path.

而当这个原则被应用到 2500 亿级对象 上时,结果就是:

≈ 100 TB RAM

被释放出来。