最近把 pingora 的代码略略过了下,稍微弄懂了 pingora 这个项目的大致情况。Pingora 作为 Cloudflare 内部开发来替代 OpenResty 的项目,可以看到不少 Nginx 的影子。比如 Pingora 的插件叫 Module,对应插件顺序叫 module_index,和 Nginx 一模一样。有趣的是还有彩蛋藏在测试代码里:
async fn test_tls_verify_sni_not_host() {
init();
let client = reqwest::Client::new();
let res = client
.get("http://127.0.0.1:6149/tls_verify")
.header("sni", "openrusty.org")
.header("verify", "1")
.send()
.await
.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}
openresty.org 是 OpenResty 的官网, 这里将 resty 替换成 rusty,暗指用 Rust 重写 OpenResty。
大体上可以认为,将 Nginx 非核心模块和路由匹配部分统统抽离,剩下的框架用 Rust 改写,就成了 pingora。Pingora 只是个框架,允许用户拓展默认实现来提供自己的代理逻辑。它没有外围配置部分的代码,甚至连插件体系也还处于草创阶段。
server side tls 支持
Pingora 在 server side tls 能力,目前看到的只有一些静态配置的像 cipher、TLS 版本的参数,动态的只有 TlsAccept 下面有个 certificate_callback,允许用户动态选择证书。你可以把它看作 OpenResty 的 ssl_certificate_by_lua。现在 OpenResty 还多了个 ssl_client_hello_by_lua,可以在里面处理 clienthello 信息。不知道之前 Cloudflare 在 OpenResty 开发的更加高阶的功能,如 tls session ticket 的轮转、keyless,在这个新架构上如何实现。
没看到 pingora 在 server side tls 上实现了 mTLS 的能力,当然它也可以由用户在 certificate_callback 上实现。
基于多个阶段的流式处理
Pingora 允许开发在各个阶段里加入请求处理的自定义逻辑。从这页文档里可以看到 pingora 请求处理的各个阶段:https://github.com/cloudflare/pingora/blob/main/docs/user_guide/phase.md。
和 Nginx/Envoy 不同的是,pingora 的 phase 并不是严格的洋葱模型。在 Nginx/Envoy 里面,如果一个请求在抵达上游之前提前被响应了,那么它还会执行 header_filter / encode_header 等响应处理。这点和来自上游的响应是相同的。但在 pingora 里,上游返回的响应和代理提前返回的响应走的路径不一样。提前响应不会走 response_filter 和 response_body_filter。
比之 OpenResty,pingora 的处理阶段做了若干改进:
- 几个 response filter 都是异步的,不像 Nginx 的 body filter 只能同步调用
- 新增一组
upstream_*_filter
,支持 upstream 特定的逻辑,比如在 cache 之前改写响应 - 支持 request_body_filter 流式处理请求体。其实 Nginx 自己提供了流式处理请求体的能力,不过 OpenResty 没有基于这个能力开发出对应 request_body_filter_by_lua。
注:pingora 文档里没有提到 request_body_filter,所以我给他们建了个 issue。
在有些场景下,我们需要在发送给上游之前收完全部的请求。比如发请求体到外部服务器进行鉴权,之后才决定是否要发送给上游。由于 pingora 在处理完 request_filter 后就会发送给上游,目前 request_body_filter 是在和上游建立连接后才会调用的,所以目前 pingora 并不能用 request_body_filter 支持这种场景。
和 OpenResty 的 read_body 一样,pingora 也提供了主动读请求体的方法:Session 下面有个 read_request_body。比之会一下子读到所有请求体的 read_body,read_request_body 每次只返回读到的一部分 body,所以它支持更动态地控制 body 的读操作。不幸的是,目前只有在开启了 retry_buffer 后,pingora 才会保存 read_request_body 读到的数据:
read_body_bytes 是 read_request_body 最终调用的方法
pub async fn read_body_bytes(&mut self) -> Result<Option<Bytes>> {
// TODO: timeout
let data = self.request_body_reader.data().await.transpose().or_err(
ErrorType::ReadError,
"while reading downstream request body",
)?;
if let Some(data) = data.as_ref() {
self.body_read += data.len();
if let Some(buffer) = self.retry_buffer.as_mut() {
buffer.write_to_buffer(data);
}
let _ = self
.request_body_reader
.flow_control()
.release_capacity(data.len());
}
Ok(data)
}
而 retry_buffer 只在和上游建立完连接后才会被设置。所以在这之前调用 read_request_body,只会消耗请求体,不会把它们发给上游。这条路看上去也走不通。
所以目前 pingora 尚且不支持在发送给上游之前收完全部的请求的场景。
上游代理功能
Pingora 选择上游是通过 upstream_peer 方法返回一个 HttpPeer 实现的。在 Nginx/Envoy 中,往往是先命中一个上游配置(upstream / cluster),再从上游配置中挑选一个 peer / endpoint 来返回。而 HttpPeer 同时包含这两种角色。HttpPeer 里面有个 PeerOption 字段,在其他代理里作为上游配置的大部分字段都存在它下面。
Pingora 目前支持代理 HTTP1 或 HTTP2 到上游,这是由 PeerOption 的 alpn 字段控制的。但是正如 HttpPeer 的名字所显示的,它不支持代理到 TCP 上游。如果底层平台不支持代理到 TCP 上游,那么在实现诸如 HTTP2Dubbo 这一类协议转换时就很困难,因为没办法在原有机制上稍作修改来满足业务需求。在 pingora 里倒是有个 connectors/l4
的模块,不过看上去它只用于通过 CONNECT 协议来做正向代理,不确定能不能修改它来支持代理到 TCP 上游。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。