連線池管理:PgBouncer、Pgpool-II 與應用層池化完全指南 | PostgreSQL
在 PostgreSQL 的 Fork 模型中,每個連線都會佔用一個獨立的 Backend Process,當連線數過高時,系統資源消耗與 Context Switch 開銷會急遽上升。連線池(Connection Pooling)透過在應用程式與資料庫之間建立中介層,重複利用已建立的連線,大幅降低開銷並提升整體吞吐量。本篇將從 PgBouncer、Pgpool-II 到應用層池化,全面解析連線池管理的實務知識。
為什麼需要連線池?
要理解連線池的必要性,必須先理解 PostgreSQL 的程序模型成本。
每當一個客戶端發起連線請求,Postmaster 會透過 fork() 系統呼叫建立一個全新的 Backend Process:
客戶端應用程式 PostgreSQL Postmaster
│ │
│── TCP connect to :5432 ──────────▶│
│ │ fork() → 新的 Backend Process
│◀─── 認證握手(AuthOK)─────────────│
│◀─── ParameterStatus / ReadyForQuery─│
│ │
│ (每次連線重複以上流程) │
每個連線的成本:
| 成本項目 | 說明 | 估計消耗 |
|---|---|---|
| OS Process Slot | 每個 Backend 佔用一個 PID | 系統 PID 表資源 |
| 程序記憶體 | work_mem、temp_buffers 等基礎開銷 | 5–10 MB/連線 |
| Shared Memory 條目 | ProcArray 中的記錄 | 固定大小 |
fork() 系統呼叫 | 記憶體映射初始化、檔案描述符複製 | 數毫秒 |
| 認證流程 | 密碼驗證、pg_hba.conf 比對、SSL 握手 | 數毫秒至數十毫秒 |
| CPU Context Switch | 連線數越多,OS 排程器負擔越重 | 非線性上升 |
以一個常見的場景來計算:
max_connections = 1000,work_mem = 64MB:
最差情況記憶體消耗 = 1000 × 64MB = 64GB(僅 work_mem)
加上 Local Memory 基礎開銷 = 1000 × 8MB = 8GB
合計 = 72GB → 遠超多數伺服器實際可用記憶體
Linux 的寫時複製(Copy-on-Write, COW)機制雖可降低 fork() 的記憶體複製成本,但無法消除上述固定開銷。這正是「連線池比提高 max_connections 更有效率」的根本原因。
理想架構是透過連線池將數百個應用端連線壓縮為少量的資料庫實際連線:
應用程式伺服器(數百個 Thread/Worker)
│
▼
連線池(PgBouncer)
─────────────────
維持 20–50 個實際連線
│
▼
PostgreSQL(max_connections = 100)
三種 Pooling 模式
連線池軟體通常提供三種模式,差異在於「何時歸還連線給池」:
Session Pooling(最寬鬆):
客戶端連線 ── 整個 Session 期間佔用一個後端連線 ── 斷線後歸還
Transaction Pooling(最常用):
客戶端連線 ── 每個 Transaction 結束後歸還 ── 下次 Transaction 時重新借用
Statement Pooling(最嚴格):
客戶端連線 ── 每個 SQL 執行後立即歸還 ── 下次 SQL 時重新借用
| 模式 | 多路複用效率 | 限制 |
|---|---|---|
| Session | 最低(幾乎無多路複用) | 無,功能完整 |
| Transaction | 高(生產環境首選) | 不支援 Session 級別狀態(SET、LISTEN/NOTIFY、Advisory Lock) |
| Statement | 最高 | 不支援 Multi-statement Transaction、Prepared Statement |
Transaction 模式 是生產環境最常使用的模式,因為絕大多數 Web 應用的資料庫操作都在單一 Transaction 中完成,不依賴跨 Transaction 的 Session 狀態。
PgBouncer 核心設定
PgBouncer 是最廣泛使用的 PostgreSQL 連線池,以 C 語言編寫、單執行緒事件迴圈(libevent)架構、記憶體極低(< 2MB),可輕鬆管理數千條客戶端連線。
安裝
# Debian/Ubuntu
sudo apt install pgbouncer
# macOS(Homebrew)
brew install pgbouncer
# 查看版本
pgbouncer --version
pgbouncer.ini 設定詳解
[databases]
# 語法:別名 = host=主機 port=埠 dbname=資料庫名稱
mydb = host=127.0.0.1 port=5432 dbname=production
# 可設定多個資料庫
analytics = host=replica.internal port=5432 dbname=analytics
# 萬用字元(所有資料庫直接轉發)
* = host=127.0.0.1 port=5432
[pgbouncer]
# 監聽設定
listen_addr = 0.0.0.0
listen_port = 6432
# 認證設定
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# Pooling 模式
pool_mode = transaction
# 連線數量控制
max_client_conn = 1000 # 最大客戶端連線數
default_pool_size = 20 # 每個 pool 的預設後端連線數
min_pool_size = 5 # 最小保持的連線數(避免冷啟動延遲)
reserve_pool_size = 5 # 緊急備用連線數
max_db_connections = 100 # 單一資料庫的最大後端連線總數
# 超時設定(秒)
server_idle_timeout = 600 # 後端閒置連線的最大生存時間
server_connect_timeout = 15 # 與後端建立連線的超時
query_wait_timeout = 120 # 等待可用連線的最大時間
# TLS 設定
server_tls_sslmode = prefer
client_tls_sslmode = prefer
# 日誌
logfile = /var/log/pgbouncer/pgbouncer.log
log_pooler_errors = 1
# 統計資訊
stats_period = 60
使用者認證
# userlist.txt 格式:"username" "md5hash或明文密碼"
# 生成 md5 hash
echo -n "密碼username" | md5sum
# 範例
"appuser" "md5abcdef1234567890abcdef1234567890"
"readonly" "md5xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
更安全的做法是使用 auth_query 從 PostgreSQL 動態查詢使用者憑證:
# pgbouncer.ini
auth_user = pgbouncer_auth
auth_query = SELECT usename, passwd FROM pg_shadow WHERE usename=$1
Transaction Pooling 模式的注意事項
Transaction 模式下,以下功能無法正常運作,因為它們依賴 Session 級別狀態:
-- ❌ SET 指令(下次 Transaction 可能由不同後端處理)
SET search_path = myschema;
-- ❌ LISTEN/NOTIFY(訂閱在 Session 結束時消失)
LISTEN my_channel;
-- ❌ Advisory Lock(Session 級別的鎖無法跨 Transaction 保持)
SELECT pg_advisory_lock(12345);
-- ❌ Prepared Statement(需 Session 保持)
PREPARE my_stmt AS SELECT ...;
解決方案:為需要 Session 功能的應用設定獨立的 pool:
[databases]
mydb_session = host=127.0.0.1 dbname=production pool_mode=session
mydb_txn = host=127.0.0.1 dbname=production pool_mode=transaction
PgBouncer 監控與調校
管理控制台
# 連線到 PgBouncer 管理控制台
psql -h 127.0.0.1 -p 6432 -U pgbouncer pgbouncer
-- 查看所有 pool 的狀態
SHOW POOLS;
-- 關鍵欄位:cl_active, cl_waiting, sv_active, sv_idle, maxwait
-- 查看統計資訊
SHOW STATS;
-- 關鍵欄位:total_xact_count, total_query_count, avg_xact_time, avg_query_time
-- 查看客戶端/後端連線
SHOW CLIENTS;
SHOW SERVERS;
-- 線上重載設定(不中斷服務)
RELOAD;
-- 維護操作
PAUSE mydb; -- 暫停接收新查詢
RESUME mydb; -- 恢復服務
KILL mydb; -- 關閉特定後端連線
關鍵監控指標
| 指標 | 說明 | 警示閾值 |
|---|---|---|
cl_waiting | 等待可用連線的客戶端數 | > 0 需注意,持續 > 5 需增加 pool_size |
maxwait | 等待最久的客戶端等待秒數 | > 30 秒需立即處理 |
sv_idle | 閒置的後端連線數 | 長期為 0 表示 pool_size 不足 |
avg_query_time | 平均查詢時間(微秒) | 依業務而定 |
avg_xact_time | 平均 Transaction 時間 | 過高表示慢查詢或鎖等待 |
設定調校建議
# 高並行 OLTP 應用(如 Web API)
pool_mode = transaction
default_pool_size = 20 # CPU 核心數 × 2
max_client_conn = 2000
min_pool_size = 5
# 分析/報告應用(長查詢)
pool_mode = session
default_pool_size = 10
query_timeout = 3600 # 允許長達 1 小時的查詢
server_idle_timeout = 1800
# 混合環境:用不同 listen port 區分
# pgbouncer_oltp.ini → port 6432,transaction mode
# pgbouncer_analytics.ini → port 6433,session mode
Pgpool-II:連線池 + 中間件
Pgpool-II 除了連線池外,還提供負載平衡、讀寫分離、健康檢查與 Failover 等企業級功能:
┌──────────────────────────────┐
│ Pgpool-II │
│ │
應用程式 ────────▶│ ┌──────────┐ ┌───────────┐ │
│ │ 連線池 │ │ 負載平衡 │ │
│ └──────────┘ └───────────┘ │
│ ┌──────────────────────────┐ │
│ │ 查詢路由(R/W Split) │ │
│ │ SELECT → Replica │ │
│ │ INSERT/UPDATE → Primary │ │
│ └──────────────────────────┘ │
│ ┌──────────┐ ┌───────────┐ │
│ │ 健康檢查 │ │ Watchdog │ │
│ └──────────┘ └───────────┘ │
└──────────────────────────────┘
│ │
┌─────▼────┐ ┌──────▼────┐
│ Primary │ │ Replica 1 │
└──────────┘ └───────────┘
核心設定
# pgpool.conf
listen_addresses = '*'
port = 5432
# 後端節點
backend_hostname0 = 'primary.db.internal'
backend_port0 = 5432
backend_weight0 = 1
backend_hostname1 = 'replica1.db.internal'
backend_port1 = 5432
backend_weight1 = 2 # Replica 接收更多 SELECT 流量
# Pooling 設定
num_init_children = 32 # 子程序數
max_pool = 4 # 每個子程序的後端連線數
# 負載平衡
load_balance_mode = on
# 健康檢查
health_check_period = 10
health_check_user = 'pgpool_health'
自動讀寫分離
Pgpool-II 會自動分析 SQL 語句,將只讀查詢路由到 Replica:
-- 自動判斷路由目標
SELECT * FROM users WHERE id = 1; -- → Replica(讀)
INSERT INTO orders VALUES (...); -- → Primary(寫)
BEGIN; SELECT FOR UPDATE ...; COMMIT; -- → Primary(寫入語意)
SELECT nextval('my_sequence'); -- → Primary(序列操作)
-- 手動指定路由
/*NO LOAD BALANCE*/ SELECT * FROM users; -- 強制走 Primary
Odyssey 與 Supavisor
Odyssey(Yandex 開發)
Odyssey 是多執行緒連線池,設計目標是在超高並行場景下超越 PgBouncer:
# odyssey.conf
listen {
host "*"
port 6432
}
storage "pg_server" {
type "remote"
host "127.0.0.1"
port 5432
}
database "mydb" {
user "appuser" {
authentication "md5"
password "secret"
storage "pg_server"
pool "transaction"
pool_size 20
pool_timeout 4000
pool_ttl 60
}
}
三者比較
| 特性 | PgBouncer | Odyssey | Supavisor |
|---|---|---|---|
| 架構 | 單執行緒(libevent) | 多執行緒 | Actor 模型(Elixir/OTP) |
| 成熟度 | 極高(2007 年起) | 中(2019 年,Yandex 大規模使用) | 較新(Supabase 平台) |
| 記憶體佔用 | 極低(< 2MB) | 較高 | 中等 |
| TLS 彈性 | 全局設定 | 每個 Route 可獨立設定 | 雲端原生 TLS |
| 多租戶支援 | 有限 | 一般 | 原生支援(雲端設計) |
| 適用場景 | 絕大多數生產環境 | 超高並行、精細 TLS 控制 | 雲端原生、多租戶 SaaS |
應用層連線池
除了獨立的連線池程序外,應用程式框架和 ORM 也提供了應用層的連線池功能。
HikariCP(Java/Kotlin)
HikariCP 是 Java 生態系中效能最佳的連線池,Spring Boot 的預設選擇:
// Spring Boot application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=appuser
// HikariCP 設定
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=600000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.leak-detection-threshold=60000
// 程式碼方式設定
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:postgresql://localhost:5432/mydb");
config.setUsername("appuser");
config.setMaximumPoolSize(10);
config.setMinimumIdle(2);
config.setKeepaliveTime(60000); // 防止 idle 斷線
config.setMaxLifetime(1800000); // 30 分鐘後強制重建
HikariDataSource dataSource = new HikariDataSource(config);
SQLAlchemy(Python)
from sqlalchemy import create_engine
# 標準設定(QueuePool,預設)
engine = create_engine(
"postgresql+psycopg2://appuser:secret@localhost:5432/mydb",
pool_size=5, # 核心連線數
max_overflow=10, # 超過 pool_size 時允許額外建立的連線數
pool_timeout=30, # 等待可用連線的超時秒數
pool_recycle=1800, # 連線生存時間(秒)
pool_pre_ping=True, # 借出前先 ping,確保連線有效
)
# 查看連線池狀態
pool = engine.pool
print(f"Pool size: {pool.size()}")
print(f"Checked in: {pool.checkedin()}")
print(f"Checked out: {pool.checkedout()}")
print(f"Overflow: {pool.overflow()}")
TypeORM / Prisma(Node.js/TypeScript)
// TypeORM DataSource 設定
const AppDataSource = new DataSource({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'appuser',
database: 'mydb',
extra: {
max: 10, // 最大連線數
min: 2, // 最小連線數
idleTimeoutMillis: 600000, // 閒置超時
connectionTimeoutMillis: 30000, // 建立連線超時
maxUses: 7500, // 每條連線最多使用次數後重建
},
});
// Prisma(透過 URL 參數設定)
DATABASE_URL="postgresql://user:pass@localhost:5432/db?connection_limit=10&pool_timeout=30"
連線池大小公式
選擇正確的 pool size 是效能調校的關鍵。設太大會浪費資源,設太小會造成排隊。
Hikari 公式
由 HikariCP 作者提出,簡潔有效:
connections = (core_count × 2) + effective_spindle_count
core_count:資料庫伺服器的 CPU 核心數effective_spindle_count:實際磁碟數(SSD 設為 1)
範例:8 核 CPU + SSD
connections = (8 × 2) + 1 = 17
建議 pool_size ≈ 20(取整 + 少量餘量)
利特爾法則(Little’s Law)
L = λ × W
其中:
L = 需要的並行連線數
λ = 每秒到達的請求數(QPS)
W = 每個請求的平均處理時間(秒)
範例:
QPS = 500,平均查詢時間 = 50ms = 0.05s
L = 500 × 0.05 = 25 個並行連線
建議 pool_size = 25 × 1.2 ≈ 30
常見反模式
❌ 反模式:「設大一點比較安全」
max_connections = 500,pool_size = 500
→ 記憶體耗盡,context switch 暴增,效能反而更差
✅ 正確做法:
公式計算 → 監控實際使用 → 逐步調整
先設小(如 20),發現 cl_waiting 持續偏高再增加
實際調校查詢
-- 觀察實際連線使用情況
SELECT COUNT(*) AS active_connections,
state,
wait_event_type
FROM pg_stat_activity
WHERE datname = 'mydb'
AND backend_type = 'client backend'
GROUP BY state, wait_event_type
ORDER BY active_connections DESC;
PgBouncer vs Pgpool-II 選型
| 評估維度 | PgBouncer | Pgpool-II |
|---|---|---|
| 核心定位 | 純連線池 | 連線池 + 中間件功能集合 |
| 效能 | 極高(overhead 最低) | 稍低(功能更複雜) |
| 設定複雜度 | 低(單一設定檔) | 高(數十個選項) |
| 讀寫分離 | 不支援 | 原生支援,自動路由 |
| 負載平衡 | 不支援 | 支援,可設定權重 |
| Failover | 不支援(需搭配 Patroni) | 內建 Watchdog 機制 |
| 記憶體佔用 | 極低(< 2MB) | 中等(多程序架構) |
選型建議:
選 PgBouncer:
✓ 主要需求是連線多路複用
✓ 已使用 Patroni 等工具管理 HA
✓ 應用層已自行處理讀寫分離
✓ 大多數 Web 應用、API 服務(佔 80% 場景)
選 Pgpool-II:
✓ 需要資料庫層的自動讀寫分離
✓ 需要 Pgpool-II 管理 Failover
✓ 整合傳統應用(無法修改程式碼)
✓ 企業環境需完整中間件方案
連線池健康檢查與連線回收
長時間閒置的連線可能因為防火牆或 NAT Gateway 的 idle timeout 被靜默中斷,導致取到已失效的連線:
應用程式 ── 取出閒置 60 分鐘的連線 ──▶ 發送查詢
│
(連線已被 NAT 丟棄)
│
Connection reset by peer
各層的防禦機制:
# PgBouncer 層
server_idle_timeout = 600 # 後端閒置超過 600 秒則關閉
server_check_query = SELECT 1 # 健康檢查查詢
server_check_delay = 30 # 每 30 秒測試一次閒置連線
# SQLAlchemy 層
engine = create_engine(url, pool_pre_ping=True)
-- PostgreSQL 伺服器端
-- postgresql.conf
tcp_keepalives_idle = 60 -- 閒置 60 秒後開始發送 keepalive
tcp_keepalives_interval = 10 -- keepalive 間隔 10 秒
tcp_keepalives_count = 3 -- 3 次無回應後斷線
Prepared Statement 與 Transaction Pooling 的兼容性
Transaction Pooling 模式下,Prepared Statement 是常見的陷阱:
Session A 在 Backend #1:PREPARE my_stmt AS SELECT ...
Transaction 結束,Backend #1 歸還連線池
Session A 下次被分配到 Backend #2
Session A 執行:EXECUTE my_stmt
→ Backend #2 上不存在 my_stmt → ERROR!
解決方案:
# 方案一:PgBouncer 1.21+ 內建追蹤
[pgbouncer]
max_prepared_statements = 100
# 方案二:禁用 Server-side Prepared Statements
engine = create_engine(
url,
connect_args={"options": "-c plan_cache_mode=force_generic_plan"}
)
雲端與 Serverless 環境
在 Serverless(如 AWS Lambda)環境中,連線池的重要性更加突顯:
傳統長駐服務:
1 個容器 × pool_size 5 = 5 個後端連線 ✓ 可控
Lambda 函數:
1000 個並行 Lambda × 嘗試建立連線 = 1000 個同時連線 ❌ 超過 max_connections
解決方案:
Lambda → RDS Proxy(AWS 托管 PgBouncer)→ RDS PostgreSQL
# RDS Proxy 連線字串
# 原本直連:
DATABASE_URL = "postgresql://user:pass@my-db.abc123.rds.amazonaws.com:5432/mydb"
# 透過 RDS Proxy:
DATABASE_URL = "postgresql://user:pass@my-db.proxy-abc123.rds.amazonaws.com:5432/mydb"
監控整合(Prometheus + Grafana)
# prometheus.yml
scrape_configs:
- job_name: 'pgbouncer'
static_configs:
- targets: ['pgbouncer-exporter:9127']
關鍵 Grafana 面板指標:
| 指標 | 說明 |
|---|---|
pgbouncer_pools_client_active_connections | 每個 Pool 的活躍客戶端 |
pgbouncer_pools_client_waiting_connections | 等待中的客戶端(需設警報) |
pgbouncer_pools_server_idle_connections | 閒置後端連線 |
pgbouncer_stats_queries_duration_microseconds | 查詢耗時分佈 |
pg_connections_used / max_connections | 連線使用率(> 80% 需警報) |
高可用架構中的連線池
在 Patroni 等高可用方案中,連線池需配合 Primary 切換:
正常狀態:
PgBouncer → Primary(:5432)
│
└─ 複寫 → Standby(:5432)
Failover 發生後:
方案一:Patroni + HAProxy
PgBouncer → HAProxy(VIP)→ 動態切換後端
方案二:Patroni REST API 觸發 PgBouncer PAUSE/RESUME
方案三:pg_auto_failover + DNS 自動更新
PgBouncer → 固定 DNS → DNS 指向當前 Primary
總結
連線池管理是 PostgreSQL 生產環境部署不可或缺的一環。核心要點:
- Fork 模型成本:每個連線佔用獨立 Process,提高
max_connections不如使用連線池 - PgBouncer 是絕大多數場景的首選,Transaction 模式提供最佳的多路複用效率
- Pgpool-II 適合需要資料庫層自動讀寫分離與 Failover 的企業環境
- Pool Size 公式:
(CPU核心數 × 2) + 磁碟數或利特爾法則計算 - Transaction Pooling 限制:需注意
SET、LISTEN/NOTIFY、Advisory Lock、Prepared Statement 的兼容性 - 健康檢查:多層防禦(PgBouncer
server_check_query+ 應用層pool_pre_ping+ PostgreSQLtcp_keepalives) - 雲端環境:使用 RDS Proxy 或外部 PgBouncer 解決 Serverless 連線暴增問題
下一篇,我們將深入探討 PostgreSQL 監控與診斷——掌握 pg_stat_* 系統視圖、EXPLAIN ANALYZE 進階用法、Prometheus + Grafana 監控建置,以及效能瓶頸的系統化排查方法。