谷歌的 HTTP/2 编解码器减慢了 Envoy
工程 2026年8月4日 8分钟阅读 Envoy 的版本更新让一个客户的 HTTP/2 吞吐量下降了 20%。追踪线索经过两个编解码器,一堆火焰图,以及每个头部中隐藏的哈夫曼编码。Dmitry Ilyevsky 联合创始人兼首席技术官 几天前,我们对客户的专用代理进行了例行的 Envoy 升级。相同的配置,相同的流量,CPU 图表大约上升了五分之一。这种图表会让你收到一封没有议程的日历邀请,所以我们决定提前调查一下,开始进行深入挖掘。 二分查找版本指向 Envoy v1.34,这是 Envoy 将其默认 HTTP/2 编解码器从 nghttp2 切换到谷歌的 oghttp2 的地方。我们不是第一个注意到这一点的人:用户自该版本发布以来就报告了 15–25% 的延迟回归,而 v1.37.0 最终将默认设置改回,源代码中的评论承诺一旦性能与 nghttp2 对齐将再试。这解决了我们客户的问题,但没有解决我们的好奇心。为什么不会对齐?一个 HTTP/2 编解码器究竟花费时间做什么?这篇文章就是调查的结果。接下来会有一个续集,我们将使快速编解码器更快,但首先是第一步。 编解码器中的最佳与最差 Envoy 在运行时标志下有两个 HTTP/2 实现,尽管没有人能在没有火焰图的情况下说出哪个是哪个: nghttp2 - Tatsuhiro Tsujikawa 的 C 库,自 2013 年以来一直是 HTTP/2 的主力军,也是 Envoy 的原始编码器。紧凑的 C 结构体,呼叫者提供的缓冲区。 oghttp2 - 谷歌的 C++ 编解码器,来自 QUICHE 家族,与 HTTP/3 堆栈共享代码。它在 v1.34 中成为 Envoy 的默认值,这也是回归报告开始的时候。 我们在四个微架构上对两者进行了基准测试(Intel Sapphire Rapids、AMD Zen 4、AWS Graviton4、Google Axion)。设置简要说明:同主机回环(h2load → Envoy → Go h2c 后端,或者 Envoy 提供 direct_response),每个进程固定在不重叠的物理核心上,Envoy 以 --concurrency 1 运行,因此 RPS/core 是一个纯粹的 CPU 效率数字。用户报告几乎完全重现:在每个主机上的头部重载代理流量中,nghttp2 在 RPS/core 上比 oghttp2 快 15–25%,而在连接大量变动的情况下,比 oghttp2 快 7–19%。因此,回归是实在的,可移植的,并且在编解码器的某处存在。是时候打开分析文件了。 PSA 如果你自己构建 Envoy,请检查你的优化标志。Envoy 的 .bazelrc 默认并不是 -c opt - 一个普通的 bazel build //source/exe:envoy-static 生成的快速构建(调试)二进制文件与同一源代码的 -c opt 版本相比,每个核心慢 15–32 倍。本篇文章中的每一个数字均来自 -c opt 构建。我们以尴尬的方式发现了这一点。 HTTP/2 编解码器的日常开销 这两个编解码器的火焰图主要被同一工作占据:头部解压缩。每个跨越代理的 HTTP/2 请求都会带着压缩的头部,代理必须解开这些头部 - 每个跳跃解压缩和重新编码两次 - 然后才能路由任何信息。每个请求需要几百字节的工作,看起来似乎不算什么,直到你把它乘以:每个核心每秒数万个请求,以及每个请求十几个头字段,因此每字段的解码路径每秒运行几十万次。压缩方案为 HPACK (RFC 7541):两种机制共同工作。第一种是索引:一个固定的 61 条目静态表覆盖通用头部(:method: GET 是一个字节),双方都维护同步的动态表,记录最近见到的头字段——因此第二次你发送 cookie: session=abc... 时,它耗费的只有一个或两个字节,而不是字面上的千字节。第二种机制处理所有无法成为索引命中项的内容:字面字符串通过哈夫曼编码。首次出现的头部、唯一值 (请求 ID、令牌、每个请求广告供应商轮换的 cookie) ,以及大多数穿过新连接的内容都通过哈夫曼路径。位于边缘的代理看到很多新连接和很多唯一值,因此这条路径非常热门。 哈夫曼编码的两分钟游览 1951 年,罗伯特·法诺 (Robert Fano) 给麻省理工学院的信息理论课程的学生们提出了一个选择:参加期末考试,或就他提供的一个问题写一篇学期论文。其中一个问题听起来几乎太简单了,无法值得给分——给定一组符号及其出现的频率,找到它们最有效的二进制编码。法诺没有提到他和克劳德·香农 (Claude Shannon) (是的,就是香农)都曾尝试解决这一确切问题,并且只得到近似解。一个名叫大卫·哈夫曼 (David Huffman) 的研究生选择了这个论文,挣扎了几个月,正如他所说的,在找到解决方案之前,刚把笔记扔进垃圾桶,开始为考试复习。所有在他之前的人都从上到下构建代码,将符号集一分为二。哈夫曼进行了反转:从底部开始——取两个最稀有的符号,将它们合并为一个,重复直到剩下一个树。稀有符号最终深藏在树中,拥有较长的编码;频繁符号则靠近树的顶部,拥有较短的编码。
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡