ADR-0005: PgBouncer làm connection pooler trước Postgres
- Status: Accepted
- Date: 2026-07-25
- Supersedes: —
- Superseded by: —
Bối cảnh
Một Postgres 16 container duy nhất trên data-node phục vụ toàn bộ database
(user_db, field_db, booking_db, payment_db, keycloak_db,
langfuse_db, chatbot_db) qua chung user football_admin
(ADR-0001).
postgres:16-alpine dùng max_connections mặc định = 100, không override ở
đâu trong repo này.
Phía consumer, mỗi NestJS pod giữ TypeORM pool max: 20
(football-booking-backend-k3s/libs/database/src/typeorm.config.ts). Đếm
từ football-booking-gitops/envs/production/backend/, 4 service nối
Postgres trực tiếp (booking-service, field-service, payment-service,
user-service) mỗi service có maxReplicas: 3
(gitops/charts/backend-service/values.yaml). Ở HPA scale full, riêng 4
service này đã có thể mở tới 4 × 3 × 20 = 240 connection — vượt
max_connections=100 chỉ với 2/3 số service scale gần hết. Cộng thêm
go-service (pgxpool) và chatbot-service/LangFuse (dùng chung Postgres
instance này qua chatbot_db/langfuse_db) thì áp lực thực tế còn cao hơn.
Đây là rủi ro production thật, chưa từng gây sự cố nhưng chưa có phòng vệ
nào ở tầng hạ tầng.
Quyết định
Thêm PgBouncer (edoburu/pgbouncer:v1.25.2-p0) vào docker-compose/stateful-stack.yml
(và bản song song stateful-stack.local.yml cho dev), chạy cạnh Postgres
trên data-node, pool_mode=transaction, default_pool_size=20, lắng nghe
port 6432 mở ra ngoài (giống cách 5432 đang mở) để K3s pod trên worker
node kết nối qua MASTER_IP:6432.
Cập nhật 2026-07-25: đã chuyển traffic runtime của user-service,
field-service, booking-service, payment-service (NestJS) và
go-availability-engine sang pgbouncer:6432 (roles/helm_apps/tasks/main.yml
— backend-secrets, go-services-secrets), verify qua SHOW CLIENTS trên
PgBouncer thấy connection thật từ các service này, state idle đúng theo
transaction pooling.
chatbot-service CỐ Ý chưa chuyển — dùng user Postgres riêng chatbot
(khác football_admin), user này chưa có trong userlist.txt của PgBouncer
(image chỉ tự sinh userlist cho 1 user từ DB_USER/DB_PASSWORD). Cần đổi
sang DATABASE_URLS (nhiều user) hoặc thêm auth_query trước khi chuyển
chatbot_db qua pooler — việc riêng, chưa làm ở ADR này.
Ràng buộc bắt buộc đi kèm quyết định này:
- Port
5432của Postgres giữ nguyên, dùng cho migration/admin job và Terraform provider (football-booking-terraform/live/*/postgres/main.tf, cầnsuperuser=trueđểCREATE ROLE/CREATE DATABASE/GRANT— các thao tác DDL này không tương thích với transaction pooling). - Bất kỳ client nào dùng extended query protocol với server-side prepared
statement cache theo connection (vd:
pgx/pgxpoolởgo-service) phải chuyển sang simple query protocol trước khi route qua PgBouncer, nếu không sẽ lỗiprepared statement does not existkhi PgBouncer đổi backend connection giữa các transaction.
Lựa chọn khác đã cân nhắc
bitnami/pgbouncer: từng là lựa chọn mặc định phổ biến, nhưng loại vì thay đổi chính sách phân phối image legacy của Bitnami (2025) khiến các tag cụ thể không còn đảm bảo pull được lâu dài từ registry miễn phí — rủi ro cho một stateful component chạy dài hạn.- Tăng
max_connectionscủa Postgres thay vì thêm pooler: đơn giản hơn (chỉ đổi 1 config), nhưng không giải quyết gốc vấn đề — mỗi connection Postgres tốn ~5-10MB RAM và tăng context-switch overhead khi số connection active cao; chỉ trì hoãn vấn đề tới ngưỡng cao hơn, không loại bỏ. - PgBouncer chạy trong K3s (sidecar hoặc Deployment riêng ở worker node) thay vì Docker Compose ở data-node: nhất quán hơn với hướng chuyển dần workload sang K3s, nhưng loại vì Postgres vẫn là Docker Compose trên data-node — đặt pooler cùng node với DB nó pool tránh thêm 1 network hop qua Kafka-style external listener, và giữ nhất quán với cách Kafka/Redis/ Mongo hiện tại đều expose thẳng từ data-node.
Hệ quả
- Được: giới hạn số connection thật tới Postgres bất kể số pod HPA scale ra
bao nhiêu (
server_active_connectionsbị chặn trần bởidefault_pool_sizethay vì bởi tổngpool.maxcủa từng pod cộng lại); giảm tail latency lúc scale-out vì pod mới không cần tự handshake TCP/auth trực tiếp tới Postgres; tách rõ traffic runtime (6432) khỏi traffic admin/migration/ Terraform (5432), dễ audit hơn. - Đánh đổi: thêm 1 thành phần hạ tầng nữa trên data-node — vốn đã là single
point of failure (ADR-0001)
— PgBouncer down đồng nghĩa mọi service runtime mất kết nối DB dù Postgres
vẫn khỏe. Không giảm tải CPU/IO thật của Postgres, chỉ giảm số connection
vật lý — nếu bottleneck là query chậm thì ADR này không giúp gì. Cần rà
lại mọi nơi dùng session-level state (
SET search_path,pg_advisory_lockgiữ xuyên nhiều statement,LISTEN/NOTIFY) trước khi chuyển traffic của service đó qua PgBouncer, vì các state đó không đảm bảo còn nguyên giữa các transaction dướipool_mode=transaction. - Ảnh hưởng role/playbook:
roles/stateful_stack(deploy service mới, không cần thêm vault secret — tái dùngDB_ADMIN_PASSWORDsẵn có);roles/obs_stack(thêmpgbouncer-exportervàoobservability-stack.yml);roles/monitoring(thêm scrape job + dashboard ConfigMap "PgBouncer — Connection Pools"). Chưa cần chạy lạiroles/helm_appsở giai đoạn này vì chưa đổiDB_HOSTcủa service nào. football-booking-go-service-k3s/pkg/postgres/pool.gođã đổi sangpgx.QueryExecModeSimpleProtocolđể chuẩn bị tương thích PgBouncer transaction pooling trước — không ảnh hưởng hành vi khi vẫn đang nối thẳng Postgres.- Đã deploy thật lên data-node/obs-node/k3s-master (2026-07-25) và verify qua psql. 2 vấn đề phát sinh lúc deploy thật, đã sửa và ghi lại để lần sau không lặp lại:
edoburu/pgbouncer:1.21.0không tồn tại thật trên Docker Hub (đoán nhầm tag) — dùngv1.25.2-p0.- Image mặc định nghe port
5432nội bộ trừ khi setLISTEN_PORT— phải setLISTEN_PORT=6432để khớp port map6432:6432. AUTH_TYPE=md5khiến image lưu password vàouserlist.txtdạng hash MD5 thay vì plaintext, nên PgBouncer không đủ dữ liệu để SCRAM handshake xuống Postgres 16 (wrong password type) — đổi sangAUTH_TYPE=scram-sha-256để image lưu plaintext, PgBouncer tự chuyển đổi sang auth method backend yêu cầu.pgbouncer-exporterdùng biến envPGBOUNCER_EXPORTER_CONNECTION_STRING, KHÔNG phảiDATA_SOURCE_NAME(khác convention củapostgres-exporter) — dùng nhầm khiến exporter fallback về default connection stringlocalhost:6543và báopgbouncer_up=0.
Liên kết
- PR/commit: —
- Docs liên quan:
docker-compose/stateful-stack.yml,docker-compose/stateful-stack.local.yml - ADR liên quan: ADR-0001