這篇涵蓋 PostgreSQL connection scaling 的根因:為什麼 PG 比多數 DB 更需要 pooler,跟 pgbouncer-config 是根因與配置的關係。


Connection-per-Process Model 是 PG 的結構性選擇

PG 接受 client connection 時的行為跟多數現代 DB 不同:每個 connection 由 postmaster fork() 一個獨立的 OS process(backend)來服務。這個 process 在 connection lifetime 內專屬該 client、不跟其他 client 共享。

對比常見 DB 的 connection model:

VendorConnection model每 connection 資源
PostgreSQLProcess-per-connection(fork)5-15MB RAM、獨立 PID
MySQLThread-per-connection256KB-2MB RAM、共享 process
OracleShared server / dedicated 可選配置決定
SQL ServerThread-per-connection(pooled)~512KB
MongoDBThread-per-connection~1MB

PG 選 process 不選 thread 是 1990s 設計決定 — 當時 thread library 在多 UNIX 平台不穩定、process 隔離性更好(一個 backend crash 不會帶倒整個 DB)。這個 trade-off 一路保留到今天、是 PG 在 high-connection-count workload 的 結構性負擔。

量化:connection 數量對 RAM 跟 CPU 的壓力

一個 PG backend process 的 RAM footprint 由三部分組成:

1backend_rss ≈ shared_buffers_attach + process_private + work_mem 高水位

shared_buffers 是所有 backend 共享的、不重複計、但 process_private(catalog cache / plan cache / temp buffer)跟 work_mem 是 per-backend:

Workload 類型process_privatework_mem 高水位單 backend RAM
Idle / 簡單 OLTP3-5MB4MB7-9MB
中等 query(join / sort)5-8MB16-64MB21-72MB
Heavy analytical(CTE / window)8-15MB256MB+264MB+

500 個 connection、平均 30MB 各 ≈ 15GB RAM 給 backend processes(還沒算 shared_buffers)。這是 PG 在 cloud instance 上很快撞到 RAM ceiling 的根因。

CPU 層面、fork() 系統呼叫在 Linux 通常 1-3ms、context switch ~3-5μs。100 connection burst 在 1 秒內進來、accumulated fork cost 100-300ms、加 query 本身的 CPU 跟 scheduler latency、平均 query 延遲會跳 2-5x。

三個 GUC 互動:max_connections / shared_buffers / work_mem

PG 的 memory 規劃由這三個 GUC 互動決定、不能獨立調:

1total_RAM ≈ shared_buffers + max_connections × (process_private + work_mem 高水位) + OS overhead

實務 sizing 規則(16GB instance、OLTP workload):

GUC建議值理由
shared_buffers25% RAM(4GB)太大 OS file cache 收益遞減、< 25% wastes RAM
work_mem8-32MB每 query operation 用一份、不是每 connection 一份
max_connections100-200超過 200 需 pooler、不是調更大
effective_cache_size50-75% RAMplanner 估 cost 用、不是實際配置
maintenance_work_mem64-512MBVACUUM / CREATE INDEX 用

max_connections = 1000 是常見 anti-pattern — 真實 active query 可能只 50-100、剩下都 idle、但每個還是吃 RAM 跟 process slot、context switch overhead 還在。

Pooler 為什麼是 production prerequisite

本段是「為什麼必裝」、實際 PgBouncer 配置看 pgbouncer-config。

Pooler 的核心責任是 把 N 個 application connection multiplex 成 M 個 PG backend(M ≪ N):

1Application (3000 connection)
2   ↓
3Pooler(PgBouncer / PgCat)
4   ↓
5PostgreSQL (50 backend process)

Application 看到的是 無限 connection 池、PG 看到的是 穩定 50 個 backend。三個層次的效益:

  1. RAM 節省:3000 connection × 30MB = 90GB → 50 backend × 30MB = 1.5GB
  2. Fork() cost 攤平:backend 重用、不是每個 client 都 fork
  3. Connection storm 緩衝:application 重啟 / scaling event 不會直接打到 PG

Pooler 有三種 pool mode、各有 application 層相容性 trade-off:

Pool modeSession 隔離適用 applicationPG feature 限制
Session每 client 獨佔 1 backend用 prepared statement、SET、temp table等同沒 pool、僅救 fork cost
Transaction每 transaction 換 backend多數 stateless API(最常用)不能用 session-level state
Statement每 statement 換 backendRead-only / analytical不能用 transaction

Production 多數選 transaction pool — 救 RAM 又保留 transaction semantics、代價是 application 不能用 session-level SET、LISTEN/NOTIFY、prepared statement(部分 pooler 已支援)。

Application-side Pool vs Middleware Pool vs RDS Proxy

三層 pool 都能解 connection 問題、但解的問題不同:

層級代表解的問題限制
Application-side(driver)HikariCP(Java)/ pgx pool(Go)/ asyncpg / SequelizeConnection 重用 + lifecycle 管理仍每 app instance 開 N 個到 PG、總量沒收斂
Middleware poolerPgBouncer / PgCatMultiplex 所有 application instance 到少數 backend多一跳 latency 0.1-1ms、需自管 HA
Cloud-managed proxyRDS Proxy / Cloud SQL ProxyMultiplex + IAM auth + Secrets Manager integrationLatency 1-3ms、cost premium、PG feature 受限

典型 production 拓撲:

1Application (HikariCP pool 10/instance × 50 instance = 500)
2   ↓
3PgBouncer transaction pool(50 backend)
4   ↓
5PostgreSQL primary

Application pool 救 fork cost、PgBouncer 救 backend 總量、兩層各做各的事不衝突。

雙層 pool 配置容易出錯:application pool size 5 + PgBouncer default_pool_size 50 + 100 個 app instance、application 最多開 500 個 connection、PgBouncer 只給 50 個 backend。transaction pool 下,500 個 connection 本身不會等待,會等待的是同時進行中的 transaction:超過 50 個時,多出來的 transaction 在 PgBouncer 排隊,看起來像「DB 慢」但實際是 pool 不足。所以同樣是 500 對 50,上面的典型拓撲成立與否,取決於同時進行中的 transaction 有沒有超過 50。

Production 踩雷

Connection storm(重啟 / autoscale 同時打進來)

情境:Kubernetes rolling restart、200 個 pod 同時重連、每 pod 開 20 個 connection、瞬間 4000 個 connection 嘗試打到 PG。

PG max_connections = 500 直接拒絕 3500 個、application 看到 FATAL: sorry, too many clients already、retry storm 雪上加霜。

修法:

  • PgBouncer 在前面、application 連 PgBouncer 不直連 PG
  • reserve_pool_size = 5 配 reserve_pool_timeout:client 等候超過 timeout 時,PgBouncer 對這個 pool 再多開最多 5 個 backend 消化重連尖峰
  • Application 端加 jittered exponential backoff、避免 retry 同步

fork() cost 在 burst 流量

情境:Cron job 每分鐘整點觸發、500 個 worker 同時開 short-lived connection 跑 30ms query、結束關閉。

每分鐘 500 次 fork() + 500 次 exit()、fork cost 500-1500ms、CPU spike、其他 OLTP query 延遲飆。

修法:

  • Worker 改 connect 到 PgBouncer transaction pool、backend 重用、fork 只在 PgBouncer 首次拓展時
  • 或 worker 改成 long-lived process + 內部 task queue、避免每分鐘重 fork

shared_buffers 跟 max_connections 互相壓縮

情境:16GB instance、shared_buffers = 8GB(50%)、max_connections = 800、work_mem = 16MB。

預估 RAM:8GB + 800 × ~30MB = 32GB ≫ 16GB instance,connection 全數用上時 Linux OOM killer 會殺掉 backend process。

修法(重新分配):

1shared_buffers = 4GB           # 25%
2max_connections = 200          # 透過 PgBouncer multiplex
3work_mem = 16MB
4effective_cache_size = 12GB
5maintenance_work_mem = 512MB

關鍵:拓展 application 容量要調 PgBouncer pool size,而不是把 max_connections 調更大。

Double-pool 配置失敗

情境:Application HikariCP pool size = 50、50 個 instance、PgBouncer default_pool_size = 20、PG max_connections = 100。

Application 願意開 2500 個 connection、PgBouncer 只給 20 個 backend、application thread 大量 block 在 PgBouncer 等 backend 釋出。

修法:

  • 計算 application 願意的並發 vs PgBouncer 允許的 backend vs PG max_connections 三層匹配
  • 通常 application_total_connection ≪ pgbouncer_max_client_conn + pgbouncer_default_pool_size + reserve ≪ pg_max_connections
  • Monitor PgBouncer SHOW POOLS 的 cl_waiting、長期 > 0 表示 pool 不足

max_connections 設太大反而慢

情境:team 看到 connection refused、把 max_connections 從 200 調到 2000、想說「給更多 connection 應該更好」。

調完 throughput 反而降 30% — context switch overhead、planner cache 競爭、lock manager 競爭都跟 connection 數線性放大。

修法:

  • max_connections 上限通常 200-500、超過要靠 pooler multiplex
  • 用 pg_stat_activity 看真實 active connection(state != ‘idle’)、通常 < 100
  • 真實上限 = active 高水位 × 安全係數 1.5、不是「未來可能會用到的數量」

跟 MySQL connection model 對比

維度PostgreSQLMySQL
Connection 模型Process-per-connection(fork)Thread-per-connection
單 connection RAM5-15MB(idle)/ 30-200MB(heavy)256KB-2MB
Fork / spawn cost1-3ms< 100μs
Pooler 必要性強烈必要(300+ connection 必裝)中等(ProxySQL 對特定 case 有用)
主流 poolerPgBouncer / PgCatProxySQL / MySQL Router

MySQL thread-per-connection model 讓它在 high-connection-count workload 上 看起來 更省 — 但 PG 透過 PgBouncer 達到的 application 看到的容量跟 MySQL 直連是一樣的、只是多一層 indirection。

實務影響:

  • MySQL 直連 1000 connection 還 OK、PG 直連 1000 connection 通常 OOM
  • PG + PgBouncer 1000 application connection、後端 50 backend、表現跟 MySQL 1000 直連相當
  • 沒有 PG 更耗 RAM 的本質結論、是 PG 預設不 multiplex、需要外掛 multiplex 層

PG 17+ 的 connection 進展

PG 17(2024)對 connection 仍維持 process-per-connection。對照 PG 17 的官方 release notes,這一版沒有降低每條 connection 記憶體或 process 數的條目;與 backend 負載相關的改進在 I/O 側:

  • io_combine_limit:把相鄰 block 的檔案系統讀取合併成一次,降低 syscall 次數;每條 connection 仍是一個 backend process

logical replication subscriber 的 parallel apply(CREATE SUBSCRIPTION ... WITH (streaming = parallel),由 parallel apply worker 套用大交易)是 PG 16(2023)加入的功能。

但 process-per-connection model 本身 沒換 — 短期內 PG 仍需 pooler。長期方向(PG 18+ 討論)可能引入 thread-based backend、但目前是 experimental patch。

相關連結

下一步