2

最近把 pingora 的代码略略过了下,稍微弄懂了 pingora 这个项目的大致情况。Pingora 作为 Cloudflare 内部开发来替代 OpenResty 的项目,可以看到不少 Nginx 的影子。比如 Pingora 的插件叫 Module,对应插件顺序叫 module_index,和 Nginx 一模一样。有趣的是还有彩蛋藏在测试代码里:

https://github.com/cloudflare/pingora/blob/acffb8aaf2a76e3ab8a4db698ed2f151cfb64566/pingora-proxy/tests/test_basic.rs#L439

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 的处理阶段做了若干改进:

  1. 几个 response filter 都是异步的,不像 Nginx 的 body filter 只能同步调用
  2. 新增一组 upstream_*_filter,支持 upstream 特定的逻辑,比如在 cache 之前改写响应
  3. 支持 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 读到的数据:

https://github.com/cloudflare/pingora/blob/acffb8aaf2a76e3ab8a4db698ed2f151cfb64566/pingora-core/src/protocols/http/v2/server.rs#L162

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 上游。


spacewander
5.6k 声望1.5k 粉丝

make building blocks that people can understand and use easily, and people will work together to solve the very largest problems.