Pingora 初探
CloudFlare
在 2022 年中时曾发布过一篇博文 How we built Pingora, the proxy that connects Cloudflare to the Internet 介绍了其为了解决 Nginx
在实际应用场景下的诸多困境从头设计并使用 Rust
开发了一个名为 Pingora
的 代理
, 并在文章中介绍将会开源该系统. 但后续针对 Pingora
便再无其他消息, 开源计划看起来也是遥遥无期, 个人也一直担心是否会像 Servo
一样胎死腹中. 但昨天 CloudFlare
在 Github
上开源了该系统相关代码 cloudflare/pingora, 同时也发布了一篇博文 Open sourcing Pingora: our Rust framework for building programmable network services 介绍了 Pingora
开发相关的工作.
在开发 Pingora
之初, 与所有其他公司一样 CloudFlare
也考虑过通过付费以便 Nginx
背后的公司可以为其进行定制化开发和迁移到其他诸如 Envoy 等第三方代理系统, 但经长期考虑最终还是选择自行构建一个全新的代理系统, 该系统设计目标主要是 快速
, 高效
和 安全
. 在不舍弃 高效
和 快速
的前提下追求尽可能 安全
的目标, CloudFlare
自然地选择了 Rust
语言开发 Pingora
. Pingora
被设计的不是一个完全可交付的最终产品, 而是一个可扩展的框架. 类比 Java
生态, Pingora
提供的更像是一个个可构建的 过滤器
, 其允许开发人员在收到来自外部的请求后可个性化地修改或拒绝该请求. 同时又要达成对于那些从事 Nginx
开发的人员可以轻松地使用 Pingora
进行高效地开发.
在介绍完相关 Pingora
开发和设计的背景后, 我们使用 Pingora
来进行基本的开发. 首先是新建一个 Rust
项目并添加 Pingora
依赖:
cargo new np && cd np && cargo add async-trait && cargo add pingora -F lb,proxy
首先我们创建一个 Server
的实例并启动它:
use pingora::prelude::*;
fn main() {
let mut server = Server::new(None).unwrap();
server.bootstrap();
server.run_forever();
}
现在这个 server
还没有做任何事情, 我们再来定义一个元组性结构体 - LB
:
pub struct LB(Arc<LoadBalancer<RoundRobin>>);
LB
内只包含一个 LoadBalancer
结构体, 而 LoadBalancer
结构体主要提供 服务发现
, 健康检测
和 后端选择
能力. LoadBalancer
在 pingora-load-balancing/src/lib.rs
中定义如下:
pub struct LoadBalancer<S> {
backends: Backends,
selector: ArcSwap<S>,
/// How frequent the health check logic (if set) should run.
///
/// If `None`, the health check logic will only run once at the beginning.
pub health_check_frequency: Option<Duration>,
/// How frequent the service discovery should run.
///
/// If `None`, the service discovery will only run once at the beginning.
pub update_frequency: Option<Duration>,
/// Whether to run health check to all backends in parallel. Default is false.
pub parallel_health_check: bool,
}
同时 RoundRobin
则是在 pingora-load-balancing/src/selection/algorithms.rs
定义并实现了 SelectionAlgorithm
Trait
:
/// Round Robin selection
pub struct RoundRobin(AtomicUsize);
impl SelectionAlgorithm for RoundRobin {
fn new() -> Self {
Self(AtomicUsize::new(0))
}
fn next(&self, _key: &[u8]) -> u64 {
self.0.fetch_add(1, Ordering::Relaxed) as u64
}
}
在了解以上 和
定义后, 为了完成我们初步的代码, 还需要为新定义的 LB
实现 ProxyHttp
Trait
. ProxyHttp
主要用于控制 HTTP
代理, 该 Trait
内的方法主要包含 filter
和 callback
两类. 其中任意一个 filter
返回 Result::Err
对象都将终止继续处理该 HTTP
请求, 同时错误将被记录. ProxyHttp
定义了很多方法, 但只有 new_ctx
和 upstream_peer
是必须要实现的, 其中 new_ctx
主要用于生成上下文 Context
, 可以在后续处理逻辑中存取对应状态; upstream_peer
主要用于获取当前代理的 HTTP
请求将发发往何处. 其他还有一些方法都是 filter
和 callback
相关的都有默认实现, 在当前可以不用考虑.
在暂时不为 LB
对象设计过多逻辑的情况下可以返回一个 空
(即 ()
) 的 Context
对象:
type CTX = ();
fn new_ctx(&self) -> Self::CTX {
}
另一个必须要实现的方法是 upstream_peer
, 在定义 LB
结构体时已经声明其包含的对象是 LoadBalancer<RoundRobin>
, 因此在 upstream_peer
方法内通过引用 LoadBalancer
对象便可轮询地返回后端实例:
async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
let upstream = self
.0
.select(b"", 256)
.unwrap();
info!("upstream peer is: {:?}", upstream);
let peer = Box::new(HttpPeer::new(upstream, true, "1.1.1.1".to_string()));
Ok(peer)
}
这里 LoadBalancer
结构体提供的 select
方法主要用于后端实例选择, 其会根据特定的选择算法返回第一个健康的后端实例, 而选择则会根据相应的 Hash
来进行, 但针对 随机
和 轮询
两种选择算法而言则不关心 Hash
值, 对应到此处由于采用的是 RoundRobin
因此 Hash
则无关紧要, 故而仅固定给了一个 b""
, select
的第二个参数是为寻找下一可用实例的 最大查询时间
.
至此我们已经基本完成了一个 HTTP
代理的雏形, 但距离实际可用还有一定的距离, 如果此刻运行就会发现后端实例此刻会返回 403 Forbidden
, 这是因为在 HTTP
的请求头里还包含了 Host
信息, 而任何遵守 RFC 7231
的客户端而言其在发起 HTTP
请求前都会根据其欲访问的地址设置对应的 Host
请求头, 此刻在经过 Pingora
代理后会服务端会发现其收到的 HTTP
请求并非是访问当前后端的, 故而拒绝响应. 如果想正常访问我们必须在经过 Pingora
代理时将请求头的 Host
改写为代理后的后端实例地址即可. 正如前文所述, 我们可以利用 ProxyHttp
里面对应的 filter
来实现:
async fn upstream_request_filter(&self, _session: &mut Session, upstream_request: &mut RequestHeader, _ctx: &mut Self::CTX) -> Result<()> {
upstream_request.insert_header("Host", "192.168.1.1").unwrap();
Ok(())
}
至此已经完成了一个 HTTP
代理的主体部分, 剩余的工作就是如何将定义的 LB
应用到之前创建的 Server
上. Server
提供了一个 add_service
方法可将对应的 Service
绑定到服务上:
let upstreams = LoadBalancer::try_from_iter(["192.168.1.1:443", "192.168.1.2:443"]).unwrap();
let mut lb = http_proxy_service(&server.configuration, LB(Arc::new(upstreams)));
lb.add_tcp("0.0.0.0:1234");
server.add_service(lb);
首先通过 LoadBalancer
定义了两个后端实例 192.168.1.1:443
和 192.168.1.2:443
, 接着再通过 http_proxy_service
将定义的实例绑定到之前声明的 LB
上, 最终将其添加到 Pingora
服务上.
在完成了以上代码后, 可以通过 cargo run
运行并通过 curl
模拟测试, 以下是后台输出日志:
Upstream peer is: Backend { addr: Inet(192.168.1.1:443), weight: 1 }
Upstream peer is: Backend { addr: Inet(192.168.1.2:443), weight: 1 }
Upstream peer is: Backend { addr: Inet(192.168.1.1:443), weight: 1 }
Upstream peer is: Backend { addr: Inet(192.168.1.2:443), weight: 1 }
从以上日志可以看出当前代理服务缺失以轮询的方式访问后端实例.
后记⌗
当前 Pingora
仍然有待完善, 首先 API
稳定性并不能保证, 后续可能随着版本升级或是功能迭代可能变化, 另一个问题是对非 Unix
系统的支持并不在路线图中.