PgBouncer & Connection Pooling — vì sao app chậm dù DB vẫn xanh?
Hồi mình mới làm backend với Go, có lần deploy xong app chạy ngon lành được 30 phút, rồi bắt đầu chậm dần, request timeout, rồi sập hẳn. Vào kiểm tra thì thấy database CPU chỉ 20%, RAM còn thừa, disk I/O bình thường. "Ủa DB vẫn xanh mà?".
Thủ phạm? PostgreSQL hết connection. 100 connections của Postgres bị chiếm hết, request mới phải chờ — rồi timeout, rồi client retry, rồi càng tệ hơn.
Từ lần đó mình mới thực sự hiểu: connection pooling không phải là chuyện "có hay không", mà là bạn đang dùng nó đúng hay sai.
Connection Pool là gì?
Mỗi lần app mở kết nối tới PostgreSQL, PostgreSQL fork một process riêng để xử lý. Công thức nổi tiếng:
connections = ((core_count * 2) + effective_spindle_count)
Nhưng trên thực tế, PostgreSQL chạy tốt nhất với khoảng 50-100 connections (tuỳ RAM). Mỗi connection ngốn ~10MB RAM. Nếu bạn mở 500 connections từ app, đó là 5GB RAM chỉ cho connection, chưa kể context switching giữa các process.
Connection pool là lớp trung gian giữa app và database, giúp tái sử dụng connections thay vì mở/tắt liên tục.
PgBouncer — nhẹ, nhanh, hiệu quả
PgBouncer là connection pooler nhẹ nhất cho PostgreSQL. Nó chạy như một process riêng, app kết nối tới PgBouncer, PgBouncer kết nối tới Postgres.
Ba chế độ:
- Session pooling — Giữ connection trong suốt session. Ít khác biệt so với kết nối thẳng.
- Transaction pooling — Chỉ giữ connection trong một transaction. Khuyến nghị nhất.
- Statement pooling — Delay giữa các statement. Hạn chế nhất, ít xài.
Mình hay dùng transaction pooling vì nó tiết kiệm connection nhất. Khi app kết thúc transaction, PgBouncer trả connection về pool cho request khác dùng.
Cài đặt cơ bản
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
pool_mode = transaction
default_pool_size = 25
max_client_conn = 200
Số default_pool_size = 25 nghĩa là PgBouncer chỉ giữ tối đa 25 connection xuống Postgres. App của bạn có thể có 200 clients, nhưng chỉ 25 cái được xử lý đồng thời ở database — số còn lại xếp hàng đợi.
Tune Pool Size
Công thức mình dùng cho hầu hết hệ thống:
pool_size = min(CPU_cores * 2, 50)
Với server 8 cores: pool_size = 16. Với server 16 cores: pool_size = 32.
Tại sao ít vậy? Vì PostgreSQL mỗi connection là một process. Quá nhiều process đồng thời -> context switching -> chậm. Ít connection hơn nhưng mỗi cái làm việc hiệu quả hơn.
Mẹo: Nếu query của bạn nhanh (dưới 10ms), pool_size càng nhỏ càng tốt. Nếu có query nặng (vài giây), bạn cần tính toán riêng — và nên đưa mấy query đó qua một pool riêng hoặc dùng read replica.
Code example: Go với pgx
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
func main() {
// Kết nối tới PgBouncer, không phải Postgres trực tiếp
connStr := "postgres://app:pass@localhost:6432/mydb?pool_min_conns=5&pool_max_conns=25"
pool, err := pgxpool.New(context.Background(), connStr)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
// Query bình thường
var name string
err = pool.QueryRow(context.Background(), "SELECT name FROM users WHERE id=$1", 1).Scan(&name)
}
Lưu ý: trỏ tới port 6432 (PgBouncer), không phải port 5432 (Postgres). pool_max_conns=25 là số connection từ app tới PgBouncer, không phải xuống Postgres.
5 sai lầm thường gặp
-
Kết nối thẳng tới Postgres — Bỏ qua pooler, mở/đóng connection liên tục. Tốn resources kinh khủng.
-
Pool size quá lớn — Nghĩ "càng nhiều càng tốt". Kết quả: database ì ạch vì context switching.
-
Dùng session pooling cho app web — Transaction pooling thường phù hợp hơn. Session pooling giữ connection cả khi app không làm gì.
-
Không monitor — Cứ để default mà không coi có bao nhiêu connection đang dùng, bao nhiêu query đang chờ.
-
Không có timeout — Query treo vô thời hạn, chiếm connection mãi không trả.
Kết luận
Connection pooling không phải là tính năng "sang chảnh" — nó là thiết yếu cho bất kỳ ứng dụng backend nào dùng PostgreSQL, đặc biệt khi scale lên vài chục hoặc vài trăm concurrent requests.
PgBouncer nhẹ đến mức bạn có thể chạy nó trên cùng máy với app. Cài đặt mất 5 phút, nhưng nó có thể cứu bạn khỏi một đêm on-call. Và nhớ: pool size nhỏ thường là pool size đẹp.