Hacker News 中文摘要

RSS订阅

通过优化1.1.1.1的DNS缓存节省100TB内存 -- Saving 100 terabytes of memory by optimizing 1.1.1.1's DNS cache

Cloudflare通过五项内存优化措施,将DNS缓存条目占用空间削减超50%,释放约100TB内存,相当于130台服务器的内存总量。同时缓存性能提升,插入吞吐量增加43%,查找延迟降低19%。

正文

好的,这是根据您的要求,对原文主要内容进行的中文重述,保留了关键细节,并删减了与主题无关的内容。


标题:通过优化1.1.1.1的DNS缓存,我们节省了100TB内存

Cloudflare的DNS服务平台Big Pineapple在任何时刻都存储着超过2500亿条DNS缓存条目。在此规模下,每个条目浪费一个字节,整个集群就会多消耗超过250GB的内存。

通过对缓存条目在内存中的存储方式进行五次连续优化,每个条目的内存占用降低了超过50%。这些优化在整个集群中释放了约100TB的内存,相当于130台第13代服务器的内存总量。同时,缓存性能也得到了提升:插入吞吐量提高了43%,查找延迟降低了19%。

我们缓存了什么?

缓存中的每个条目都是一个键值对。键(CacheKey)标识了查询内容,包含域名、记录类型等。值(CacheEntry)存储了DNS响应本身,包括回答、权威区和附加区记录,以及创建时间、命中次数、TTL等元数据。这两个结构体都有优化空间,因为一些字段在条目存储后带来了不必要的开销。

衡量内存使用

为了衡量每次优化的效果,我们使用随机生成的条目填充缓存进行基准测试,这些条目模拟了生产环境中的流量分布:56%的A记录、25%的AAAA记录和19%的TXT记录。我们通过自定义分配器追踪每个缓存条目的分配数量和大小,同时测量插入吞吐量和查找延迟,以确保内存节省不以牺牲性能为代价。

优化一:消除容量字段的开销

Vec<T> 存储了指向堆数据的指针、当前长度和总容量三个字段。一旦DNS响应被存入缓存,就再也不会被修改,因此容量字段毫无用处,但每个Vec仍会占用8字节。同时,Vec预分配的多余堆空间也被浪费了。

使用Box<[T]>可以解决这两个问题。它在创建后无法增长,因此不需要容量字段或为未来元素预留空间。String同理,用Box<str>替换可以去掉其容量字段。每个缓存条目存储了8个Vec和String字段,将它们替换后,每个字段节省8字节,每个条目共节省64字节。结合消除的预分配堆内存,在2500亿条目的规模下,总计节省超过15TB内存。

优化二:减少列表,减少指针

与其将回答、权威区和附加区记录存储在三个独立的列表中,不如将它们存储在一个列表中,并用偏移量标记每个部分的起始位置。由于每个部分的记录数量可以用u16(2字节)表示,相比每个独立Box<[T]>所需的8字节指针和8字节长度,这节省了大量空间。此优化移除了两个列表,并用两个2字节的偏移量替代,每个条目节省了28字节。

优化三:丢弃记录所有者

每条DNS记录都有一个所有者(所属域名)。在许多情况下,这个所有者与查询的域名相同。对于这些记录,我们可以完全丢弃所有者字段,在读取时从缓存键中推断出来。只有当所有者不同时(例如CNAME后面的A记录),我们才存储完整的名称。在实践中,大多数缓存记录的所有者都与查询域名相同,因此大部分记录不需要为所有者字段进行堆分配。

优化四:枚举类型大小优化

Rust的枚举类型大小由其最大的变体决定。将每种DNS记录类型作为枚举变体存储时,由于NAPTR等大型变体的存在,整个枚举类型变得很大(144字节)。而占流量80%以上的A记录(4字节)和AAAA记录(16字节)会浪费超过120字节的填充空间。

通过将大型变体装箱(Box),枚举类型只存储一个8字节的堆指针,而数据在堆上只占用其实际所需的大小。对于A和AAAA记录,每个记录节省了120字节。虽然这引入了分配器开销和内存局部性变差的问题,但总体收益巨大。

优化五:以线路格式存储记录

作为折中方案,我们将记录数据以原始字节形式存储,而缓存条目的其余部分仍保持结构化字段。我们将所有记录编码为一个带长度前缀的字节缓冲区(Box<[u8]>)。这消除了每个变体的枚举开销和之前优化中引入的装箱堆分配。数据变得连续紧凑,改善了CPU缓存局部性。在构建DNS响应时,大多数记录类型(如A、AAAA、TXT)可以直接从缓冲区复制到出站消息中,减少了查找路径上的工作量。结合改善的内存局部性,这使得缓存查找延迟降低了5%。同时,为构建记录数据缓冲区,我们使用一个可复用的暂存空间,将之前每个装箱记录的单独分配替换为一次分配,这使缓存插入吞吐量提高了13%。

结果

在生产环境中,这些优化使每个实例的内存使用量大幅下降。在p99百分位上,内存从9.3 GB降至5.3 GB,降低了43%;在p90百分位上,从6.5 GB降至3.8 GB,降低了42%。在基准测试中,每个条目的内存占用从953字节降至420字节,降幅达56%。整个集群的聚合工作集内存降低了约100TB。

性能方面,缓存插入吞吐量提高了43%,查找延迟降低了19%。

| 指标 | 优化前 | 优化后 | 变化 | | :--- | :--- | :--- | :--- | | 每条目净占用 | 953 字节 | 420 字节 | -56% | | 每条目分配量 | 1.1 KB | 461 字节 | -58% | | 缓存插入吞吐量 | 625,000 条目/秒 | 893,000 条目/秒 | +43% | | 缓存查找延迟 | 828 ns | 670 ns | -19% |

我们计划将释放的内存重新投入到增加缓存容量中,以提高缓存命中率并减少上游查询量。

评论

根据评论内容,总结主要观点如下:

1. 对缓存设计缺陷的批评(评论1) - 指出DNS响应缓存后永不修改,但capacity字段仍占用8字节/Vec,属于设计冗余。 - 质疑系统设计时未发现此类明显问题。 - 关键引用:"Once we store a DNS response in the cache... we never modify it again. The capacity field serves no purpose, but still costs 8 bytes per Vec" / "Were there no design discussions/reviews when the system was setup to catch trivial things like this?"

2. 系统编程与内存优化的重要性(评论2、3) - 强调系统编程仍具价值,指出Rust中可优化为将记录数据直接放在CacheEntry成员后,而非单独分配内存。 - 分享MaraDNS经验:通过单次malloc()分配大块内存,将黑名单内存占用从237MB降至9.5MB。 - 关键引用:"This is why system programming still matters... missing the obvious optimisation of putting the record data right after the CacheEntry members" / "When I was using one malloc() per entry... 237 megabytes... optimized... only took up 9.5 megabytes"

3. 数据序列化与对齐问题(评论4) - 指出文章采用的2字节长度前缀+原始字节的编码方式与netlink协议类似,但存在对齐问题。 - 建议使用[u32; 1024]声明缓冲区以确保4字节对齐,避免Miri检测到未定义行为。 - 关键引用:"Interestingly this is exactly how netlink works-ish... alignment is only guaranteed to be on 1 byte, not 4 bytes" / "declare the buffer with a type that mandates the alignment... [u32; 1024]"

4. 缓存必要性与规模质疑(评论5) - 质疑如此大的缓存是否仍算"缓存",建议考虑用更少的RAM数据库替代多个缓存。 - 关注数据源规模(2500亿条目)与响应速度的关系。 - 关键引用:"why are they cacheing at all? If your cache is that big it isn't a cache" / "replace your multiple caches with fewer in-RAM databases"

5. 安全性与优化权衡(评论7) - 指出将多个独立列表合并为单个Vec并依赖偏移量,可能削弱Rust的安全保证(如越界访问无panic)。 - 认为这是小问题,但文章未提及此权衡。 - 关键引用:"joining several distinct list into a single one... undercuts Rust's safety guarantees" / "open the door to indexing out of range of these sub-slices without any panics"

6. 解析与性能的意外发现(评论8) - 指出更丰富的解析表示不一定更快,因为热路径是"从缓存读取并序列化回DNS",预解析可能增加不必要工作并损害局部性。 - 关键引用:"the richer parsed representation was not necessarily the faster one" / "parsing everything upfront only to serialize it again can become unnecessary work and hurt locality"