Cloudflare 开源 Pingora 之后,很多人会问:它能不能替代 Nginx?如果问题只是“能不能把请求转发到后端服务”,答案是能,而且几十行 Rust 就够了。但 Pingora 不是一个读取 nginx.conf 的服务器,它更像一套用来写代理、网关和网络服务的 Rust 框架。

这篇只做一件事:用 Pingora 写一个最小反向代理,让 http://127.0.0.1:6188/ 转发到本机的 127.0.0.1:3000,然后把这个二进制按服务方式部署起来。

本文示例版本核对时间:2026-05-17。示例使用 pingora = "0.8"

先看等价的 Nginx 配置

如果用 Nginx,最小反代大概长这样:

server {
    listen 6188;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto http;
    }
}

Pingora 的思路不一样。你不是写配置块,而是实现 ProxyHttp trait:请求进来以后,代码决定它要转发到哪里,转发前要改哪些请求头,最后日志怎么记。

创建项目

cargo new mini-pingora-proxy
cd mini-pingora-proxy

Cargo.toml

[package]
name = "mini-pingora-proxy"
version = "0.1.0"
edition = "2021"

[dependencies]
async-trait = "0.1"
env_logger = "0.11"
log = "0.4"
pingora = { version = "0.8", features = ["proxy"] }

这里用的是 pingora 聚合 crate,并启用 proxy feature。Pingora 没有默认打开所有能力;如果后面要做负载均衡,需要再启用 lb;如果要监听 HTTPS 或连接 HTTPS upstream,还要选择对应的 TLS feature,比如 opensslboringssls2n 或实验性的 rustls

实现最小代理

src/main.rs

use async_trait::async_trait;
use log::info;
use pingora::prelude::*;

struct MiniProxy {
    upstream: &'static str,
}

#[async_trait]
impl ProxyHttp for MiniProxy {
    type CTX = ();

    fn new_ctx(&self) -> Self::CTX {}

    async fn upstream_peer(
        &self,
        _session: &mut Session,
        _ctx: &mut Self::CTX,
    ) -> Result<Box<HttpPeer>> {
        let peer = HttpPeer::new(self.upstream, false, String::new());
        Ok(Box::new(peer))
    }

    async fn upstream_request_filter(
        &self,
        session: &mut Session,
        upstream_request: &mut RequestHeader,
        _ctx: &mut Self::CTX,
    ) -> Result<()> {
        if let Some(host) = session
            .req_header()
            .headers
            .get("host")
            .and_then(|value| value.to_str().ok())
        {
            upstream_request.insert_header("Host", host).unwrap();
        }

        if let Some(client_addr) = session.client_addr().and_then(|addr| addr.as_inet()) {
            let ip = client_addr.ip().to_string();
            let forwarded_for = match session
                .req_header()
                .headers
                .get("x-forwarded-for")
                .and_then(|value| value.to_str().ok())
            {
                Some(existing) if !existing.is_empty() => format!("{existing}, {ip}"),
                _ => ip.clone(),
            };

            upstream_request.insert_header("X-Real-IP", &ip).unwrap();
            upstream_request
                .insert_header("X-Forwarded-For", forwarded_for)
                .unwrap();
        }

        upstream_request
            .insert_header("X-Forwarded-Proto", "http")
            .unwrap();

        Ok(())
    }

    async fn logging(
        &self,
        session: &mut Session,
        error: Option<&Error>,
        _ctx: &mut Self::CTX,
    ) {
        info!(
            "{} error={:?}",
            session.request_summary(),
            error.map(|e| e.to_string())
        );
    }
}

fn main() {
    env_logger::init();

    let mut server = Server::new(None).unwrap();
    server.bootstrap();

    let proxy = MiniProxy {
        upstream: "127.0.0.1:3000",
    };

    let mut service = http_proxy_service(&server.configuration, proxy);
    service.add_tcp("0.0.0.0:6188");

    server.add_service(service);
    server.run_forever();
}

看到这里,Pingora 的核心就出来了:

  • upstream_peer() 决定请求要去哪个后端,类似 Nginx 的 proxy_pass
  • upstream_request_filter() 在转发前补请求头,类似 proxy_set_header
  • http_proxy_service()MiniProxy 变成真正监听 TCP 端口的服务。

HTTP 解析、连接建立、连接复用、请求体和响应体转发这些通用代理工作,Pingora 已经处理了。你写的是“这个请求应该怎么被代理”的业务逻辑。

本地运行

先启动一个后端服务:

python3 -m http.server 3000

再启动 Pingora 代理:

RUST_LOG=info cargo run

测试:

curl -v http://127.0.0.1:6188/

如果能看到 Python 静态服务器返回的目录列表或页面,说明最小反代已经跑通。

编译 release 版本

开发时用 cargo run 就够了,上服务器时应该编译 release 版本:

cargo build --release

复制到服务器上的常见位置:

install -m 755 target/release/mini-pingora-proxy /usr/local/bin/mini-pingora-proxy

如果后端服务和 Pingora 代理都在同一台机器上,确认本机能访问后端:

curl -I http://127.0.0.1:3000/

再直接运行代理:

RUST_LOG=info /usr/local/bin/mini-pingora-proxy

另开一个终端测试:

curl -I http://127.0.0.1:6188/

用 systemd 部署

最简单的 systemd 单元可以这样写。

/etc/systemd/system/mini-pingora-proxy.service

[Unit]
Description=Mini Pingora Reverse Proxy
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/local/bin/mini-pingora-proxy
Restart=always
RestartSec=3
Environment=RUST_LOG=info
LimitNOFILE=1048576

[Install]
WantedBy=multi-user.target

启动:

systemctl daemon-reload
systemctl enable --now mini-pingora-proxy
systemctl status mini-pingora-proxy

看日志:

journalctl -u mini-pingora-proxy -f

到这里,这个最小代理已经能作为一个系统服务运行。它还没有配置文件、没有 HTTPS、没有 Web 管理界面,也没有把 upstream 做成可热更新配置,但作为理解 Pingora 的起点已经足够。

接入 Pingora 的运行配置

如果你想让这个二进制更像正式服务,可以让 Pingora 接管命令行参数:

use pingora::prelude::*;

fn main() {
    env_logger::init();

    let mut server = Server::new(Some(Opt::parse_args())).unwrap();
    server.bootstrap();
    // add service...
    server.run_forever();
}

这样二进制就可以读取 Pingora 的运行配置。一个典型 conf.yaml

---
version: 1
threads: 2
pid_file: /run/mini-pingora-proxy.pid
error_log: /var/log/mini-pingora-proxy/error.log
upgrade_sock: /run/mini-pingora-proxy-upgrade.sock
upstream_keepalive_pool_size: 512
work_stealing: true

后台启动:

RUST_LOG=INFO /usr/local/bin/mini-pingora-proxy -c conf.yaml -d

优雅停止:

pkill -SIGTERM mini-pingora-proxy

优雅升级:

pkill -SIGQUIT mini-pingora-proxy
RUST_LOG=INFO /usr/local/bin/mini-pingora-proxy -c conf.yaml -d -u

这种模式更接近传统代理服务的运行方式。新进程可以接管监听 socket,旧进程继续处理已经在途的请求,避免粗暴停启造成连接中断。

如果要多个后端

最小代码只转发到一个后端。真实服务通常还要多个 upstream 和健康检查。Pingora 可以做,但你要继续写代码。

启用负载均衡 feature:

pingora = { version = "0.8", features = ["proxy", "lb"] }

核心逻辑类似这样:

use async_trait::async_trait;
use pingora::prelude::*;
use std::sync::Arc;

struct LB(Arc<LoadBalancer<RoundRobin>>);

#[async_trait]
impl ProxyHttp for LB {
    type CTX = ();

    fn new_ctx(&self) -> Self::CTX {}

    async fn upstream_peer(
        &self,
        _session: &mut Session,
        _ctx: &mut Self::CTX,
    ) -> Result<Box<HttpPeer>> {
        let upstream = self.0.select(b"", 256).unwrap();
        let peer = HttpPeer::new(upstream, false, String::new());
        Ok(Box::new(peer))
    }
}

fn main() {
    let mut server = Server::new(None).unwrap();
    server.bootstrap();

    let upstreams =
        LoadBalancer::try_from_iter(["127.0.0.1:3000", "127.0.0.1:3001"]).unwrap();

    let mut service = http_proxy_service(&server.configuration, LB(Arc::new(upstreams)));
    service.add_tcp("0.0.0.0:6188");

    server.add_service(service);
    server.run_forever();
}

这对应 Nginx 的:

upstream app {
    server 127.0.0.1:3000;
    server 127.0.0.1:3001;
}

server {
    listen 6188;
    location / {
        proxy_pass http://app;
    }
}

如果再加主动健康检查,Pingora 官方示例会用 TcpHealthCheck,并把 LoadBalancer 放到 background service 里运行。看到这里,问题就变成了:你到底是想写一个自己的代理程序,还是想部署一个现成的反向代理?

什么时候直接用 Pingora

Pingora 适合下面这些场景:

  • 你要学习代理链路,理解请求从 downstream 到 upstream 的生命周期。
  • 你要把鉴权、灰度、限流、审计、路由规则写成 Rust 代码。
  • 你要做自己的 API Gateway、Ingress、CDN 代理或内部网关产品。
  • 你能接受维护自己的配置格式、证书加载、管理接口、日志指标和升级策略。

如果目标只是“把几个域名反代到几个服务,顺便配 HTTPS”,直接写 Pingora 不是最短路径。Pingora 能做到,但 TLS、ACME、配置热更新、访问日志、Web 管理和插件系统都需要你自己补。

参考资料