← 返回上一頁
DevOps 開源軟體

Pingora 實戰:Cloudflare 開源 Rust 反向代理框架,從零搭一個比 Nginx 快的 LB

本頁目錄

Cloudflare 在 2024 年初把 Pingora 開源出來的時候,他們公開的數字是這個 Rust 寫的代理框架在自家網路一天扛超過 1 兆次(trillion)HTTP 請求,至今累計處理量已經接近 1 千兆次(quadrillion)。換成同等流量,Pingora 比舊的 Nginx 服務少用大約 70% 的 CPU、67% 的記憶體,連線重用率從 87% 拉到 99.92%,相當於每天省下 434 年的 TLS handshake 時間。

這些數字看起來很驚人,但實際讀完官方 blog 與 GitHub README 之後,會發現 Pingora 跟 Nginx 不是同一個層級的東西。Nginx 是裝完就能用的 server,Pingora 是要寫 Rust 程式碼編譯出自己 binary 的 framework。把它們直接放在一起比「誰快」其實不太公平,但 Pingora 解決的問題確實是 Nginx 在超大規模場景下繞不過去的痛點。

這篇實作把 Pingora 從零裝起來,跑最小可用的反向代理、加上 round-robin 負載平衡、設定健康檢查,順便整理它跟 Nginx 在設計上到底差在哪裡,以及自架站在什麼情況下值得(或不值得)導入。

一、Pingora 的技術定位

1.1 不是 Nginx 替代品,是 framework

Cloudflare 自己在 README 裡寫得很直接:「Pingora is a library and toolset, not an executable binary... Pingora is the engine that powers a car, not the car itself.」用車子的比喻來說,Pingora 是引擎、不是整台車。

具體差異:

項目 Nginx Pingora
形態 單一 binary,apt/yum 裝完就能跑 Rust crate,要寫程式碼編譯
設定方式 nginx.conf 設定檔 Rust 程式碼定義 trait 行為
學習門檻 改設定檔就能上手 要會 Rust + async + trait
改邏輯 多半要 Lua(OpenResty)或寫 C module 直接寫 Rust,編譯進 binary
部署 套件管理直接裝 自己 build 出 binary 再部署

如果只是要把流量導到後端、做 TLS termination、加幾條 rewrite rule,Nginx 的設定檔幾分鐘就搞定;Pingora 同樣的事情你要先跑 cargo new、寫 Rust、再 cargo build --release。所以「比 Nginx 快」這個說法成立的前提,是你本來就需要用程式碼客製代理邏輯——例如根據 header 動態選後端、做 A/B 路由、串自家 control plane——這些用 Nginx 會寫到很掙扎的場景。

1.2 為什麼用 Rust 重寫

Cloudflare 並不是覺得 Nginx「不夠快」才換,他們在 How we built Pingora 裡列的真正痛點是:

1. Nginx 的 worker process 模型把連線池切碎
Nginx 是 multi-process,每個 worker 各自維護自己的 upstream 連線池。當請求落到 worker A,它只能重用 worker A 的連線——worker B 已經建好的連線完全用不到。Cloudflare 規模一大,水平擴 worker 反而讓連線重用率惡化,要對外建更多新連線才能撐住。

2. 一個請求綁一個 worker
Nginx 裡每個 request 從進來到結束都黏在同一個 worker 上。如果這個請求碰到吃 CPU 的工作或 blocking I/O,整顆 worker 卡住,core 之間負載不均。

3. C 寫的東西要再加功能太累
Nginx 是 C,要在上面長出複雜邏輯(特別是會碰記憶體的)成本與風險都高。Cloudflare 想要既快又能放心改的東西,就選了 Rust + 多執行緒共享 connection pool。

換成 Pingora 之後,所有 thread 共享同一個 upstream 連線池。對某個大型客戶,連線重用率從 87.1% 直接拉到 99.92%,新建連線數降為原本的 1/160——換算下來相當於每天節省 434 年的 TLS handshake 時間。

中位數的 time-to-first-byte 縮短 5ms,p95 縮短 80ms。CPU 與記憶體用量分別降 70% 與 67%。這些數字是 Cloudflare 真實上線後的數據,不是 lab benchmark。

二、架構與核心概念

2.1 Worker model

圖 1

對比 Nginx 的多進程:每個 worker process 各有自己的 listener、自己的 connection pool,無法跨 worker 共享。

圖 2

差別不只是「多執行緒比較潮」這麼簡單。多執行緒帶來:

  • 連線池可以全 thread 共用:對 keep-alive 與 HTTP/2 multiplexing 是巨大優勢。
  • Request 可以跨 thread 移動:CPU 重的任務可以丟給 worker thread,event loop 不被卡住。
  • Graceful reload 比較好做:不像 Nginx 要 fork 出新 master 才能換 config。

2.2 與 Nginx 的設計差異

維度 Nginx Pingora
並行模型 multi-process + epoll multi-thread async (Tokio)
連線池 per-worker global / shared
客製化 C module 或 Lua(OpenResty) 直接寫 Rust trait 實作
TLS 後端 OpenSSL OpenSSL / BoringSSL / rustls / s2n-tls
HTTP/3 1.25+ 內建 QUIC roadmap 中(截至 2026/04 還沒 GA)
記憶體安全 C,要靠 review Rust 編譯期保證

Pingora 並沒有要把 Nginx 所有功能都長出來。例如 Nginx 用得很順手的 try_files、靜態檔伺服、FastCGI 這些跟反向代理本質沒關係的東西,Pingora 不打算碰——它就是專心做 reverse proxy。

三、實作範例

下面實作以 Debian 13、Rust 1.84+ 為前提。Pingora 從 2025 年起 MSRV(最低支援版本)跟著滾動,落後一兩個版本通常編不過。

3.1 最小可跑反向代理

先把工具鏈準備好:

# 裝 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
rustc --version

# 編譯 Pingora 需要的系統依賴(Debian/Ubuntu)
sudo apt-get install -y build-essential pkg-config libssl-dev cmake clang perl

建立專案:

cargo new my_pingora_proxy
cd my_pingora_proxy

編輯 Cargo.toml

[package]
name = "my_pingora_proxy"
version = "0.1.0"
edition = "2021"

[dependencies]
pingora = { version = "0.4", features = ["proxy"] }
async-trait = "0.1"
tokio = { version = "1", features = ["full"] }

features = ["proxy"] 不能少,Pingora 把功能拆得很細,預設只給最小核心,proxy / cache / lb 都是可選 feature。

src/main.rs

use async_trait::async_trait;
use pingora::prelude::*;

pub struct MyProxy;

#[async_trait]
impl ProxyHttp for MyProxy {
    type CTX = ();
    fn new_ctx(&self) -> Self::CTX {}

    async fn upstream_peer(
        &self,
        _session: &mut Session,
        _ctx: &mut Self::CTX,
    ) -> Result<Box<HttpPeer>> {
        // 第二個參數 false 表示不開 TLS;第三個是 SNI hostname
        let peer = Box::new(HttpPeer::new(
            "127.0.0.1:8080",
            false,
            "localhost".to_string(),
        ));
        Ok(peer)
    }
}

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

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

    server.add_service(proxy);
    println!("Pingora proxy listening on :6199");
    server.run_forever();
}

整個反向代理只要這幾十行。ProxyHttp 這個 trait 是核心,upstream_peer() 決定請求要送到哪個後端。CTX 是這次請求的上下文,可以塞自己想跨 callback 帶的狀態。

編譯與啟動:

cargo build --release
./target/release/my_pingora_proxy

第一次編譯會抓很多 crate,慢一點是正常的(5~10 分鐘)。

開另一個 terminal 用 Python 起一個假的後端:

mkdir /tmp/web && cd /tmp/web
echo "hello from backend" > index.html
python3 -m http.server 8080

測試:

curl -i http://127.0.0.1:6199/
# HTTP/1.1 200 OK
# hello from backend

通了。這就是 Pingora 的最小代理,比起 Nginx 一行 proxy_pass http://127.0.0.1:8080; 是繁瑣很多,但這個架構的價值在後面要加客製邏輯時才會展現。

3.2 Load balancing

實際生產環境後端不會只有一台。Pingora 的 pingora-load-balancing 提供現成的 round-robin、random、consistent hashing 演算法。

Cargo.toml 加上:

pingora-load-balancing = "0.4"

改寫 main.rs

use async_trait::async_trait;
use pingora::prelude::*;
use pingora_load_balancing::{selection::RoundRobin, LoadBalancer};
use std::sync::Arc;

pub 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)
            .ok_or_else(|| Error::explain(ErrorType::InternalError, "no upstream"))?;

        println!("forwarding to {:?}", upstream);
        Ok(Box::new(HttpPeer::new(upstream, false, "".to_string())))
    }
}

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

    let upstreams =
        LoadBalancer::try_from_iter(["127.0.0.1:8080", "127.0.0.1:8081", "127.0.0.1:8082"])
            .unwrap();
    let lb = Arc::new(upstreams);

    let mut proxy = http_proxy_service(&server.configuration, LB(lb));
    proxy.add_tcp("0.0.0.0:6199");

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

select(b"", 256) 第一個參數是給 consistent hashing 用的 key,round-robin 模式下會被忽略;第二個是 retry 次數上限(避開那些已經被標記 unhealthy 的 backend)。

開三個後端 server 後反覆 curl,可以看到 stdout 印出輪流送往不同 upstream。

3.3 Health check 與失敗 retry

光輪詢還不夠,後端掛了要能感知到。Pingora 內建 TCP 與 HTTP health check,但要把它變成 background task 跑:

use pingora::services::background::background_service;
use pingora_load_balancing::health_check::TcpHealthCheck;
use std::time::Duration;

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

    let mut upstreams = LoadBalancer::try_from_iter([
        "127.0.0.1:8080",
        "127.0.0.1:8081",
        "127.0.0.1:8082",
    ])
    .unwrap();

    // 設定 health check
    let hc = TcpHealthCheck::new();
    upstreams.set_health_check(hc);
    upstreams.health_check_frequency = Some(Duration::from_secs(5));

    // 把 LB 包成 background service,定期跑 health check
    let bg = background_service("health check", upstreams);
    let lb = bg.task();

    let mut proxy = http_proxy_service(&server.configuration, LB(lb));
    proxy.add_tcp("0.0.0.0:6199");

    server.add_service(bg);
    server.add_service(proxy);
    server.run_forever();
}

幾個重點:

  • background_service() 會把 LB 變成獨立的 background task,每 5 秒打 TCP 連線檢查所有 backend。
  • bg.task() 拿到的是同一個 LB instance 的 Arc 參考,proxy 跟 health checker 共用狀態。
  • 故意把 8081 後端關掉,幾秒後 select() 就不會再選到它;重啟後會自動恢復。

如果要更細緻的 HTTP health check(檢查特定 path、status code),把 TcpHealthCheck::new() 換成 HttpHealthCheck 並設定 request path。

至於 upstream 連線失敗的 retry,Pingora 用 fail_to_connect()fail_to_proxy() 這兩個 callback,可以在程式內判斷錯誤類型決定要不要重試、換 backend。比起 Nginx proxy_next_upstream 的設定旗標,這邊邏輯能寫得很細。

四、效能對比 vs. Nginx

4.1 Cloudflare 公開的 benchmark

Cloudflare 自己給的數字是生產環境實測,不是 synthetic benchmark:

指標 改善幅度
CPU 使用 -70%
記憶體使用 -67%
中位數 TTFB -5ms
p95 TTFB -80ms
連線重用率(某大型客戶) 87.1% → 99.92%
新連線數 約原本的 1/160

要注意這個對比的底是「Cloudflare 自己改過、跑了多年的 Nginx」,不是你 apt install 來的 vanilla Nginx。Cloudflare 在 Nginx 上累積的客製碼非常重,他們是把客製過的 Nginx 換成 Pingora,省下來的 CPU/Memory 來自架構層面的勝利(多執行緒、共享連線池),不是 Rust 比 C 快。

社群獨立做的 benchmark 結果不一致:GitHub issue #372 裡有人實測單純 throughput Nginx 還略勝;Pingora 在低延遲與穩定性上略好,但都在誤差範圍。也就是說:單純比每秒多打幾個 HTTP request,Pingora 不會有戲劇性贏過 Nginx。它真正的優勢在連線池共享、長時間穩定運行下的資源節省。

4.2 適合場景與不適合場景

適合:

  • 大量 keep-alive、HTTP/2 multiplexing 的反向代理場景
  • 需要動態決定 upstream 的場景(A/B、灰度、tenant routing、地理路由)
  • 想用 Rust 寫整條控制平面、需要把 proxy 邏輯與業務邏輯整合
  • 要 BoringSSL 或 post-quantum crypto

不適合:

  • 單純的 web server(serve 靜態檔、跑 PHP-FPM)——直接 Nginx 或 Caddy
  • 團隊沒人會 Rust,而且也沒打算學
  • 要的是設定檔即生效、不想 build binary 的便利
  • 需要 HTTP/3 / QUIC(截至 2026/04 還沒 GA)

順帶一提,如果想要「Pingora 的引擎 + Nginx-like 設定檔」的折衷方案,社群有 Pingap;Internet Security Research Group(Let's Encrypt 那個 ISRG)也基於 Pingora 做了 River,目標就是「memory-safe 的 Nginx 替代」。

五、與 Nginx / HAProxy / Traefik 比較

工具 語言 設計目標 上手難度 設定方式 生態成熟度 適合單機自架
Nginx C 多用途 web server + proxy nginx.conf 極成熟
HAProxy C L4/L7 高效能 LB haproxy.cfg 成熟
Traefik Go 雲原生、自動服務發現 中低 YAML / labels 成熟 ✓(搭 Docker/K8s 更佳)
Envoy C++ service mesh data plane YAML / xDS API 成熟 △ 太重
Caddy Go 自動 HTTPS、容易上手 極低 Caddyfile
Pingora Rust 客製化代理 framework Rust 程式碼 早期 △ 殺雞用牛刀

選擇邏輯大概是:

  • 一般網站、PHP/Node 後端 → Nginx 或 Caddy
  • 純 L7 LB、要做複雜 routing → HAProxy
  • K8s ingress → Traefik / Nginx Ingress / Envoy
  • service mesh → Envoy / Linkerd
  • 寫得起 Rust、要極端高並發 + 客製邏輯 → Pingora

六、實際採用建議

幾個現實的問題我覺得要先想清楚:

1. 團隊有沒有人能 maintain Rust 程式碼?
Pingora 不是裝完就忘的東西,它是你產品的一部分原始碼。沒人會 Rust 的話,下次要加 feature、debug、升級 dependency,都會卡住。

2. 真的需要客製代理邏輯嗎?
如果只是 proxy_pass、改幾個 header、做 TLS termination,老老實實用 Nginx。Pingora 的價值在「Nginx 配置已經寫到很噁心、你想要直接寫程式」的那一刻才會出現。

3. 對 HTTP/3 的需求
Nginx 從 1.25 開始有 QUIC、Caddy 也支援,Pingora 還沒 GA。如果要前線跑 HTTP/3 給瀏覽器,Pingora 還不到位。

4. 流量規模值不值得
70% CPU 節省聽起來很爽,但你得是「每天打幾億請求、機房裡有幾百台 LB」的規模才有感。如果你每秒幾百到幾千 RPS,Nginx 的 CPU 使用率本來就在個位數,省 70% 變成「省 1 個 core」,還抵不過引入 Rust 帶來的維運成本。

老實講,自架站想試 Pingora 比較合理的玩法是當作 Rust 網路程式設計的學習素材,而不是真的拿來換 Nginx。除非你的代理邏輯複雜到 Lua/OpenResty 已經寫不下去。

七、踩雷與限制

實際操作過程中遇到的幾個點:

  • Rust 版本要新:Pingora 的 MSRV 是滾動 6 個月推進,距離開源時的 1.72 已經拉到 1.84+。Debian/Ubuntu 套件版本太舊,乖乖用 rustup 裝。
  • 編譯依賴cmakeclangperlpkg-configlibssl-dev 這幾個沒裝會在 build BoringSSL / libz-ng-sys 那邊噴錯。錯誤訊息不直觀,第一次踩會花時間。
  • Crate feature 要選對pingora crate 是 façade,proxycachelb 都是可選 feature,沒打開會編不到對應模組。
  • API 還會變:Cloudflare 自己在 README 寫 API stability is not guaranteed,從 0.1 升到 0.4 中間就有幾個破壞性變更(例如 LoadBalancer 的構造方式)。生產環境記得釘版本。
  • 沒有 hot reload:Nginx nginx -s reload 一秒搞定,Pingora 的 graceful upgrade 要靠 SIGQUIT + 啟動新 binary,操作流程比較繁瑣。
  • macOS / Windows 不是首選平台:開發機用 macOS 通常沒問題(會有 feature 限制),生產跑 Linux x86_64 / aarch64 才是主軸;Windows 是社群 best-effort。
  • 記憶體安全 ≠ 邏輯安全:Rust 編譯期防住的是 memory unsafety,業務邏輯漏洞、SSRF、header injection 該檢查還是要檢查。

八、小結

Pingora 是一個框架,定位很清楚:給 Cloudflare 等級的玩家寫客製 proxy 用。它解決的問題(多執行緒共享連線池、Rust 記憶體安全)對超大規模業務是真痛點,70% CPU、67% 記憶體節省的數字也是真的。

但對絕大多數自架站來說,這些優勢沒有作用點。Nginx 跑得好好的,沒必要為了「比 Nginx 快」這幾個字,把一個原本五行設定檔的東西改寫成幾百行 Rust 程式碼。比較合理的態度是:把 Pingora 當成「值得追蹤的反向代理新典範」,等它的高階包裝(Pingap、River)成熟,或等到自己的代理邏輯複雜到非得寫程式不可的那一天,再認真考慮導入。

Cloudflare 把這套東西開源出來這件事本身就值得鼓掌——畢竟整個 Internet 中介層被一坨 1990 年代的 C code 撐著,已經是事實。Rust 把這層慢慢替換掉,是好事。

參考資料

分享這篇
X LinkedIn Facebook Hacker News Reddit

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料