VPS PostgreSQL Rust 連線池 Sharding

pgbouncer transaction mode 底下悄悄壞掉的 SET 跟 LISTEN:PgDog 用 Rust 從協議層重寫連線池,順手把 sharding 做完

PgDog 是用 Rust 跟 Tokio 寫的 PostgreSQL 連線池,單一 process 可以推到每秒 200 萬個查詢。真正的重點不是效能,而是它把 pgbouncer 拖 15 年沒解的 transaction mode 破洞一次補完:SET、LISTEN/NOTIFY、prepared statement、advisory lock 全部能用,同時把 sharding 做進 proxy 層。這篇拆解它的架構取捨、與 pgbouncer/Pgcat 的實務差異,以及什麼時候該把 VPS 上的池子換掉。

PgDog 是 2026 年上半年在 Postgres 圈累積討論度的一支 Rust 連線池,作者是前 Instacart 資料庫團隊、也是 Pgcat 原始作者 Lev Kokotov。上線至今公開的實測是單一 process 每秒 200 萬個查詢、單一 thread 每秒 5 萬筆交易。但真正讓它值得從 pgbouncer 遷過來的原因不是效能數字,而是它把 transaction mode 底下那些「文件上寫『不支援』、實務上工程師被咬過才知道」的破洞一次補完:SETLISTEN/NOTIFY、prepared statement、advisory lock,都能在 transaction pooling 底下正常運作。對每一臺跑 Postgres 的 VPS,這是 pgbouncer 之外第一個值得認真評估的替代方案。

transaction mode 是 pgbouncer 的主力,但破洞從來沒補齊

pgbouncer 三種 pooling mode 中,session mode 等於沒池化,statement mode 幾乎沒人用,實務上 90% 的部署跑 transaction mode。這個模式的邏輯是:一個 client 開 transaction,pgbouncer 從池子撈一條後端連線給它;transaction 一結束(COMMIT 或 ROLLBACK),連線立刻歸還池子,下一個 client 可能拿到完全不同的後端。

問題出在 Postgres 有一整批機制的生命週期綁在「連線」而不是「交易」上。SET search_path = app; 這種指令是 session 層的,設完之後不管開幾個 transaction 都有效——但在 transaction mode 底下,下一次 client 進來拿到別條後端,search_path 就變回原樣。ORM 或某些 framework 為了處理時區、encoding、statement_timeout 會自動在連線建立時發 SET,這些設定在 transaction mode 下等於全部失效。

LISTENNOTIFY 更嚴重。它是 Postgres 內建的 pub/sub 機制,很多小型系統靠它做非同步任務通知。但 LISTEN 註冊在連線上,transaction 結束後連線歸還,通知就永遠收不到。要用 pgbouncer 又要用 LISTEN,唯一解法是額外拉一條 session mode 的池子專門服務通知路徑,等於系統要維護兩套連線設定。

Advisory lock、WITH HOLD cursor、prepared statement 同樣是這個模式。pgbouncer 1.21 在 2023 年底才補了「協議層」prepared statement 的支援,SQL 文字層的 PREPARE ... AS ... 到今天仍然壞掉,因為 pgbouncer 完全不 parse SQL、看不到那條指令。

PgDog 選了 pgbouncer 15 年不肯做的那件事:真的 parse SQL

PgDog 內建 pg_query,也就是 Postgres 官方 parser 的 C library 抽出來的版本。這代表它進來的每一條 SQL 都會被解析成 AST,才決定要怎麼處理。這是 pgbouncer 一直拒絕做的事情,理由是「parser 太重、會拖慢 proxy」。PgDog 用 Rust 的 zero-copy 跟 Tokio 的 async 把這個成本壓下去,端到端延遲多加 100 微秒左右,換來的是整套 session 語意能被還原。

SET 的處理方式是:PgDog 記住每個 client 目前的 session 變數狀態,client 送 query 過去的時候,先跟即將分配的後端連線比對差異,用 pipeline 一次把差異補齊再送 query。舉例說 client A 之前設過 SET timezone = 'Asia/Taipei'SET statement_timeout = '3s',這次分到後端 B(B 目前是 UTC、沒有 timeout),PgDog 會把兩條 SET 塞在真正查詢前面一起送出,Postgres 端會收到三條指令。多花一個 round trip 的網路成本,但 client 那邊看到的行為跟 session mode 完全一樣。

LISTEN/NOTIFY 走的是另一條路:PgDog 內部維護一個 Tokio broadcast channel,每個 LISTEN 的 channel 被記錄在 client-side state,PgDog 自己拉一條 dedicated 連線去 Postgres 幫所有 client LISTEN,收到通知後再從自己的 channel 廣播出去。這個設計讓 client 完全不用感知底層連線切換,也不需要為 pub/sub 另外配一組池子。

Prepared statement 是最展現 SQL parser 價值的地方。client 發 PREPARE stmt1 AS SELECT * FROM users WHERE id = $1,PgDog 會把 stmt1 重寫成一個 process 全域唯一的名稱、記在全域快取,同時記住這個 client 用的別名叫 stmt1。client 後續發 EXECUTE stmt1(42) 時,PgDog 翻譯成全域名稱送到後端。多個 client 用同名 prepared statement 不會撞名、不會出現「duplicate prepared statement」錯誤,pgbouncer 至今只在協議層做到這件事。

Sharding 不是加分題,是原生設計

PgDog 的另一個定位是「shard router」。作者把 sharding 邏輯直接做進 proxy,用內建的 pg_query 從 SQL 抽出 sharding key,決定這條查詢該走哪個 shard,或者要 fan-out 到全部 shard 再合併結果。

支援的 sharding function 對齊 Postgres 官方的 partition function:HASH、LIST、RANGE,也支援用 schema name 直接指定 shard。跨 shard 的 write 用 Postgres 原生的 two-phase commit:PREPARE TRANSACTION 送到所有涉及的 shard,全部成功才發 COMMIT PREPARED,任何一個失敗就整組 rollback。這條路線比在應用層自己實作分散式交易要穩定很多,因為 Postgres 已經處理過 crash recovery、對於 prepared transaction 的清理有明確語意。

Re-sharding 用 Postgres logical replication:新增 shard 時,PgDog 幫忙設定 publication/subscription,把資料按新 hash key 遷過去,切換的瞬間才更新 routing 表。整個過程對應用透明。COPY 指令(含 CSV、binary format)在跨 shard 情境下會被拆解到對應目標,這對批次 ETL 特別實用。

當然 sharding 不是免費午餐。跨 shard 的 aggregate、ORDER BYGROUP BY 目前只支援部分場景,被引用的欄位必須出現在 SELECT 結果集裡(讓 PgDog 拿得到值做後處理)。CTE 跟 subquery 目前的處理是在所有 shard 上跑同一份,適合唯讀分析、寫入類要小心。真的重度依賴複雜跨 shard 分析的場景,Citus 或應用層 sharding 還是更成熟的選擇。

單執行緒 vs 多執行緒:這次真的是硬體問題

pgbouncer 從 2007 年到現在還是單一 event loop 的架構,開再多 process 也是靠 SO_REUSEPORT 湊。這在 8 核心以下的機器沒差,但當 VPS 給到 32 核、後端 Postgres 已經開到 400 個 shared connection、前端 client 有幾千條的規模,pgbouncer 那個核心就會被 CPU-bound。實務上要多開幾支 pgbouncer 進程,共用 backend pool 就變成得靠外面另一層 HAProxy 分流。

PgDog 走 Tokio 多執行緒 async runtime,每個 client task 分派到 worker thread pool,backend 連線池在 process 內共享(用 Rust 的 lock-free channel 協調)。單一 process 直接吃到多核心,實測 32 核機器上單 process 就穩定推到 2M QPS,不需要多開 instance。這對 VPS 選型的直接影響是:以前為了避開 pgbouncer 單核瓶頸而挑「更多核心但每核心較弱」的方案不再必要,可以直接選 Intel Gold 那種單核性能好、核心數適中的機型,運算資源比較不會被 proxy 層浪費。

什麼情境該換,什麼情境不用

PgDog 目前的定位建議:新專案、或者已經被 pgbouncer transaction mode 那些限制咬過的服務,值得直接評估。特別是三種情境幾乎無腦換:

第一種是用 Django、Rails、SQLAlchemy 這類 ORM 的專案,這些 framework 預設會在連線建立時發一批 SET,pgbouncer transaction mode 下這些設定會被無聲丟棄,很多莫名其妙的時區、encoding bug 都是這樣來的。第二種是用 LISTEN/NOTIFY 做輕量 job queue 的服務,PgDog 直接省掉多池架構。第三種是準備做水平擴充但還不想踩 Citus 的坑,PgDog 的 sharding 路由夠用又保留退場空間(拿掉 PgDog 底下每個 shard 就是普通 Postgres)。

不建議換的情境:現有 pgbouncer 部署穩定、只用最基礎的 transaction pooling、應用層沒踩到那些破洞的服務,換過去不會有明顯收益,反而多引入一個相對年輕的元件。PgDog 的核心穩定但 sharding 部分官方標為 experimental,重度依賴 sharding 之前需要壓測。

部署上 PgDog 只要兩個 TOML 檔(pgdog.toml 定義 hosts 跟 sharding、users.toml 定義帳密),單一 binary 起來就能跑,Docker image 也有官方版本。監控介面刻意做成跟 pgbouncer 相容的 admin database,加上 OpenMetrics 端點跟 OTEL push,接進現有 Prometheus/Grafana 或 SigNoz 都是零改動。

選對池子等於幫 VPS 省一整層架構

Postgres 的連線池選型長期被 pgbouncer 壟斷,但代價是應用要遷就 transaction mode 的坑、要為 LISTEN/NOTIFY 額外開 session 池、要放棄 sharding 或另外裝 Citus。PgDog 把這幾件事收在一支 Rust binary 裡,同時把單核瓶頸解掉。對正在規劃資料庫層架構的團隊,這是一個能同時省下 proxy 層與應用複雜度的選項。

NCSE Network 的臺灣 VPS 採用 Intel Gold CPU 與 NVMe SSD,適合作為 PgDog 這類多執行緒 Rust 服務的部署節點,也能同時承載 Postgres 與 pooler 的 co-located 架構。如果正在評估把資料庫層搬回自架、或需要為多租戶服務規劃 sharding 路由,歡迎到 NCSE Network 了解 VPS 與 IP Transit 方案。

需要穩定的雲端主機?

NCSE Network 提供企業級 VPS,7 天免費試用,臺灣是方電訊機房,99% SLA 保證。

查看 VPS 方案 →