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:
这里会同时出现四种浪费:
- container metadata;
- unused capacity;
- struct alignment / padding;
- 多次 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
被释放出来。