每天有超过一万亿次 HTTP 请求,在 Cloudflare 的全球网络和各地源站服务器之间流动。
这中间有一层代理,负责接收每一个缓存未命中的请求,转发给对应的源站,再把响应送回来。CDN、Workers、Tunnel、Stream、R2——Cloudflare 的大量核心产品,都依赖这一层代理正常工作。
2022 年,Cloudflare 宣布这层代理已经悄悄换掉了。新的系统叫 Pingora,用 Rust 从零构建,处理同等流量只需要原来三分之一的 CPU 和内存,同时还带来了显著的性能提升。
而替换掉的那个旧系统,叫 NGINX。
NGINX 用了很多年,为什么不够用了
NGINX 是一个经过时间检验的成熟项目,在绝大多数场景里表现出色。Cloudflare 使用它多年,也基于它做了大量的定制和优化。但随着业务规模持续增长,一些根本性的架构问题开始变得无法回避。
进程模型带来的连接池碎片化
NGINX 采用多进程架构,每个 Worker 进程独立处理请求。这个模型有一个内在的问题:连接池是按进程隔离的。
当 Cloudflare 的边缘节点要把请求转发给源站时,会复用已有的 TCP 连接,这样可以跳过 TCP 握手和 TLS 握手,大幅减少延迟。但在 NGINX 里,一个请求落在哪个 Worker 进程,就只能复用那个进程自己的连接池。随着 Worker 数量增加,连接被分散在越来越多的独立池子里,整体的连接复用率反而越来越低。
连接复用率低,意味着需要更频繁地建立新连接。TLS 握手的开销相当可观,这个问题在规模足够大的时候,会直接体现为延迟上升和资源浪费。
除此之外,进程间的负载也很难均衡。CPU 密集型任务或者阻塞 IO,会拖慢同一个 Worker 进程里的其他请求,影响范围无法隔离。
复杂功能难以实现
Cloudflare 要做的事情,远不止是一个普通的负载均衡器或网关。当业务需要某些 NGINX 原生不支持的行为时——比如在重试请求时修改请求头,然后发往不同的源站——工程师们不得不在 NGINX 的约束下绕路实现,这消耗了大量工程资源,同时让代码越来越难以维护。
语言本身的限制
NGINX 的核心是 C,内存安全完全依赖开发者的自律。在如此复杂的代码库里,内存问题很难完全规避,Cloudflare 历史上也曾发生过解析器 bug 导致内存泄漏的事故。
为了补充功能,Cloudflare 大量使用了 Lua(通过 OpenResty)。Lua 风险低一些,但性能有明显上限,而且缺乏静态类型,复杂业务逻辑写起来容易出错、难以维护。
三条路,一个不容易的选择
意识到问题之后,Cloudflare 工程团队面临三个选项:
选项一:继续投入 NGINX,甚至 Fork 一个自己的版本。 团队有足够的技术积累,但 NGINX 的进程模型是架构层面的根本限制,在这个基础上做深度改造,工程量巨大,且改造完的结果基本上已经是另一个东西了。
选项二:迁移到现有的第三方代理,比如 Envoy。 Envoy 是一个优秀的项目,Dropbox 就做过类似的迁移。但这条路意味着把自己的核心基础设施的演进节奏,交给另一个社区决定,几年后可能面临同样的问题。
选项三:从零开始,自己构建。 前期工程投入最大,但一旦做成,基础设施完全在自己掌控之下,可以按需演进。
Cloudflare 连续几个季度评估这三个选项,最终在权衡了投入和收益之后,选择了第三条路——从零构建 Pingora。
Pingora 的核心设计决策
为什么选 Rust
Rust 是当时少数几个能在不牺牲性能的前提下提供内存安全保证的语言。相比 C,Rust 的编译器会在编译期拦截大量潜在的内存错误;相比 Go,Rust 没有 GC 暂停,更适合延迟敏感的代理场景。
选 Rust 的决定不只是语言偏好,而是对一个核心工程目标的回应:在互联网规模下安全地、快速地迭代。
为什么自研 HTTP 库
Rust 生态里有成熟的 HTTP 库,比如 hyper。但 Cloudflare 处理的是真实互联网上的全量流量,里面充斥着各种不符合 RFC 规范的边界情况。
一个典型的例子:HTTP 状态码按规范应在 100 到 599 之间,但实际上很多服务器会返回 600 到 999 之间的状态码。对于 hyper 这类严格遵循规范的库来说,这些请求可能直接被拒绝,而 Cloudflare 必须能够处理它们。
类似的边界情况不是少数,而是系统性存在。为了确保对这些情况的完整控制权,团队决定自己实现 HTTP 处理层。
多线程 + work stealing,解决进程模型的根本问题
Pingora 选择多线程模型,而非 NGINX 的多进程模型。所有线程共享同一个连接池,一个线程建立的连接,其他线程可以直接复用,彻底解决了连接池碎片化的问题。
同时引入了 work stealing 调度机制:当一个线程的任务队列空了,可以主动"偷"其他线程的任务来执行,避免负载不均。底层的异步运行时使用了 Tokio,其调度器正好原生支持 work stealing,契合度很高。
类 OpenResty 的事件钩子接口
Pingora 设计了一套基于请求生命周期的可编程接口,开发者可以在请求的不同阶段注入自定义逻辑——比如在收到请求头时执行过滤,在转发前修改请求,在收到响应后做处理。
这个设计思路来自 NGINX/OpenResty,对原来团队里熟悉 OpenResty 的工程师来说几乎没有学习成本。业务逻辑和通用代理逻辑通过钩子分离,让代码结构更清晰,也更容易独立演进。
生产环境的真实表现
延迟:握手时间省出来了
Pingora 上线后,整体流量的 TTFB(首字节时间)中位数降低了 5ms,P95 降低了 80ms。
这个提升不是因为 Pingora 的代码执行更快——原来的系统在请求处理上本来就能做到亚毫秒级。性能改善的核心来源是连接复用率的提升:更少的新连接,意味着更少的 TLS 握手,而 TLS 握手恰恰是延迟的大头。
数字层面:Pingora 建立新连接的频率,只有原来系统的三分之一。对某个主要客户来说,连接复用率从 87.1% 提升到了 99.92%,新建连接数减少了 160 倍。
用一个更直观的说法:每天,Pingora 为 Cloudflare 的客户节省了相当于 434 年的握手等待时间。
资源消耗:同等流量下省了 70% 的 CPU
在生产环境中,处理同等流量,Pingora 比旧系统消耗的 CPU 少约 70%,内存少约 67%。
节省来自多个方向:
Rust 代码本身的运行效率比 Lua 高。以访问 HTTP 头部为例,在 NGINX/OpenResty 里,Lua 代码要读取 NGINX 的 C 结构体,分配一个 Lua 字符串并复制内容,之后还要 GC 回收。在 Pingora 里,直接就是一次字符串引用,没有额外分配和复制。
多线程模型下的共享数据访问也更高效。NGINX 的共享内存需要互斥锁保护,而 Pingora 大多数共享数据通过原子引用计数直接访问,开销小得多。
建立更少的新连接,也直接减少了 TLS 握手的 CPU 开销,这部分节省相当可观。
安全性:数百万亿请求,零次服务代码崩溃
Rust 的内存安全保证,在生产环境里得到了直接验证。Pingora 上线以来,处理了数百万亿次请求,没有一次崩溃是由服务自身代码引起的。
偶发的崩溃反而成了排查其他问题的线索。有一次崩溃事件,最终追踪到了一个 Linux 内核 bug;另外几次,发现了硬件故障。在旧系统里,类似的崩溃很难判断是软件 bug 还是其他原因,调试过程耗时费力。Rust 把软件层面的不确定性消除之后,异常信号变得更纯粹,反而帮助团队更快定位问题根因。
这个案例说明了什么
Pingora 的故事,表面上是一次技术栈的替换,但背后折射出几个值得借鉴的工程判断。
规模会让原来不是问题的东西变成真正的问题。 NGINX 的进程模型在中小规模下运转良好,Cloudflare 在很长一段时间里也通过各种补丁和优化让它继续工作。但连接池碎片化这个问题,不是靠优化能根治的,它是架构层面的结构性缺陷,只有换掉才能解决。
"继续修修补补"和"从零重建"之间,没有一个通用的最优解。 Cloudflare 连续几个季度评估这个决策,最终做出选择的依据是投入产出比在某个时间点翻转了——而不是因为 NGINX 突然变坏,或者某个新技术突然变好。这种基于长周期观察做出的判断,比任何技术潮流的追随都要扎实。
语言选择是一个工程决策,不是品味问题。 选 Rust 的原因不是因为 Rust 时髦,而是因为它在内存安全和性能这两个维度同时满足了需求。生产环境里零次服务代码崩溃,是对这个选择最有说服力的验证。
原文链接:blog.cloudflare.com/how-we-buil… Pingora 已于 2024 年开源:github.com/cloudflare/…