再省下 100TB 内存
文章摘要
这篇 Cloudflare 博客发表于 2026 年 9 月 18 日,原标题是《用数学(和 Rust)再省下 100TB 内存》,作者是 Kevin Guthrie、Mariia Iurchenko、Zaidoon Abd Al Hadi 和 Ivan Babrou。标题里的”再”是因为一个月前,Cloudflare 的 DNS 团队刚刚省下了 100TB 内存。这次的主角是另一个服务:通过改动一个算法的几个细节,一个基于 Pingora 的服务内存占用大幅下降,全球合计回收了超过 100TB 内存。
开篇讲的是规模。 作者说 Cloudflare 的规模大到”在这里工作多年了都觉得不真实”:全球有数千台服务器,合计 PB 级的内存和数百万个 CPU 核心,而且全部被压到极限。资源再庞大也是有限的,当每个服务都要在每个节点上运行时,就没有浪费的余地。在这种规模下,小改进会被极大放大,哪怕每次只省 1% 也值得庆祝。
起因是一张工单。 性能团队的 Ivan 提了一张工单:”Pingora Backend Router 中 pingora-ketama 内存占用过高”。Pingora Backend Router(简称 PBR)是 Cloudflare 内部的负载均衡服务,pingora-ketama 是他们开源的一致性哈希库。PBR 的内存用量远超预期,大头都在和 pingora-ketama 相关的数据结构里。
一致性哈希入门。 Cloudflare 用一致性哈希按 URL 把可缓存的请求路由到服务器,这样每个数据中心里每个文件只需要存一份,而且能稳定地找到它在哪台机器上。作者没有用常见的”哈希环”来讲,而是把 32 位哈希输出画成一条数轴:服务器按 IP 等标识哈希到数轴上,任务按缓存键哈希到数轴上,每个任务分配给它左边第一台服务器;最右边的区间绕回开头,这就是”环”的由来。问题在于每台服务器负责的区间长度是随机的,而服务器承担的请求量和区间长度成正比。
数学告诉你单点哈希有多不均匀。 对 N 台服务器、每台一个哈希点的情况,每台服务器区间占比的期望是 1/N,标准差是 (1/N)·sqrt((N−1)/(N+1))。把标准差除以期望得到变异系数 CV = sqrt((N−1)/(N+1))。当 N = 100 时,CV 约为 99%,这意味着有些服务器的工作量可能是应有水平的两倍,另一些却几乎无事可做。
“唯一的解法就是更多的哈希。” 作者调侃说,一致性哈希的简单是一把双刃剑:任何改进都只能落在同一条数轴上,所以所有问题的答案都是加更多哈希。”它不像一把金锤子(拿着它看什么都像钉子),更像一颗金钉子,让所有工具都变成了锤子。”第一层是给每台服务器分配多个哈希点,靠大数定律让总长度趋于平均。NGINX 把每台服务器的基准哈希点数硬编码为 160,Pingora 也沿用这个默认值;在 100 台服务器的例子里,这能把 CV 从约 99% 降到约 8%。第二层是 ketama 算法的加权:如果希望服务器 S1 承担的请求是 S2 的 w 倍,就给它 w 倍的哈希点。Cloudflare 按磁盘容量设置权重(算力密集型的业务可能按 CPU 或 GPU 数量)。第三层是最要命的:由于合规要求、启用的缓存功能等原因,只有一部分服务器能处理某个请求,这没法靠在同一个环上加点解决,每一种功能组合都可能需要一个独立的环。”按组合复制是指数爆炸的经典配方”:几个功能就产生了 2 的”几”次方,也就是几十个独立的一致性哈希环。Ivan 发现的”内存过高”(有些情况下达到 6GB)就是这么来的。
第一个优化:挤掉结构体里的两个字节。 Zaidoon 注意到 PBR 存哈希点的结构体是一个 u32 的哈希加一个 u32 的服务器索引,共 8 字节。但 PBR 几乎不可能同时协调超过 2 的 16 次方(约 6.5 万)台服务器,所以索引用 u16 就够了。问题是 Rust 的内存对齐规则要求结构体大小是其最大字段对齐值的整数倍,把索引改成 u16 后结构体仍然占 8 字节。#[repr(packed)] 可以解决,但它”有争议,而且理由充分”。更安全但可读性差一点的做法是直接存一个 6 字节数组 [u8; 6],再写两个 getter 用 from_ne_bytes 分别读出哈希和索引,两种写法编译出来的代码是一样的。这个简单(虽然啰嗦)的改动让一致性哈希的内存占用直接减少了 25%。
第二个优化:其实根本不需要那么多哈希点。 大多数资料只给出每台服务器 k 个哈希点时标准差的近似值或渐近极限,作者自己推导出了精确公式(完整推导放在另一篇补充文章里,他说自己不是统计学家,但”我是由一位微积分老师养大的,嗨,妈妈!”):期望仍为 1/N,SD_k = sqrt((k+1)/(N(kN+1)) − 1/N²),对应的 CV_k = sqrt((N−1)/(Nk+1))。画出曲线就能看到”加更多哈希”这种思路的问题:误差每降一个台阶,哈希点数几乎都要增加一个数量级,收益递减得非常快。以每台 160 个基础点乘以一个 625 的权重因子为例,k = 100,000,而最后加的那 9 万个点只换来了 0.7% 的误差下降。更糟的是,上面的公式假设哈希分布在连续的环上,而实际用的是 32 位整数,点数越多,哈希碰撞的概率就上升得越快(生日悖论)。碰撞意味着某些点的贡献被随机丢弃,引入不可预测的误差。模拟显示,在有 2048 台服务器的数据中心里,每台服务器的哈希点从 1 万增加到 10 万时,误差率反而上升了。结论是:可以把每台服务器生成的哈希点减少 90%,而不会带来任何可察觉的误差。
迁移时不能把源站压垮。 换哈希环会改变部分可缓存请求的去向,如果全网一次性切换,几乎所有缓存内容都会实质上失效,”把一次内存优化变成一场源站流量的末日灾难”。所以他们让 PBR 在一段时间内同时在内存中保留新旧两个环,每个请求通过常规的迁移框架决定用哪个环选后端,这个决定按请求哈希保持稳定,也保留了干净的回滚路径,不用重新部署就能把新请求切回旧环。迁移分层推进:先是小规模验证机房,再到逐步扩大的数据中心组,最后才是全球。关键在于独立控制两个维度:多少流量使用新环,以及这些流量被允许在哪些地方移动。简单的全局百分比灰度会让缓存抖动同时扩散到所有地方,按数据中心灰度则能把影响范围控制得很小。期间他们盯着后端选择链路追踪、环版本计数器、PBR 连接错误、进程内存、启动时间、缓存行为和源站流量。迁移到 100% 后,移除旧环代码路径,内存曲线陡然下降,前后对比正好少了 100TB。
你也可以试试。 所有改动都已进入 pingora-ketama crate,暂时以一个未公开宣传的 cargo feature 形式提供。v2 环使用压缩存储格式、更快的排序方法,并且可以调整每个节点的基础哈希点数;v1 环与原来完全一致,库支持两者同时运行,并按请求决定用哪个。作者最后希望读者从中得到启发,去深挖自己系统里那些”简单”或”显而易见”的决定背后藏着的收益,”你也许没法用 Rust 解决所有问题,但数学是通用的”。
HN 评论精华
110 条评论。讨论大致分成四条线:对文章本身的称赞(特别是”终于读到人写的 Cloudflare 博客了”),对一致性哈希方案的技术质疑和替代方案,关于”软件该不该在乎内存”的老话题,以及顺带的 AI 与就业之争。
“这是人写的。” 不少人第一反应是夸文风。parallax_error 说比最近很多 Cloudflare 博客都更喜欢这篇的写法;variety8675 说”很高兴看到 Cloudflare 在经历了 LLM 垃圾博客的风波后,又让人类来写博客了”;MisterMunchkin 说”读到真正的英语而不是’Claude 腔’很让人耳目一新”。ChoosesBarbecue 把前 1000 词丢进 Pangram(一个 AI 文本检测工具)测了一下,结果判定为人类所写。从语气看应是作者本人的 terabyteoff 回了一句妙语:”AI 会知道在专业文章里写’嗨,妈妈’是个坏主意。”AdamN 接话:”好了,现在你把下一代 AI 训练成会在文章里加’嗨,妈妈’来冒充人类了。”agosta 说很享受文中那段微积分推导,terabyteoff 回复”谢谢!收着点,不然别人会以为我给你钱了”。
sfink 的”公开犯傻”成了本帖最有教益的一段。 他说读的时候以为这是个好玩的脑筋急转弯,但很快觉得问题设定有问题:为什么不直接把任务哈希后对 N 取模?为什么还要对服务器做哈希?”如果你掷 6 个骰子,再掷一个来决定用哪个,并不会得到更多随机性。”然后他洋洋洒洒设计了一整套按权重排序、按 2 的幂分组、增删服务器时后台重建表的方案,最后说”肯定是我漏了什么约束,请告诉我我有多蠢”。procaryote 点破了关键:对服务器做哈希,是为了让增删一台服务器不直接影响其他服务器在环上的位置,新加的服务器只是从某些服务器那里分走一部分负载;这样才能保持”粘性”,同一个键的请求大多落在同一台服务器上。按权重排序的方案则会让一台在列表前面反复上下线的服务器破坏整个集合的粘性。sfink 恍然大悟:”我漏掉的是:各个负载均衡器对服务器集合的视图并不一致,不存在什么神奇的同步机制。你希望对可用服务器集合看法略有不同的负载均衡器,在它们都认可的那些服务器上大多做出相同的选择。”robotresearcher 安慰他这不算蠢,”不需要全局一致状态”这个约束太重要了,排除了太多方案,文章本该明确写出来;而且文中的统计模型其实隐含了单一的全局真相来源,实际中他们很可能并没有。
其他替代方案。 jiggawatts 惊讶于文章和评论都没提到分层的会合哈希(rendezvous hashing),”它就是为这种代理和缓存负载均衡场景专门设计的”。nopurpose 也觉得一定有比存一大张哈希查找表更好的按概率选服务器的方法。vlovich123 更激进,说”我会干掉一致性哈希和 ketama,换一个能再省 600TiB 的方案”:用键哈希的前 N 位选服务器分区(比如每区 128 台),再对服务器名用高质量的预计算哈希做锦标赛式哈希,他认为这样也比”每个键算 160 个哈希”省 CPU。MakersF 纠正了他的误解:每个请求其实只算一次哈希,然后在有序表里做一次 lower_bound 查找,160 乘以权重个哈希是按服务器(按功能组合)预先生成、用来划分哈希空间的。QuaternionsBhop 提出可以用父节点累加子节点权重的 B 树,但也承认这样增删节点可能大幅改变整体映射,”也许这就是为什么要用一致性哈希”。sharktheone 认为还有更多省内存的手段,比如指针压缩。christina97 则不太买账:大科技公司有成队的博士在做整个计算集群的优化,这篇文章的数学更像”看我多酷,会一点微积分”,还没解释清楚为什么要用一致性哈希。goodpoint 的总结更刻薄:”原有实现设计得很差。他们把两个整数打包得更紧凑,省了内存。”agosta 则提醒大家,文章真正的省钱点不在打包,而在于发现不需要那么多哈希点,关键是算出能砍掉多少而不影响负载均衡。
软件该不该在乎内存? zer0x4d 说很高兴看到这一系列文章,怀念 RAM 和算力稀缺时开发者绞尽脑汁的年代,”内存变贵反而开启了一个被迫优化的新时代”。jfengel 不怀念那段日子:”挑战确实有趣,但我真正想要的是把产品交到客户手里。现在我能多花时间想客户要什么,少花时间想计算机要什么。”1vuio0pswjnm7 反问:”要是客户要的就是内存效率呢?”locknitpicker 认为把”占用更少内存”等同于”质量更高”是幼稚的混淆,1% 的改进对大多数软件毫无意义;ffsm8 反驳说关系往往正相反,浪费资源的应用通常正是那些没想清楚、敷衍了事的实现。HappMacDonald 给出了一个更高层次的论点:资源约束历来是高质量工程的堡垒,必须在预算内解决问题会迫使工程师慢下来想清楚,就像排放和节水法规让汽车和马桶的质量大幅提升一样。MisterMunchkin 从商业角度解释了为什么是 Cloudflare 在做这件事:它的产品就是性能,成本主要是硬件和网络;而对汉堡王来说,成本都在租金和人工,他们不会在乎网站用多少内存,但会在供应链和排班上做同样的优化。necovek 则泼了冷水:说到底这是 1% 的改进,对其他大多数软件系统来说不值得投入。
公司筒仓与 AI。 ricardobeat 由此联想:公司什么时候会变成一堆难以穿透的筒仓,什么都不像你预期的那样工作?procaryote 认为诀窍是让筒仓保持松耦合、小到能很快理解,文中这个组件就是好例子:”它只按权重路由流量,不在乎权重代表什么,不在乎服务器在哪,也不在乎流量是什么。拥有它的团队需要理解它,其他人只管用就行。”BobbyTables2 感慨说,想在远程设备上点亮一个指示灯,现在要用到网络、TLS、认证插件、证书校验、分布式日志、容器、编排、HTTP、进程间通信……”电报时代可没有软件!”pixl97 回应:”系统要尽可能简单,但不能更简单。那些东西存在,是因为我们活在一个对抗性的世界里,你要是敢用电报线传钱,我就把它全偷走。”
顺带的几个支线。 swe_dima 问”这是不是意味着内存价格能降了?拜托?”。sroussey 调侃”肯定是有人急需几百 TB 拿去跑推理,才去地毯底下翻”;why_only_15 解释说 CPU 内存没法高效用于推理,推理要的是带宽而不是容量,GPU 显存带宽高出十倍以上。Fordec 认为这类工作说明”你没法靠一次性氛围编程搞定这种事”,真正的软件工程岗位是安全的;killingtime74 反驳说模型数学题做得很好,完全可以用自动研究循环去优化任意程序的内存,两人随后就 AI 对编程岗位和薪资的影响展开了一番长篇辩论。schobi 则想象其他内部团队看到这篇文章后的反应:”省 100TB 就能出风头?看我们的吧!”