Pingora
2024-02-29 14:23:07 阿炯

Pingora 是一个 Rust 异步多线程框架,用于构建可编程的网络服务。在2022年时Cloudflare就宣布放弃Nginx,转而使用 Rust 自行编写的软件 Pingora。2024年,Cloudflare 在GitHub上开源 Pingora 框架,并在 Apachev2 许可证下授权。

Pingora is a Rust framework to build fast, reliable and programmable networked systems. It is battle tested as it has been serving more than 40 million Internet requests per second for more than a few years.


Pingora 长期以来一直在 Cloudflare 内部使用,能够承受大量流量;其开源后有助于在 Cloudflare 外部构建基础设施。官方对 Pingora 框架的描述是:其提供库和 API,用于在 HTTP/1 和 HTTP/2、TLS 或 TCP/UDP 基础上构建服务。作为代理,它支持 HTTP/1 和 HTTP/2 端到端、gRPC 和 websocket 代理。(它还提供可定制的负载平衡和故障转移策略。在合规性和安全性方面,它支持常用的 OpenSSL 和 BoringSSL 库,这两个库都符合 FIPS 标准并支持后量子加密。除了提供这些功能,Pingora 还提供过滤器和回调,允许用户完全自定义服务应如何处理、转换和转发请求。对于 OpenResty 和 nginx用户来说,这些 API 尤为熟悉,因为其中许多 API 都可以直观地映射到 OpenResty 的"*_by_lua"回调上。在操作上,Pingora 提供零停机时间的服务重启,在不丢弃任何一个传入请求的情况下进行自我升级。Syslog、Prometheus、Sentry、OpenTelemetry 和其他必备的可观察性工具也可轻松与 Pingora 集成。

Features

Async Rust: fast and reliable
HTTP 1/2 end to end proxy
TLS over OpenSSL or BoringSSL
gRPC and websocket proxying
Graceful reload
Customizable load balancing and failover strategies
Support for a variety of observability tools


Pingora中还包括一个使用其构建负载平衡器的示例。处于 1.0 前阶段的 Pingora 还没有稳定的 API,Cloudflare 目前也没有计划支持非 Unix 操作系统。许多 Cloudflare 客户和用户使用 Cloudflare 全球网络作为 HTTP 客户端(例如 Web 浏览器、应用程序、物联网设备等)和服务器之间的代理。过去,对于浏览器和其他用户代理如何连接到我们的网络,我们已进行过许多讨论,我们开发了很多技术并实施了新协议(参见 QUIC 和 http2 优化)来使这段连接更高效。

架构的限制损害了性能

Nginx worker(进程)架构对于我们的用例而言存在操作缺陷,这会损害我们的性能和效率。首先在 Nginx 中,每个请求只能由单个 worker 处理。这会导致所有 CPU 内核之间的负载不平衡,从而导致速度变慢。

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

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


但 Nginx 连接池与单个 worker 相对应。当请求到达某个 worker 时,它只能重用该 worker 内的连接。当我们添加更多 Nginx worker 以进行扩展时,我们的连接重用率会变得更差,因为连接分散在所有进程的更多孤立的池中。这导致更慢的 TTFB 以及需要维护更多连接,进而消耗我们和客户的资源。但如果我们能够解决根本问题:worker/进程模型,我们将自然而然地解决所有这些问题。Nginx 纯粹是用 C 语言编写的,这在设计上不是内存安全的。使用这样的第 3 方代码库非常容易出错。即使对于经验丰富的工程师来说,也很容易陷入内存安全问题,我们希望尽可能避免这些问题。用来补充 C 语言的另一种语言是 Lua。它的风险较小,但性能也较差。此外在处理复杂的 Lua 代码和业务逻辑时经常发现自己缺少静态类型。

随着客户群和功能集的持续增长,我们持续评估了三种选择:
1.继续投资Ng并向其付款进行定制,使其完全满足我们的需求。我们拥有所需的专业知识,但鉴于上述架构限制,需要付出大量努力才能以完全支持我们需求的方式重建它。
2.迁移到另一个第三方代理代码库。肯定有好的项目,比如 envoy 和其他一些。但这条道路意味着在几年内可能会重复同样的循环。
3.从头开始建立一个内部平台和框架。这一选择需要在工程方面进行最大的前期投资。

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

Pingora 项目

设计决定

为了打造一个每秒提供数百万次请求且快速、高效和安全的代理,我们必须首先做出一些重要的设计决定。我们选择 Rust 作为项目的语言,因为它可以在不影响性能的情况下以内存安全的方式完成 C 语言可以做的事情。尽管有一些很棒的现成第 3 方 HTTP 库,例如 hyper,我们选择构建自己的库是因为我们希望最大限度地提高处理 HTTP 流量的灵活性,并确保我们可以按照自己的节奏进行创新。

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

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

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

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

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

在生产中更快

Pingora 处理几乎所有需要与源服务器交互的 HTTP 请求(例如缓存未命中),在此过程中收集了很多性能数据。首先看看 Pingora 如何加快我们客户的流量。Pingora 上的总体流量显示,TTFB 中位数减少了 5 毫秒,第 95 个百分位数减少了 80 毫秒。这不是因为我们运行代码更快。甚至我们的旧服务也可以处理亚毫秒范围内的请求。

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


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

更多功能

拥有工程师熟悉的开发人员友好界面,同时消除以前的限制,让我们能够更快地开发更多功能。像新协议这样的核心功能充当我们为客户提供更多产品的基石。如能够在没有重大障碍的情况下向 Pingora 添加 HTTP/2 上游支持。这使我们能够在不久之后向我们的客户提供 gRPC。将相同的功能添加到 Nginx 将需要更多的工程工作,并且可能无法实现。

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

更高效

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

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

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

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

更安全

在我们这样的规模下,快速安全地发布功能十分困难。很难预测在每秒处理数百万个请求的分布式环境中可能发生的每个边缘情况。模糊测试和静态分析只能缓解这么多。Rust 的内存安全语义保护我们免受未定义行为的影响,并让我们相信我们的服务将正确运行。有了这些保证,我们可以更多地关注我们的服务更改将如何与其他服务或客户来源进行交互。我们能够以更高的节奏开发功能,而不用背负内存安全和难以诊断崩溃的问题。

当崩溃确实发生时,工程师需要花时间来诊断它是如何发生的以及是什么原因造成的。自 Pingora 创立以来,我们已经处理了数百万亿个请求,至今尚未因为我们的服务代码而崩溃。事实上,Pingora 崩溃是如此罕见,当我们遇到一个问题时,我们通常会发现不相关的问题。最近,我们的服务开始崩溃后不久,我们发现了一个内核错误。我们还在一些机器上发现了硬件问题,过去排除了由我们的软件引起的罕见内存错误,即使在几乎不可能进行重大调试之后也是如此。

 
另有基于Pingora的反向代理软件:Pingap

Pingap 是基于 Pingora 开发的,采用 ApacheV2.0 协议开源,pingora 提供了各类模块便于 rust 开发者使用,但并不方便非 rust 开发者使用,因此 pingap 提供了以 toml 的形式配置简单易用的反向代理,实现支持多 location 代理转发,通过插件的形式支持更多的需求场景。特性如下:
支持通过 host 与 path 筛选对应的 location
正则形式重写 Path
HTTP 1/2 的全链路支持
基于 TOML 格式的配置,可保存至文件或 etcd
应用配置更新后,无中断式的优雅更新程序
访问日志的模板化配置
WEB 形式的管理后台界面
可通过 let's encrypt 生成 tls 相关证书,简单易用
支持各种事件的推送:lets_encrypt, backend_unhealthy, diff_config, restart 等
丰富的 http 转发插件:compression, static serve, limit, stats, mock, 等。




Location 的处理逻辑

该 Server 下的所有 location 在初始化时根据权重按高至低排序,接受到请求时按顺序一个个匹配到符合的 location 为止,若无符合的则返回出错。在选择对应的 location 之后,判断是否有配置重写 pathh (若无则不需要),添加请求头 (若无则不需要),成功响应时添加响应头 (若无则不需要)。

Location 主要配置请求的匹配、请求头响应头的插入,以及各种插件的关联,是整个流程中的最重要组成部分。下面是相关参数的详细说明:
upstream: 配置该 location 对应的 upstream,若该 location 所有的处理均由插件完成,则可不配置
host: 匹配的域名,如果是多个域名则使用 , 分隔
path: 匹配的路径,具体使用后续细说
proxy_headers: 转发至 upstream 时添加的请求头
headers: 响应至 downstream 时添加的响应头
rewrite: 请求路径的重写规则
proxy_plugins: 添加至该 location 的插件列表,按顺序执行

Location 支持配置对应 host (支持多个)与 path 规则,path 支持以下的规则,权重由高至低:
全等模式,配置以 = 开始,如 =/api 表示匹配 path 等于 /api 的请求
正则模式,配置以 ~ 开始,如 ~^/(api|rest) 表示匹配 path 以 /api 或 /rest 开始请求
前缀模式,如 /api 表示匹配 path 为 /api 开始的请求
空模式,若未指定 path 则表示所有的 path 均匹配,一般建议配置一个 / 的前缀模式即可,无需使用空模式

插件体系

Pingap 中通过 Locaton 添加各种插件支持更多的应用场景,如鉴权、流控等。现在插件支持在 request_filter 与 proxy_upstream_filter 阶段执行,均为转发到上游节点前的处理。下面介绍一下 proxy plunin 的具体逻辑,trait 如下:
#[async_trait]
pub trait ProxyPlugin: Sync + Send {
fn category(&self) -> ProxyPluginCategory;
fn step(&self) -> ProxyPluginStep;
async fn handle(&self, _session: &mut Session, _ctx: &mut State) -> pingora::Result {
Ok(false)
}
}

主要分三个实现:
category: 插件类型,用于区分该插件是哪类形式的插件
step: 插件的执行阶段,现只支持在 request_filter 与 proxy_upstream_filter 阶段执行
handle: 插件的执行逻辑,若返回的是 Ok(true),则表示请求已处理完成,不再转发到上游节点

Upstream 的处理逻辑

Upstream 的逻辑比较简单,在匹配 location 后,根据该 location 配置的 upstream 节点列表,按算法选择可用节点,并将请求转发至该节点即可。注意插件也可配置在 proxy_upstream_filter 转发至 upstream 之前执行,可按需配置对应的插件。其配置为节点地址列表,配置为域名则会根据解析后的 IP 添加所有节点地址(之后并不会再次刷新域名解析),需要注意节点会使用默认的 tcp health check 的形式检测节点是否可用,建议配置为 http health chech。下面针对相关参数详细说明:
addrs: 节点地址列表,地址为 ip:port weight 的形式,weight 权重可不指定,默认为 1
algo: 节点的选择算法,支持 hash 与 round_robin 两种形式,如 hash:ip 表示按 ip hash 选择节点。默认为 round_robin
sni: 若配置的是 https,需要设置对应的 SNI
verify_cert: 若配置的是 http,是否需要校验证书有效性
health_check: 节点健康检测配置,支持 http 与 tcp 形式
ipv4_only: 若配置为域名时,是否仅添加解析的 ipv4 节点
alpn: 在 tls 握手时,alpn 的配置,默认为 H1
connection_timeout: tcp 连接超时,默认为无
total_connection_timeout: 连接超时,对于 https 包括 tls 握手部分,默认为无
read_timeout: 读取超时,默认为无
write_timeout: 写超时,默认为无
idle_timeout: 空闲超时,指定连接空闲多久后会自动回收,如果设置为 0,则连接不复用,需要注意有些网络设备对于无数据的 tcp 连接会过期自动关闭,因此可根据需要设置对应的值。默认为无


最新版本:


项目主页:
https://github.com/cloudflare/pingora