作为一个对 Rust 语言和新兴技术充满兴趣的开发者,我最近了解到一个令人振奋的项目------Pingora。
这是Cloudflare使用Rust构建的全新HTTP代理,意在替代Nginx。Pingora每天处理超过1万亿个请求,不仅大幅提升了性能,还为客户带来了许多新功能,同时只需以前基础架构的三分之一CPU和内存资源。本文将为大家介绍Pingora的设计理念以及它所带来的巨大优势。
为什么Cloudflare需要一个新的代理?
随着Cloudflare的快速扩展,现有的NGINX代理在处理能力和功能方面出现了局限性。虽然NGINX多年来运作良好,但在Cloudflare的大规模应用中,仍存在一些难以解决的问题:
-
- 架构限制损害性能:NGINX的worker架构导致CPU负载不平衡,进而影响整体性能。
-
- 糟糕的连接重用:NGINX的连接池与单个worker绑定,导致连接重用率低,影响请求首字节时间(TTFB)。
-
- 功能扩展困难:NGINX在添加复杂功能时存在局限性,且其用C语言编写存在内存安全问题。
构建自有代理的决策过程
面对这些挑战,Cloudflare评估了三种解决方案:
-
- 继续投资NGINX,并进行定制化改造。
-
- 迁移到其他第三方代理代码库,如Envoy。
-
- 从头开始构建一个内部平台和框架。
最终,Cloudflare决定从头开始构建一个适合其需求的新代理系统------Pingora。Pingora的设计不仅解决了NGINX的架构缺陷,还大大提升了性能和效率。
Pingora项目的设计决策
为了打造一个高效、安全且易于扩展的代理系统,Cloudflare做出了一些关键设计决策:
-
- 选择Rust语言:Rust提供内存安全特性,同时具备C语言的高性能,是构建高效代理系统的理想选择。
-
- 自建HTTP库:为了最大化处理HTTP流量的灵活性,Cloudflare选择构建自己的HTTP库,而不是使用现成的第三方库。
-
- 多线程架构:采用多线程而非多进程架构,以便更好地共享资源,尤其是连接池。
-
- 开发者友好的接口:设计类似NGINX/OpenResty的基于"请求生命周期"事件的可编程接口,使开发者可以轻松上手并快速开发新功能。
Pingora在生产环境中的表现
自Pingora上线以来,它处理了几乎所有需要与源服务器交互的HTTP请求,性能数据显著提升:
-
- TTFB显著降低:Pingora上流量的TTFB中位数减少了5毫秒,第95百分位数减少了80毫秒。
-
- 连接重用率大幅提升:Pingora的连接重用率提高,使每秒新连接数减少了三分之二。
-
- 资源消耗大幅降低:在相同流量负载下,Pingora的CPU和内存消耗减少了约70%和67%。
自Cloudflare上线Pingora以来,它处理了几乎所有需要与源服务器交互的HTTP请求,性能数据显著提升。
让我们看看Cloudflare使用Pingora如何加快客户的流量。
Pingora的总体流量数据显示,TTFB中位数减少了5毫秒,第95个百分位数减少了80毫秒。主要是得益于新架构可以跨所有线程共享连接,从而提高了连接重用率,减少了TCP和TLS握手时间。
与旧服务相比,Pingora每秒的新连接数减少到原来的三分之一。对于某个主要客户,连接重用率从87.1%提升到了99.92%,这将新连接减少了160倍。更直观地说,通过切换到Pingora,Cloudflare每天为客户和用户节省了434年的握手时间。
更高效、更安全、更强大
Pingora不仅性能出众,还在功能扩展和安全性方面表现优异:
-
- 功能扩展更灵活:例如,Cloudflare能够轻松添加HTTP/2上游支持,并向客户提供gRPC服务。
-
- 更高效的资源使用:Rust代码和多线程模型使得资源使用更高效,减少了连接创建和数据共享的成本。
-
- 内存安全:Rust的内存安全语义大大减少了内存错误的可能性,确保服务稳定运行。
实现简单负载均衡器的示例代码
为了更好地展示Pingora的强大功能,下面提供一个使用Pingora实现简单负载均衡器的示例代码:
use async_trait::async_trait;
use pingora::prelude::*;
use std::sync::Arc;
fn main() {
// 创建一个服务器实例,参数为None表示使用默认配置
let mut my_server = Server::new(None).unwrap();
// 初始化服务器
my_server.bootstrap();
// 创建一个负载均衡器,包含两个上游服务器
let upstreams = LoadBalancer::try_from_iter(["192.168.9.34:80", "10.0.0.9:80"]).unwrap();
// 创建一个HTTP代理服务,并传入服务器配置和负载均衡器
let mut lb = http_proxy_service(&my_server.configuration, LB(Arc::new(upstreams)));
// 添加一个TCP监听地址
lb.add_tcp("0.0.0.0:6188");
// 将服务添加到服务器中
my_server.add_service(lb);
// 运行服务器,进入事件循环
my_server.run_forever();
}
// 定义一个包含负载均衡器的结构体LB,用于包装Arc指针以实现多线程共享。
pub struct LB(Arc<LoadBalancer<RoundRobin>>);
// 使用#[async_trait]宏,异步实现ProxyHttp trait。
#[async_trait]
impl ProxyHttp for LB {
/// 定义上下文类型,这里使用空元组。对于这个小例子,我们不需要上下文存储
type CTX = ();
// 创建新的上下文实例,这里返回空元组
fn new_ctx(&self) -> () {
()
}
// 选择上游服务器并创建HTTP对等体
async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
// 使用轮询算法选择上游服务器
let upstream = self
.0
.select(b"", 256) // 对于轮询,哈希不重要
.unwrap();
println!("上游对等体是:{upstream:?}");
// 创建一个新的HTTP对等体,设置SNI为example.com
let peer: Box<HttpPeer> = Box::new(HttpPeer::new(upstream, false, "example.com".to_string()));
Ok(peer)
}
// 在上游请求发送前,插入Host头部
async fn upstream_request_filter(
&self,
_session: &mut Session,
upstream_request: &mut RequestHeader,
_ctx: &mut Self::CTX,
) -> Result<()> {
// 将Host头部设置为example.com
upstream_request
.insert_header("Host", "example.com")
.unwrap();
Ok(())
}
}
结语
Pingora的发布标志着代理技术领域的一次重大飞跃。我相信pingora也将成为rust一个有意义项目。如果你对互联网新兴技术、rust语言 等感兴趣,欢迎关注我的后续文章和技术分享!