pingora 解决什么问题
主线位置:三条主线的外面。本页回答写一个反向代理要决定哪些事、pingora 替你决定了哪些、留给你的是什么;README 里的说法逐条对到代码上;最后用一次实测说明「自己的 HTTP/1 实现」在线上看得见的后果。
flowchart TB user["你的代码 impl ProxyHttp"] --> proxy["pingora-proxy 固定阶段"] proxy --> h1["自己的 HTTP/1"] proxy --> h2["h2 crate"] proxy --> conn["连接器与连接池"] h1 --> l4["L4 流与 TLS 后端"] conn --> l4 srv["Server 进程管理"] --> proxy srv --> rt["每服务一个 tokio 运行时"]
写一个反向代理要决定什么
一个反向代理收到下游的请求,决定发往哪个上游,转发过去,再把响应转回来。这句话里藏着一串要决定的事:怎么解析下游的请求、能不能改写它、上游的连接怎么建立和复用、上游失败了重试还是换一个、两边谁先关、进程收到信号怎么退、升级二进制时监听端口怎么不中断。
配置驱动的代理(nginx 是典型)把这些事做成配置项,再用模块或嵌入脚本补上配置表达不了的逻辑。pingora 的做法是:这些事全部写成 Rust 代码,框架把一次请求切成固定的阶段,在每个阶段调你实现的 trait 方法。最小的代理就是实验模板里的这几行:
struct Hello;
#[async_trait]
impl ProxyHttp for Hello {
type CTX = ();
fn new_ctx(&self) -> Self::CTX {}
async fn upstream_peer(&self, _session: &mut Session, _ctx: &mut ()) -> Result<Box<HttpPeer>> {
Ok(Box::new(HttpPeer::new("127.0.0.1:18081", false, String::new())))
}
}
fn main() {
let mut server = Server::new(None).unwrap();
server.bootstrap();
let mut proxy = http_proxy_service(&server.configuration, Hello);
proxy.add_tcp("127.0.0.1:18080");
server.add_service(proxy);
server.run_forever();
}
ProxyHttp 有 42 个方法,只有 new_ctx 和 upstream_peer 没有默认实现。其余 40 个按阶段分:读完请求头(early_request_filter、request_filter)、缓存(request_cache_filter、cache_key_callback 等十来个)、选上游和连上之后(upstream_peer、connected_to_upstream、upstream_request_filter)、响应(upstream_response_filter、response_filter 和各自的 body / trailer 版本)、失败(fail_to_connect、error_while_proxy、fail_to_proxy)、收尾(logging)。哪个阶段先调、调几次,一次请求逐个打日志看;每个钩子的语义见 ProxyHttp。main 里的 Server 管的是进程那一层:读配置、起运行时、处理信号,见一个 Server 的一生。
README 的说法,落在哪段代码上
README 的「Feature highlights」有七条。逐条核实:
| README | 代码 | 核实 |
|---|---|---|
| Async Rust: fast and reliable | 每个服务一个 tokio 运行时(run_service),可选工作窃取或 N 个单线程运行时 | 「fast」没有离线可复现的数字,不讲 |
| HTTP 1/2 end to end proxy | 下游:process_new 按 ALPN 或 h2c 前言分流;上游:proxy_to_upstream 里 ClientSession::H1 / H2 两个分支,h2 失败时可以降级到 h1(prefer_h1) | 下游 h2 + 上游 h1 也行,proxy_1to1 里专门从 :authority 补 Host |
| TLS over OpenSSL, BoringSSL, s2n-tls, or rustls (experimental) | 门面 crate 的四个 feature,pingora-core 里按 feature 把后端改名成 tls | 只能开一个,见怎么读这一卷 |
| gRPC and websocket proxying | gRPC-Web 转换是一个下游模块 GrpcWebBridge;WebSocket 走 HTTP/1 的 Upgrade,0.9.0 默认的上游请求策略是 WebSocketOnly:只放行 WebSocket 升级,其他 Upgrade 被 sanitize_h1_upstream_request 删掉 | 原生 gRPC 就是 h2 上游,不需要额外代码 |
| Graceful reload | SIGQUIT 时旧进程把监听 fd 经 Unix socket 交给带 -u 启动的新进程(server/transfer_fd/) | 传 fd 只在 Linux 上实现;macOS 上旧进程照样打印「listener sockets sent」,见一个 Server 的一生 |
| Customizable load balancing and failover strategies | pingora-load-balancing;fail_to_connect / error_while_proxy 决定重试,重试时重新调 upstream_peer | 见 07 负载均衡 和错误与重试 |
| Support for a variety of observability tools | pingora-prometheus、sentry、dial9、pingora-foundations | 本卷都不展开 |
README 另有一句「serving more than 40 million Internet requests per second」,链到 Cloudflare 的博客。这类规模数字离线无法核实,本卷不引用。它在「System requirements」一节写得很直白:「Linux is our tier 1 environment and main focus」,macOS 只保证大部分代码能编译,「some features might be missing」。本卷的实验平台正好是后者,缺了什么在用到的页面上写明。
和用 hyper 搭的代理比
用 hyper 也能写反向代理:hyper 的服务端连接收请求,hyper-util 的 legacy Client 发给上游。和这种写法比,下面四处差别都能在代码里核实。
自己的 HTTP/1。 cargo tree -p pingora@0.9.0 里没有 hyper,HTTP/1 在 pingora-core/src/protocols/http/v1/(13212 行)里,解析用 httparse,编码自己写。最直接的后果在头名的大小写上。hyper 的 HeaderMap 里头名一律小写,默认也按小写写出去,要原样转发得打开 preserve_header_case,见 hyper 卷的解析与编码。pingora 的 RequestHeader 在 http crate 的 Parts 旁边多存了一份 header_name_map,记下每个头在线上的原始写法,默认就按它写出去。本页最后的实验能看到这一点。
固定阶段的钩子。 tower 的中间件是 Service 套 Service,每一层都能改请求、改响应、决定要不要往里传(tower 卷:一个请求穿过一摞中间件)。pingora 只有一个 ProxyHttp,阶段的顺序写死在 process_request 里,你只能在这些位置插代码,换来的是不用自己拼请求体、响应体的双向转发和重试循环。
服务级共享的上游连接池。 HttpProxy 里有一个 client_upstream(Connector),整个服务的所有下游连接、所有工作线程共用它的连接池(默认容量 upstream_keepalive_pool_size: 128)。上游连接用完就放回池里,下一个请求不论来自哪条下游连接,都先查池。hyper-util 的 legacy Client 也有连接池(hyper 卷的连接池),差别在于 pingora 不需要你选:每个代理默认如此,复用的判定(同 IP:port、scheme、SNI、证书设置等才算同一个 Peer)也是框架定的。实测见一次请求。
自带进程管理。 hyper 只管一条连接上的 HTTP,accept 循环、信号、停机都由使用者组织(hyper 卷的优雅停机用的是 hyper-util 的 GracefulShutdown)。pingora 的 Server 把这些都包了:命令行(-c、-d、-t、-u)、YAML 配置、守护进程、SIGTERM 宽限期、SIGQUIT 平滑升级。代价是你得按它的方式组织程序:run_forever 不返回,服务的运行时由它建。
这四处各有代价,完整的对照放在 09 设计取舍。
问: pingora 也用 http crate 的 HeaderMap,HeaderName 存进去就是小写的,原始大小写存在哪?转发时改了头会不会丢?
答: 存在 RequestHeader / ResponseHeader 的 header_name_map: Option<CaseMap> 里,它是一个和 HeaderMap 平行的映射,键是同一个小写的 HeaderName,值是线上原样的字节。所以 pingora 不让你直接拿 &mut HeaderMap 去改:通过 insert_header / append_header 改,两份才能同步。这也是 0.9.0 去掉 RequestHeader 和 ResponseHeader 的 DerefMut 的原因之一,CHANGELOG 的说法是「unrestricted mutation could violate internal invariants」,见请求头与错误。
实验:一个请求头穿过 pingora
intro-run 的 hooks 代理(127.0.0.1:18100 → 18101,所有钩子只打日志、不改任何东西)前面放一个 Python 写的客户端,后面放一个原样打印收到字节的上游(rawup.py,回一个带 x-UpStReAm: yes 的 200)。客户端发:
GET /case HTTP/1.1\r\n
Host: 127.0.0.1:18100\r\n
X-MiXeD-CaSe: 1\r\n
connection: close\r\n
\r\n
X-MiXeD-CaSe 和 connection 两行各变成什么样?客户端拿到的响应里,上游写的 x-UpStReAm 又是什么写法?平台是 M2 Max、macOS 27、rustc 1.99.0,pingora 0.9.0(tokio 1.53.2),debug。
动脑筋答案
上游收到的(\r\n 照原样标出):
GET /case HTTP/1.1\r\n
Host: 127.0.0.1:18100\r\n
X-MiXeD-CaSe: 1\r\n
\r\n
客户端收到的:
HTTP/1.1 200 OK\r\n
x-UpStReAm: yes\r\n
Content-Length: 3\r\n
Date: Thu, 29 Oct 2026 05:06:50 GMT\r\n
Connection: close\r\n
\r\n
ok
- 三行头,
connection那一行没了。Connection是逐跳头(hop-by-hop header),只对一跳有意义。0.9.0 起上游请求默认经过sanitize_h1_upstream_request,删掉标准的逐跳头和Connection里点名的头,CHANGELOG 把它列在破坏性变更里:「Upstream requests strip hop-by-hop and Connection-nominated headers by default」。下游要求关连接,不等于上游连接也要关;上游连接能不能复用,由 pingora 自己决定。 X-MiXeD-CaSe原样保留。 这就是上一节说的header_name_map。经过 hyper 默认配置的代理,上游会看到x-mixed-case。- 响应方向同样保留。
x-UpStReAm一个字母都没变。 - pingora 加了两行。
Date和Connection: close是下游 HTTP/1 会话在prepare_response_header里写的:Date用insert_header,上游给了也会被换成 pingora 缓存的当前时间;Connection按这条下游连接还能不能 keep-alive 来写,这里客户端要求了close。
小结
- pingora 把代理的行为写成
ProxyHttp上的代码:42 个方法,必须写的只有new_ctx和upstream_peer,阶段顺序固定在process_request里。 - README 的七条特性都能在代码里找到;平滑升级的传 fd 只在 Linux 上实现;规模数字不在本卷核实范围内。
- 和 hyper 搭的代理比:没有 hyper 依赖,自己的 HTTP/1 默认保留头名大小写;钩子固定不可嵌套;上游连接池按服务共享、默认开启;
Server管进程、信号和升级。 - 实测:请求头和响应头的大小写原样通过;
connection: close不会转给上游(0.9.0 默认删逐跳头);下游响应的Date、Connection由 pingora 自己写。