Cloudflare 发了篇《How we saved 100 terabytes of memory by optimizing 1.1.1.1's DNS cache》。这类文章我很少错过,因为它把"内存优化"这件听起来抽象的事,落到了具体的字节账上。
先把量级摆出来
支撑 1.1.1.1 的平台叫 Big Pineapple,它同时跑着 Gateway DNS、DNS Firewall、AS112 等好几个 DNS 服务。这个平台上任意时刻存着超过 2500 亿条 DNS 缓存记录。
作者给了一个换算,是全篇最有说服力的一句:在这个规模下,每条记录浪费一个字节,整个集群就要多花超过 250GB 内存。
2500 亿条,一条一字节 = 250GB。这个换算说明了一件事:在这种量级下,任何"每条记录多几个字节无所谓"的想法都是错的。缓存条目的结构体里那些为了对齐而插入的 padding、那些可以用更窄类型表示的字段、那些重复存储的字符串,乘以 2500 亿之后都是可观的账单。
结果:五轮改动,减半
他们做了五轮连续的改动,把每条记录的内存占用砍掉了 50% 以上。而且——这是我最看重的一点——没有牺牲速度:插入吞吐提升了 43%,查找延迟下降了 19%。
这两组数字通常是对立的。压缩数据结构往往意味着解码开销、额外的间接层、更差的缓存局部性,最后用 CPU 换内存。这里能同时改善,说明改动的方向不是"压得更紧",而是"去掉本来就不该有的开销"。
报告里有一个细节很能说明这类工作的性质:某次改动减少了结构体周围的 padding,结果结构体缩小的幅度超过了那几个布尔字段本身的大小。意思是,他们不是把字段变小省了几个字节,而是删掉/重排字段之后,整个结构体的对齐空洞塌掉了。这是典型的"结构体布局优化"收益——你改一个字段的类型,省下的不是那 1 个字节,是它引发的 7 个字节 padding。
这类优化具体在做什么
DNS 缓存条目的优化,套路基本固定,从 Cloudflare 给出的数字能反推他们踩过的几类:
消除 padding。 C 结构体按最宽字段对齐,字段顺序不当会产生大量空洞。把 bool 这类窄字段集中到一起、把 64 位字段排在最前或最后,能显著减小结构体。上一条提到的"缩小幅度超过布尔本身"就是这个效果。
用相对时间代替绝对时间。 TTL 如果用绝对时间戳(64 位秒/纳秒)存,每条要 8 字节以上;如果存成"相对某个基准的秒数"并用更窄的类型(比如 32 位甚至 24 位),能省一半以上,还能顺带解决时间精度过剩的问题。
域名驻留/压缩。 DNS 名字是分标签的字符串,重复率极高。同一批记录里大量名字共享相同的顶级域和二级域。把标签做驻留、用索引代替字符串、或者按后缀树共享前缀,收益很大。报告里那条示例记录 300 IN A 198.51.100.1 example.com 就是典型的缓存条目形态——TTL(300)、类型(IN A)、地址、名字。
去掉冗余字段。 一条缓存记录往往同时存了"从哪个上游学到""什么时候过期""被查过几次""是否被标记"等信息。很多字段可以从别的字段推导出来,或者用几个 bit 表示。把一批布尔和枚举打包进一个位域,比每个占一字节更省。
改布局方式。 从"每条记录一个独立分配的结构体(AoS)"改成"所有记录的同名字段连续存放(SoA)",查找时只加载需要的字段,缓存局部性更好。这往往同时改善内存和延迟——正好对应他们 19% 的延迟下降。
值得学的思路:先算"每条省多少 × 条数"
这篇最有价值的地方不是具体技巧,是它的思考顺序:先把总账算出来,再决定要不要动手。
如果只看单条记录,"省几个字节"听起来微不足道,很容易被排到待办列表末尾。但把它乘以 2500 亿,结论就反过来了——省 1 字节等于 250GB,这比大多数架构级优化的收益都大。
这个思路可以反过来用:当你面对一个"数据量巨大、单条很小"的系统时,不要优化算法复杂度,去优化单条的大小。算法复杂度关心的是增长的阶,但如果你有 2500 亿条固定规模的数据,真正决定成本的是每条的大小。
我的做法
在缓存/索引这类长期驻留的数据结构上,我现在会固定做三步:
- 先量单条的真实内存占用,用
pahole或sys.getsizeof之类看结构体实际布局,找出 padding 和重复字段。 - 算"单条 × 条数"的总账,判断优化值不值得做。多数情况下结论是"值得",因为缓存条目数量通常比人想的大得多。
- 优先做"减法"而不是"压缩"。删掉可推导的字段、合并重复字符串、缩窄整数类型,这些不增加解码成本;如果只能靠编码/解压来省空间,就要重新评估延迟代价。
Cloudflare 这篇的难得在于,它把一个"看起来是毫米级"的优化做成了"千米级"的收益,而且没有为此付性能税。
浙公网安备 33010602011771号