Pingora 是 Cloudflare 使用 Rust 在内部构建的新 HTTP 代理,它每天处理超过 1 万亿个请求,提高了 Cloudflare 的性能,并为 Cloudflare 客户带来了许多新功能,同时只需要 Cloudflare 以前代理基础架构的三分之一的 CPU 和内存资源。

Pingora是什么  第1张

随着 Cloudflare 规模的扩大,Cloudflare 已经超越了 NGINX 的处理能力。多年来它一直运作良好,但随着时间的推移,它在 Cloudflare 规模上的局限性意味着 Cloudflare 有必要构建一些新的东西。Cloudflare 无法再获得 Cloudflare 所需要的性能,NGINX 也没有 Cloudflare 在非常复杂的环境中所需要的功能。

许多 Cloudflare 客户和用户使用 Cloudflare 全球网络作为 HTTP 客户端(例如 Web 浏览器、应用程序、物联网设备等)和服务器之间的代理。过去,对于浏览器和其他用户代理如何连接到 Cloudflare 的网络,Cloudflare 已进行过许多讨论,Cloudflare 开发了很多技术并实施了新协议(参见 QUIC 和 http2 优化)来使这段连接更高效。

为什么要再建一个代理

这些年来,Cloudflare 对 NGINX 的使用遇到了限制。对于部分限制,Cloudflare 进行了优化或选择绕过它们。但另一些限制则更难克服。

架构的限制损害了性能

NGINX worker(进程)架构对于 Cloudflare 的用例而言存在操作缺陷,这会损害 Cloudflare 的性能和效率。

首先,在 NGINX 中,每个请求只能由单个 worker 处理。这会导致所有 CPU 内核之间的负载不平衡,从而导致速度变慢。

由于这种请求进程锁定效应,执行 CPU 繁重或阻止 IO 任务的请求可能会减慢其他请求的速度。正如这些博客文章所表明的那样,Cloudflare 已经花了很多时间来解决这些问题。

对于 Cloudflare 的用例来说,最关键的问题是糟糕的连接重用。Cloudflare 的机器与原始服务器建立 TCP 连接,以代理 HTTP 请求。连接重用通过重用之前从连接池建立的连接,跳过新连接所需的 TCP 和 TLS 握手,来加快请求的 TTFB(首字节时间)。

但是,NGINX 连接池与单个 worker 相对应。当请求到达某个 worker 时,它只能重用该 worker 内的连接。当 Cloudflare 添加更多 NGINX worker 以进行扩展时,Cloudflare 的连接重用率会变得更差,因为连接分散在所有进程的更多孤立的池中。这导致更慢的 TTFB 以及需要维护更多连接,进而消耗 Cloudflare 和客户的资源(和金钱)。

正如在过去的博客文章中所提到的,Cloudflare 为其中一些问题提供了解决方法。但如果 Cloudflare 能够解决根本问题:worker/进程模型,Cloudflare 将自然而然地解决所有这些问题。

有些类型的功能难以添加

NGINX 是一个非常好的 Web 服务器、负载均衡器或简单的网关。但 Cloudflare 的作用远不止于此。Cloudflare 过去常常围绕 NGINX 构建 Cloudflare 需要的所有功能,但要尽量避免与 NGINX 上游代码库有太多分歧,这并不容易。

例如,当重试请求/请求失败时,有时 Cloudflare 希望将请求发送到具有不同请求标头集的不同源服务器。但 NGINX 并不允许执行此操作。在这种情况下,Cloudflare 需要花费时间和精力来解决 NGINX 的限制。

同时,Cloudflare 被迫使用的编程语言并没有帮助缓解这些困难。NGINX 纯粹是用 C 语言编写的,这在设计上不是内存安全的。使用这样的第 3 方代码库非常容易出错。即使对于经验丰富的工程师来说,也很容易陷入内存安全问题,Cloudflare 希望尽可能避免这些问题。

Cloudflare 用来补充 C 语言的另一种语言是 Lua。它的风险较小,但性能也较差。此外,在处理复杂的 Lua 代码和业务逻辑时,Cloudflare 经常发现自己缺少静态类型。而且 NGINX 社区也不是很活跃,开发往往是“闭门造车”。

选择建立 Cloudflare 自己的

在过去的几年里,随着 Cloudflare 的客户群和功能集的持续增长,Cloudflare 持续评估了三种选择:

继续投资 NGINX,向其付款进行定制,使其 100% 满足 Cloudflare 的需求。Cloudflare 拥有所需的专业知识,但鉴于上述架构限制,需要付出大量努力才能以完全支持 Cloudflare 需求的方式重建它。

迁移到另一个第三方代理代码库。肯定有好的项目,比如 envoy 和其他一些。但这条道路意味着在几年内可能会重复同样的循环。

从头开始建立一个内部平台和框架。这一选择需要在工程方面进行最大的前期投资。

在过去的几年中,Cloudflare 每个季度都会对这些选项进行评估。没有明显的公式来判断哪种选择是最好的。在几年的时间里,Cloudflare 继续走阻力最小的道路,继续增强 NGINX。然而,在某些情况下,建立自有代理的投资回报率似乎更值得。Cloudflare 呼吁从头开始建立一个代理,并开始设计 Cloudflare 梦想中的代理应用程序。

Pingora 项目

设计决定

为了打造一个每秒提供数百万次请求且快速、高效和安全的代理,Cloudflare 必须首先做出一些重要的设计决定。

Cloudflare 选择 Rust 作为项目的语言,因为它可以在不影响性能的情况下以内存安全的方式完成 C 语言可以做的事情。

尽管有一些很棒的现成第 3 方 HTTP 库,例如 hyper,Cloudflare 选择构建自己的库是因为 Cloudflare 希望最大限度地提高处理 HTTP 流量的灵活性,并确保 Cloudflare 可以按照自己的节奏进行创新。

在 Cloudflare,Cloudflare 处理整个互联网的流量。Cloudflare 必须支持许多奇怪且不符合 RFC 的 HTTP 流量案例。这是 HTTP 社区和 Web 中的一个常见困境,在严格遵循 HTTP 规范,和适应潜在遗留客户端或服务器的广泛生态系统的细微差别之间存在矛盾和冲突,需要在其中作出艰难抉择。

HTTP 状态码在 RFC 9110 中定义为一个三位整数,通常预期在 100 到 599 的范围内。Hyper 就是这样一种实现。但是,许多服务器支持使用 599 到 999 之间的状态代码。Cloudflare 为此功能创建了一个问题,探讨了争论的各个方面。虽然 hyper 团队最终确实接受了这一更改,但他们有充分的理由拒绝这样的要求,而这只是 Cloudflare 需要支持的众多不合规行为案例之一。

为了满足 Cloudflare 在 HTTP 生态系统中的地位要求,Cloudflare 需要一个稳健、宽容、可定制的 HTTP 库,该库可以在互联网的各种风险环境中生存,并支持各种不合规的用例。保证这一点的最佳方法就是实施 Cloudflare 自己的架构。

下一个设计决策关于 Cloudflare 的工作负载调度系统。Cloudflare 选择多线程而不是多处理,以便轻松共享资源,尤其是连接池。Cloudflare 认为还需要实施工作窃取来避免上面提到的某些类别的性能问题。Tokio 异步运行时结果非常适合 Cloudflare 的需求。

最后,Cloudflare 希望 Cloudflare 的项目直观且对开发人员友好。Cloudflare 构建的不是最终产品,而是应该可以作为一个平台进行扩展,因为在它之上构建了更多的功能。Cloudflare 决定实施一个类似于 NGINX/OpenResty 的基于“请求生命周期”事件的可编程接口。例如,“请求过滤器”阶段允许开发人员在收到请求标头时运行代码来修改或拒绝请求。通过这种设计,Cloudflare 可以清晰地分离 Cloudflare 的业务逻辑和通用代理逻辑。之前从事 NGINX 工作的开发人员可以轻松切换到 Pingora 并迅速提高工作效率。

Pingora 在生产中更快

让 Cloudflare 快进到现在。Pingora 处理几乎所有需要与源服务器交互的 HTTP 请求(例如缓存未命中),Cloudflare 在此过程中收集了很多性能数据。

首先,让 Cloudflare 看看 Pingora 如何加快 Cloudflare 客户的流量。Pingora 上的总体流量显示,TTFB 中位数减少了 5 毫秒,第 95 个百分位数减少了 80 毫秒。这不是因为 Cloudflare 运行代码更快。甚至 Cloudflare 的旧服务也可以处理亚毫秒范围内的请求。

时间节省来自 Cloudflare 的新架构,它可以跨所有线程共享连接。这意味着更好的连接重用率,在 TCP 和 TLS 握手上花费的时间更少。

在所有客户中,与旧服务相比,Pingora 每秒的新连接数只有三分之一。对于一个主要客户,它将连接重用率从 87.1% 提高到 99.92%,这将新连接减少了 160 倍。更直观地说,通过切换到 Pingora,Cloudflare 每天为客户和用户节省了 434 年的握手时间。

更多功能

拥有工程师熟悉的开发人员友好界面,同时消除以前的限制,让 Cloudflare 能够更快地开发更多功能。像新协议这样的核心功能充当 Cloudflare 为客户提供更多产品的基石。

例如,Cloudflare 能够在没有重大障碍的情况下向 Pingora 添加 HTTP/2 上游支持。这使 Cloudflare 能够在不久之后向 Cloudflare 的客户提供 gRPC。将相同的功能添加到 NGINX 将需要更多的工程工作,并且可能无法实现。

最近,Cloudflare 宣布推出了 Cache Reserve,其中 Pingora 使用 R2 存储作为缓存层。随着 Cloudflare 向 Pingora 添加更多功能,Cloudflare 能够提供以前不可行的新产品。

更高效

在生产环境中,与 Cloudflare 的旧服务相比,Pingora 在相同流量负载的情况下,消耗的 CPU 和内存减少了约 70% 和 67%。节省来自几个因素。

与旧的 Lua 代码相比,Cloudflare 的 Rust 代码运行效率更高。最重要的是,它们的架构也存在效率差异。例如,在 NGINX/OpenResty 中,当 Lua 代码想要访问 HTTP 头时,它必须从 NGINX C 结构中读取它,分配一个 Lua 字符串,然后将其复制到 Lua 字符串中。之后,Lua 还对其新字符串进行垃圾回收。在 Pingora 中,它只是一个直接的字符串访问。

多线程模型还使得跨请求共享数据更加高效。NGINX 也有共享内存,但由于实施限制,每次共享内存访问都必须使用互斥锁,并且只能将字符串和数字放入共享内存。在 Pingora 中,大多数共享项目可以通过原子引用计数器后面的共享引用直接访问。

如上所述,CPU 节省的另一个重要部分是减少了新的连接。与仅通过已建立的连接发送和接收数据相比,TLS 握手成本显然更为高昂。

更安全

在 Cloudflare 这样的规模下,快速安全地发布功能十分困难。很难预测在每秒处理数百万个请求的分布式环境中可能发生的每个边缘情况。模糊测试和静态分析只能缓解这么多。Rust 的内存安全语义保护 Cloudflare 免受未定义行为的影响,并让 Cloudflare 相信 Cloudflare 的服务将正确运行。

有了这些保证,Cloudflare 可以更多地关注 Cloudflare 的服务更改将如何与其他服务或客户来源进行交互。Cloudflare 能够以更高的节奏开发功能,而不用背负内存安全和难以诊断崩溃的问题。

当崩溃确实发生时,工程师需要花时间来诊断它是如何发生的以及是什么原因造成的。自 Pingora 创立以来,Cloudflare 已经处理了数百万亿个请求,至今尚未因为 Cloudflare 的服务代码而崩溃。

事实上,Pingora 崩溃是如此罕见,当 Cloudflare 遇到一个问题时,Cloudflare 通常会发现不相关的问题。最近,Cloudflare 的服务开始崩溃后不久,Cloudflare 发现了一个内核错误。Cloudflare 还在一些机器上发现了硬件问题,过去排除了由 Cloudflare 的软件引起的罕见内存错误,即使在几乎不可能进行重大调试之后也是如此。

总结

Cloudflare 已经建立了一个更快、更高效、更通用的内部代理,作为 Cloudflare 当前和未来产品的平台。

Cloudflare 之后将介绍有关 Cloudflare 面临的问题和应用优化的更多技术细节,以及 Cloudflare 从构建 Pingora 并将其推出以支持互联网的重要部分的经验教训。同时还将介绍 Cloudflare 的开源计划。