资讯动态

一个函数吃掉了 680 个 CPU 核心——Cloudflare 是怎么用自研 Trie 把它抢回来的

发布时间:2026/8/27 10:45:47 来源:尧图企业网站定制
有些性能问题藏在最不起眼的地方。Cloudflare 的工程师在审查 pingora-origin 的代码时看到了一个注释// PERF: heavy function: 1.7% CPU timepubfnclear_internal_headers(request_header:mutRequestHeader){INTERNAL_HEADERS.iter().for_each(|h|{request_header.remove_header(h);});}函数只有三行逻辑极其简单——遍历一个内部 Header 列表从 HTTP 请求里把它们删掉。每个离开 Cloudflare 网络的请求在转发给源站之前都必须经过这个函数清理掉内部用于路由和监控的请求头不能让它们泄漏到外部。这个函数吃掉了 pingora-origin 总算力的1.71%。把这个数字换算一下pingora-origin 在全球相当于 40000 个饱和 CPU 核心在运行1.71% 意味着有680 个核心在任何时刻都只用来执行这三行代码。先量化再动手在修改任何代码之前团队的第一步是建立精确的基线测量。他们使用Criterion——Rust 生态里事实上的标准微基准测试框架可以把函数的执行时间精确到纳秒级并在多次运行中统计聚合过滤掉测量噪声。基准测试的输入是一批合成请求每个请求包含随机数量的 Header内部 Header 和普通 Header 按均匀分布混合模拟真实流量的特征。测量结果原始实现平均需要3.65μs。有了基线后续每一轮优化都可以和它直接对比不靠感觉只看数字。第一轮翻转查找方向仔细看原始代码它做的事情是遍历所有内部 Header100 多个对每一个都去请求里执行一次删除操作。遍历方向内部 Header 列表100→ 在请求里查找并删除但一个正常的 HTTP 请求只有 10 到 30 个 Header。两个集合取交集迭代较小的那个明显更划算pubfnclear_internal_headers(request_header:mutRequestHeader){letto_removerequest_header.headers.keys().filter_map(|name|INTERNAL_HEADER_SET.get(name)).collect::Vec_();to_remove.into_iter().for_each(|k|{request_header.remove_header(k);});}新方向请求 Header10-30 个→ 判断是否在内部集合里改动只有几行但效果出乎意料地显著函数执行时间从3.65μs 降到 1.53μs快了2.39 倍。预测 CPU 占用1.71% × 1.53 / 3.65 ≈ 0.72%。也就是说这一步能省掉约 1% 的总 CPU——这已经不错了但团队认为还能做得更好。第二轮换数据结构从理论开始翻转查找方向之后INTERNAL_HEADER_SET这个集合的数据结构选型就变得关键了——每个请求的每个 Header 名都要在它里面查一次。先从理论上整理各个选项HashMap大家的默认选择查找复杂度是 O(1)但这个 O(1) 有代价——计算哈希值需要读完整个键的每一个字节所以实际复杂度是 O(L)L 是键的长度。BTreeSet / FST有限状态转换器基于比较的有序集合复杂度是 O(log L)听起来更好。但实测中FST 比标准 HashMap 慢了约 50ns原因是常数因子不占优势。正则表达式正则本质上是一个状态机每次只读一个字节遇到不匹配的字符立刻终止特别适合大多数时候是负例的场景。实测比 HashMap 快了约一倍这很令人惊喜——但还不够。Trie前缀树理论上的正确方向Trie 是一种树形数据结构每个节点代表一个字符前缀边代表下一个可能出现的字符。以存储 “and”、“ant”、“dad”、“do”、“dot” 为例root / \ a d / / \ n a o / \ | / \ d t d t .从根节点开始如果输入的第一个字母不是 a 或 d立刻就知道这个字符串不在集合里——不需要继续往下看。这正是 Trie 最有价值的性质负例查找是 O(log L)平均读几个字节就能排除。而我们的场景中一个请求里超过 90% 的 Header 都不是内部 Header负例才是主路径。命中的情况仍然是 O(L)但命中的 Header 不超过 10%均摊下来性能会大幅改善。现有 Trie 库全部不够用理论上找到了正确方向接下来在 crates.io 上找现成的 Trie 实现来测试。结论令人沮丧测试过的最快实现是radix_trie但它比 HashMap 慢了整整1μs。原因很清楚crates.io 上的 Trie 库几乎都是为自动补全、前缀搜索等场景设计的——这些场景的调用频率是键盘敲击速度而不是每秒 3500 万次请求。为这种极端热路径做优化不在这些库的设计目标里。唯一的出路自己写一个。trie-hard为热路径而生的前缀树Cloudflare 开发了trie-hard核心设计思路是两点节点关系存在无符号整数的位里传统 Trie 用指针连接子节点每次跳转都可能发生缓存未命中。trie-hard 把每个节点的子节点信息编码到整数的比特位上通过位运算直接计算位置关系消除了指针追踪。整棵树存储在连续内存里传统 Trie 的节点散布在堆上访问模式是随机的对 CPU 缓存极其不友好。trie-hard 把所有节点压缩进一块连续的内存区域充分利用缓存局部性让树的遍历基本上是顺序内存访问。两个设计合力的效果实现执行时间CPU 占比预测原始每次遍历 100 内部 Header3.65μs1.71%基线翻转方向 HashMap1.53μs0.72%trie-hard0.93μs0.43%节省1.71% - 0.43% 1.28%的总 CPU。目标是节省 1%实际超出了预期。生产数据验证预测和现实吻合所有的基准测试最终都要在真实流量下接受检验。trie-hard 从 2024 年 7 月开始在生产环境运行。在整个优化过程中团队使用了采样式栈追踪statistical stack sampling——定期对 pingora-origin 进程进行栈快照统计每个函数在快照中出现的频率以此估算其 CPU 占用比例。结果与预测高度吻合实现采样命中实际 CPU 占用预测 CPU 占用原始19 / 11111.71%—HashMap翻转方向9 / 11030.82%0.72%trie-hard4 / 11710.34%0.43%实际效果甚至比预测略好——最终 CPU 占用从 1.71% 降到了0.34%降幅超过 80%。这件事真正的启示博客作者在结尾说了一句让人印象深刻的话知道代码慢在哪里、慢了多少比知道怎么优化它更重要。这次优化的起点是那行注释里写着的 “1.7% CPU time”。如果没有火焰图没有精确的采样数据这个函数就会一直看起来没问题地存在。整个优化过程的逻辑链非常清晰用 Criterion 建立精确基线而不是靠感觉先从算法层面找低成本的改进翻转查找方向快了 2.4 倍再从数据结构层面系统性评估选项每个都测用数据说话现有库不满足需求时自己写然后开源上线后用生产采样数据验证而不是只信本地 benchmark在每秒 3500 万请求的规模下3μs 的节省不是一个小数字。这次优化实际释放的算力相当于数百个 CPU 核心可以用来承载更多用户的流量而不是去清理 HTTP Header。原文链接https://blog.cloudflare.com/pingora-saving-compute-1-percent-at-a-time/trie-hard 开源地址https://github.com/cloudflare/trie-hard

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价