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

来源:HackerNews

背景与概述

当我们在浏览器中输入一个网址并按下回车,背后发生的第一件事往往被大多数人忽略——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。
  1. 利用内存池或对象复用:如果你在 Go 中使用 sync.Pool,或在 Java 中使用 ThreadLocal 来复用对象,可以显著减少 GC 压力和内存碎片。对于高频创建和销毁的对象,这几乎是必须的。
  1. 考虑无锁数据结构:如果你的服务有高并发读多写少的场景,可以研究一下 sync.Map(Go)或 ConcurrentHashMap(Java)的内部实现,理解它们如何通过分段或原子操作避免锁竞争。但注意,无锁结构通常更复杂,需要谨慎测试。
  1. 监控内存分配的细节:使用 pprof(Go)或 JFR(Java)等工具,分析内存分配的热点。很多时候,内存浪费并不在业务逻辑中,而是隐藏在标准库或框架的默认行为里。
  1. 不要过早优化,但要持续度量:Cloudflare 的优化是基于详尽的 profiling 数据的。在动手优化之前,先用工具量化你的内存占用分布,找到真正的“大头”。

总结

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