Ở nhiều hệ thống backend, PostgreSQL không sập vì query quá phức tạp trước tiên; nó nghẽn vì quá nhiều connection. Mỗi pod Rails, Node.js, Go service, background worker và job scheduler đều mở pool riêng. Khi autoscaling tăng instance, số connection tăng theo cấp số nhân, trong khi max_connections của PostgreSQL là tài nguyên hữu hạn.
PostgreSQL connection pooling với PgBouncer là lớp đệm giúp application không mở trực tiếp quá nhiều server connection vào database. Nhưng PgBouncer không phải “bật lên là xong”. Nếu sizing sai, transaction pooling không tương thích, timeout lệch hoặc thiếu observability, bạn chỉ chuyển điểm nghẽn từ PostgreSQL sang PgBouncer.

Vì sao PostgreSQL connection là tài nguyên đắt?
Mỗi PostgreSQL backend process tiêu tốn memory, scheduler overhead và context switching. Khi max_connections bị đẩy quá cao để chiều lòng application, database có thể mất ổn định ngay cả khi query không nặng. Triệu chứng thường gặp:
- Log xuất hiện
remaining connection slots are reserved. - P95/P99 latency tăng dù CPU chưa chạm 100%.
- Background job tranh connection với web request.
- Migration hoặc admin session không vào được lúc incident.
- Autoscaling làm hệ thống tệ hơn vì mỗi pod thêm một pool mới.
Nếu bạn đã tối ưu index như bài Index trong PostgreSQL nhưng hệ thống vẫn thỉnh thoảng nghẽn, hãy kiểm connection budget trước khi tăng cấu hình mù quáng.
PgBouncer giải quyết vấn đề gì?
PgBouncer là lightweight connection pooler cho PostgreSQL. Application kết nối vào PgBouncer; PgBouncer giữ một số lượng server connection giới hạn tới PostgreSQL và tái sử dụng chúng giữa nhiều client.
Có ba chế độ pooling:
- Session pooling: client giữ server connection trong suốt session. Tương thích cao, tiết kiệm ít hơn.
- Transaction pooling: server connection chỉ được giữ trong thời gian transaction. Hiệu quả cao cho web workload ngắn.
- Statement pooling: trả connection sau từng statement; ít dùng vì hạn chế transaction.
Với web API stateless, transaction pooling thường là lựa chọn đáng cân nhắc. Với workload phụ thuộc session state, prepared statement, temp table hoặc LISTEN/NOTIFY, session pooling hoặc tách service riêng sẽ an toàn hơn.
Cách tính pool size: bắt đầu từ connection budget

Đừng lấy default pool size của framework nhân với số pod rồi hy vọng database chịu được. Hãy làm ngược lại:
usable_connections = postgres_max_connections
- superuser_reserved_connections
- admin_reserved_connections
- migration_reserved_connections
- maintenance_reserved_connections
web_pool_budget = usable_connections * 0.55
worker_pool_budget = usable_connections * 0.30
spare_budget = usable_connections * 0.15
Ví dụ PostgreSQL có max_connections=200. Bạn giữ 10 connection cho superuser/admin, 10 cho migration/maintenance, còn 180 usable. Có thể chia 100 cho web, 55 cho worker, 25 spare. Nếu web có 10 pods, điều đó không có nghĩa mỗi pod được pool 20. Với PgBouncer, bạn đặt default_pool_size hoặc database/user pool size ở phía PgBouncer để tổng server connection không vượt budget.
Nguyên tắc sizing thực dụng
- Pool size nên dựa trên concurrent database work, không dựa trên request per second.
- Giảm transaction duration trước khi tăng pool size.
- Tách web request, background worker, reporting job bằng user/database pool riêng nếu có pattern tải khác nhau.
- Luôn giữ spare connection cho incident response.
- Load test bằng p95 wait time tại PgBouncer, không chỉ HTTP latency.
Cấu hình PgBouncer mẫu cho production
[databases]
appdb = host=postgres.internal port=5432 dbname=appdb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 80
min_pool_size = 10
reserve_pool_size = 20
reserve_pool_timeout = 3
server_idle_timeout = 60
server_lifetime = 3600
query_wait_timeout = 5
client_idle_timeout = 300
server_reset_query = DISCARD ALL
ignore_startup_parameters = extra_float_digits
max_client_conn là số client connection PgBouncer nhận, không phải số connection vào PostgreSQL. default_pool_size mới là giới hạn server connection cho mỗi database/user pool. query_wait_timeout nên đủ thấp để request fail nhanh thay vì xếp hàng vô hạn và gây cascading failure.
Transaction pooling: các gotcha dễ gây lỗi production

Transaction pooling đổi lại hiệu quả bằng việc không đảm bảo cùng một client luôn dùng cùng server connection. Vì vậy, những thứ sống ở session level có thể không hoạt động như bạn nghĩ:
- Prepared statements: nhiều driver/ORM cache prepared statement theo session. Với transaction pooling, cần tắt prepared statement phía client hoặc dùng cấu hình tương thích phiên bản PgBouncer/PostgreSQL đang chạy.
-
Session variables:
SET search_path, timezone, role hoặc custom setting cần reset rõ ràng hoặc đặt ở transaction scope. - Temporary tables: không phù hợp nếu request sau kỳ vọng vẫn thấy temp table cũ.
- Advisory locks: session-level advisory lock dễ sai; ưu tiên transaction-level advisory lock.
- LISTEN/NOTIFY: thường cần session pooling hoặc connection trực tiếp riêng.
Với Rails, nhiều team cần đặt prepared_statements: false khi đi qua transaction pooling. Với Node.js pg, cần kiểm cách query prepared/name statement được dùng. Với Prisma hoặc ORM khác, hãy đọc kỹ support matrix trước khi bật transaction pooling toàn site.
Kubernetes/autoscaling: lỗi phổ biến khi mỗi pod tự mở pool lớn
Khi chạy Kubernetes, lỗi phổ biến là đặt application pool size bằng default 10 hoặc 20, sau đó HPA scale từ 5 lên 40 pods. PostgreSQL đột nhiên nhận hàng trăm connection, đúng lúc traffic đang cao. PgBouncer nên là điểm kiểm soát tập trung hoặc sidecar được sizing theo tổng ngân sách, không phải nơi phóng đại pool.
Một pattern an toàn:
- Application local pool nhỏ: đủ cho concurrency trong process, ví dụ 2-5 nếu transaction ngắn.
- PgBouncer pool theo service/user: web, worker, migration tách nhau.
- HPA scale dựa trên request latency/queue depth, nhưng có guardrail từ DB wait time.
- Migration job dùng connection path riêng hoặc reserved pool, không tranh với web.
Khi traffic vượt khả năng DB, đừng chỉ tăng pod. Hãy áp dụng backpressure/load shedding như bài Backpressure và Load Shedding trong Backend: fail fast, queue có giới hạn, giảm concurrency, bảo vệ database.
Observability: metric cần theo dõi ở PgBouncer

Các lệnh SHOW POOLS, SHOW STATS, SHOW CLIENTS, SHOW SERVERS rất quan trọng. Dashboard production nên có:
-
cl_active,cl_waiting: client đang chạy và đang chờ. -
sv_active,sv_idle,sv_used: server connection tới PostgreSQL. - Average/max wait time trước khi được cấp server connection.
- Pool saturation theo database/user.
- Query duration và transaction duration ở app/DB.
- PostgreSQL locks, CPU, I/O, replication lag để phân biệt nghẽn pool với nghẽn query.
Nếu đã có tracing, thêm span attribute như db.pool.wait_ms, pgbouncer.pool, db.user. Bài Distributed Tracing cho Microservices có thể dùng làm nền để nối wait time vào request path.
Runbook khi connection pool nghẽn
- Kiểm
SHOW POOLS: pool nào cócl_waitingcao? - Kiểm PostgreSQL: CPU/I/O/lock có nghẽn không hay chỉ thiếu server connection?
- Tìm transaction dài: request nào giữ connection lâu?
- Tạm giảm worker concurrency nếu worker tranh connection với web.
- Bật hoặc siết timeout để tránh hàng đợi vô hạn.
- Giữ admin connection path riêng để vẫn điều tra được.
- Sau incident, tính lại budget thay vì chỉ tăng
max_connections.
Checklist trước khi ship PgBouncer
- Biết rõ
max_connectionsvà connection budget sau khi trừ reserve. - Application local pool không nhân vượt budget khi autoscale.
- Đã chọn rõ session/transaction pooling theo workload.
- Đã kiểm prepared statements, temp table, advisory lock, LISTEN/NOTIFY.
- Có timeout: connect, query wait, statement, transaction.
- Web/worker/migration có pool hoặc user tách biệt nếu cần.
- Có dashboard PgBouncer và alert trên
cl_waiting/wait time. - Có runbook bypass/admin connection khi PgBouncer hoặc pool nghẽn.
- Load test với traffic và số pod gần production.
- Document cách rollback về direct connection nếu rollout lỗi.
Kết luận
PgBouncer là công cụ rất mạnh để giữ PostgreSQL ổn định dưới tải thật, nhưng chỉ hiệu quả khi đi kèm connection budget, pool sizing, timeout, compatibility review và observability. Với backend production, mục tiêu không phải là mở được nhiều connection hơn; mục tiêu là giữ số connection vào PostgreSQL đủ thấp, đủ dự đoán được và không để một workload chiếm hết tài nguyên của workload khác.
Nếu bạn đang vận hành microservices, Rails monolith, Node.js API hoặc worker-heavy system, hãy coi connection pooling như một phần của system design, ngang hàng với API contract, index strategy, backpressure và incident runbook.