# Saving 100 terabytes of memory by optimizing 1.1.1.1's DNS cache

> 来源：[HackerNews](https://blog.cloudflare.com/dns-cache-memory-optimization-1111/)

## 背景与概述

当我们在浏览器中输入一个网址并按下回车，背后发生的第一件事往往被大多数人忽略——DNS 解析。这个看似简单的“域名转 IP”过程，实际上承载着整个互联网的寻址体系。对于 Cloudflare 这样的全球性 CDN 和 DNS 服务商来说，1.1.1.1 作为其公共 DNS 服务，每天要处理数以万亿计的查询请求，而每一次查询的背后，都离不开一个关键组件：DNS 缓存。

缓存的意义在于“复用”。如果每个 DNS 查询都要递归到权威服务器去获取答案，不仅延迟会飙升，上游服务器的压力也会不堪重负。因此，1.1.1.1 会在内存中维护一个庞大的 DNS 缓存表，将近期查询过的域名记录暂存起来。然而，随着互联网域名的爆炸式增长，这个缓存表的体积也在不断膨胀——直到有一天，工程师们发现，它竟然吃掉了超过 100 TB 的内存。

100 TB 是什么概念？假设一台服务器配备 256 GB 内存，这相当于 400 台服务器全部用于缓存。显然，这不是一个可持续的状态。Cloudflare 的工程师们决定对 1.1.1.1 的 DNS 缓存进行深度优化，最终成功节省了 100 TB 的内存。这篇��章将带你深入剖析他们是如何做到的，以及我们能从中获得哪些启发。

## 核心内容

### 1. 问题定位：缓存条目中的“隐性浪费”

在优化之前，工程师们首先对缓存的数据结构进行了剖析。一个典型的 DNS 缓存条目包含什么？域名（如 `example.com`）、记录类型（A、AAAA、CNAME 等）、TTL（生存时间）、以及一组 IP 地址或文本记录。直观来看，这些字段都是必需的。但问题出在“存储方式”上。

最初的实现中，每个缓存条目都是一个独立的对象，包含指向字符串的指针、时间戳、标志位等元数据。在 64 位系统上，一个空对象可能就要占用 16 字节的头部空间。当缓存中有数十亿个条目时，这些“看不见”的头部开销就累积成了天文数字。更糟糕的是，由于内存对齐和填充（padding），实际占用的空间往往比理论值还要多出 20%-30%。

### 2. 优化策略一：紧凑的二进制编码

第一个突破口是改变数据的表示方式。工程师们将原本使用字符串存储的域名，改为紧凑的二进制格式。例如，对于常见的顶级域（`.com`、`.net` 等），使用枚举值代替完整的字符串；对于 IP 地址，直接使用 4 字节或 16 字节的二进制形式，而不是可读的文本形式。

这种做法的收益是立竿见影的：一个 IPv4 ���址从 `"192.168.1.1"`（含引号约 13 字节）压缩为 4 字节，节省了约 70% 的空间。对于域名，如果原本平均长度为 25 字符，压缩后可能只需 10 字节左右。

### 3. 优化策略二：共享 TTL 与批量过期

DNS 缓存中，每个条目都有自己的 TTL（生存时间）。传统做法是为每条记录单独存储一个绝对过期时间戳。但 Cloudflare 的工程师发现，大量记录的 TTL 值其实相同（例如，很多记录都是 300 秒或 3600 秒）。他们引入了“TTL 桶”的概念：将相同 TTL 的记录归入同一个桶，桶内共享一个过期时间基准值，每条记录只需存储一个相对偏移量。

这一改进不仅减少了时间戳的存储开销，还让缓存清理（过期扫描）变得更加高效——只需检查桶的基准时间，而不必遍历每条记录。

### 4. 优化策略三：无锁并发与内存池

在高并发的 DNS 服务中，缓存读写需要保证线程安全。原来的实现使用了读写锁（RWLock），但锁竞争在高 QPS 下会成为瓶颈，同时锁本身也占用内存。优化后，工程师采用了基于原子操作的无锁哈希表（lock-free hash table），并结合了内存池（memory pool）技术——预先分配一大块连续内存，按需从池中分配条目，而不是每次通过 `malloc` 向操作系统申请。

内存池的好处是减少了内存碎片，也避免了频繁的系统调用。更重要的是，由于内存是连续的，CPU 缓存的命中率大幅提升，查询速度反而更快了。

### 5. 结果：100 TB 的节省

经过上述组合优化，1.1.1.1 的 DNS 缓存内存占用从原来的 100+ TB 降至接近 0 TB（实际是降至可忽略的规模）。这个数字听起来夸张，但考虑到 Cloudflare 的全球网络规模，以及缓存条目数量级在百亿级别，这样的节省是完全合理的。更重要的是，查询性能不仅没有下降，反而因为更好的内存局部性和无锁设计而有所提升。

## 技术分析

从架构层面看，这次优化体现了几个核心原则：

**第一，数据结构的选择至关重要。** 哈希表、跳表、B+树各有优劣，但在超大规模缓存场景下，内存布局的紧凑性往往比算法复杂度更重要。Cloudflare 选择的是定制化的开放寻址哈希表，而非标准库中的实现，就是为了精确控制内存布局。

**第二，空间换时间的经典权衡被打破。** 通常我们认为“空间换时间”是常态，但这里通过减少空间占用，反而提升了时间性能（CPU 缓存友好）。这说明在内存带宽受限的场景下，紧凑的数据结构可能同时赢得空间和时间。

**第三，系统级优化需要端到端的视角。** 从字符串编码、TTL 管理到并发控制，每一层都有微小的浪费，但累积起来就是 100 TB。这提醒我们，性能优化不是单点突破，而是系统性工程。

## 实践建议

对于普通开发者，虽然我们可能不需要处理百亿级的缓存条目，但以下几点建议依然有直接参考价值：

1. **审视你的数据结构**：检查你的代码中是否有过度使用字符串的地方。例如，IP 地址、状态码、枚举值，是否可以用整数或枚举代替？在 Go 或 Rust 中，`struct` 的字段顺序会影响内存对齐，合理排列字段可以减少 padding。

2. **利用内存池或对象复用**：如果你在 Go 中使用 `sync.Pool`，或在 Java 中使用 `ThreadLocal` 来复用对象，可以显著减少 GC 压力和内存碎片。对于高频创建和销毁的对象，这几乎是必须的。

3. **考虑无锁数据结构**：如果你的服务有高并发读多写少的场景，可以研究一下 `sync.Map`（Go）或 `ConcurrentHashMap`（Java）的内部实现，理解它们如何通过分段或原子操作避免锁竞争。但注意，无锁结构通常更复杂，需要谨慎测试。

4. **监控内存分配的细节**：使用 `pprof`（Go）或 `JFR`（Java）等工具，分析内存分配的热点。很多时候，内存浪费并不在业务逻辑中，而是隐藏在标准库或框架的默认行为里。

5. **不要过早优化，但要持续度量**：Cloudflare 的优化是基于详尽的 profiling 数据的。在动手优化之前，先用工具量化你的内存占用分布，找到真正的“大头”。

## 总结

Cloudflare 这次对 1.1.1.1 DNS 缓存的优化，不仅仅是一次技术胜利，更是一次工程思维的展示。它告诉我们：在互联网基础设施的规模下，任何微小的低效都会被放大到惊人的程度。100 TB 的内存节省，背后是工程师对每一个字节的极致追求。对于中国开发者而言，这个故事同样具有启发意义——无论是做 CDN、边缘计算，还是大型分布式系统，内存优化都是一项值得长期投入的硬功夫。希望这篇文章能为你打开一扇新的优化思路之窗。