Connection pool database: vì sao cạn connection
Bài trước bạn đã thấy load balancer chia request và rút instance hỏng ra khỏi vòng khi health check fail. Nhưng có một chi tiết quan trọng: mỗi instance bạn thêm vào đều mở connection riêng tới database. Và database chỉ cho mở một số lượng connection hữu hạn, nên scale ngang app quá tay có thể giết chính database của bạn. Đây là nút thắt tiếp theo sau load balancer.
Sau bài này bạn có thể:
- Giải thích vì sao connection là tài nguyên hữu hạn và vì sao phải pool
- Tính số connection tối đa mà hệ thống được phép dùng
- Tính pool size mỗi instance từ
max_connectionsvà từ tải thực tế - Chẩn đoán 4 nguyên nhân cạn connection thường gặp và cách xử lý
Connection: tài nguyên hữu hạn nằm ở phía database
Một connection tới PostgreSQL không phải thứ miễn phí. Mở nó cần TCP handshake, TLS, xác thực, rồi database cấp phát bộ nhớ riêng cho phiên đó (vài MB mỗi connection). Vì vậy PostgreSQL có tham số max_connections, mặc định là _____. MySQL mặc định 151.
Khi vượt ngưỡng đó, database không "chậm lại", nó từ chối thẳng: FATAL: sorry, too many clients already.
Đó là lý do connection pool tồn tại. Pool giữ sẵn một tập connection và cho request mượn rồi trả lại, thay vì mở mới mỗi lần. Nhưng pool có hai mặt:
- Mặt tốt: tái sử dụng, không tốn handshake mỗi query
- Mặt xấu: pool đầy thì request phải xếp hàng chờ slot. Lúc đó app không báo lỗi ngay, nó chỉ chậm dần, rồi đột ngột timeout.
Đây là điểm nhiều người hiểu sai: cạn pool hiếm khi biểu hiện bằng lỗi rõ ràng. Nó biểu hiện bằng p95 latency leo dốc.
Bẫy scale ngang: pool nhân lên theo số instance
Giả sử database của bạn có max_connections = 100, và bạn dự phòng 10 connection cho superuser, migration, monitoring. Vậy ví dụ chỉ còn 90 connection cho app dùng. Con số 90 này là ngân sách cố định.
Giờ xem điều gì xảy ra khi autoscale:
| Số instance | pool/instance | Tổng connection | Kết quả với ngân sách 90 |
|---|---|---|---|
| 3 | 30 | 90 | Vừa khít, không dư |
| 6 | 15 | 90 | Vừa khít |
| 12 | 15 | 180 | Cạn, khoảng 6 instance bị từ chối |
| 12 | 7 | 84 | An toàn, còn dư 6 |
Công thức duy nhất bạn cần nhớ:
pool mỗi instance = floor((max_connections - reserved) / số instance tối đa)
Với ví dụ trên: floor(90 / 12) = 7.
flowchart TD
LB["⚖️ Load balancer"] --> I1["🖥️ Instance 1<br/>pool 15"]
LB --> I2["🖥️ Instance 2<br/>pool 15"]
LB --> I3["🖥️ Instance N<br/>pool 15"]
I1 --> SUM["🧮 Tổng = 15 x N"]
I2 --> SUM
I3 --> SUM
SUM --> CHK{"Còn chỗ không?"}
CHK -->|Có| OK["✅ Chạy ổn"]
CHK -->|Không| BAD["❌ Instance mới bị từ chối"]
Điều đáng sợ là vòng xoáy: instance thứ 7 không mở được connection, health check DB ping fail, load balancer rút nó ra, tải dồn sang 6 instance còn lại, mỗi instance cần nhiều connection hơn, và cả cụm sập cùng lúc. Scale ngang mà không tính database là cách nhanh nhất để biến traffic tăng thành sự cố toàn hệ thống.
Pool size bao nhiêu là đủ: tính từ tải, không đoán
Trần max_connections chỉ là giới hạn trên. Pool thực sự cần bao nhiêu phụ thuộc vào tải. Dùng Little's law:
connection đồng thời ≈ RPS x thời gian giữ connection (giây)
Ví dụ 1: query nhanh. Một instance nhận 200 RPS, mỗi query giữ connection 20ms (0.02s). Cần 200 x 0.02 = 4 connection đồng thời. Pool 10 là quá dư.
Ví dụ 2: query chậm gấp 10. Cùng 200 RPS nhưng query mất 200ms. Cần 200 x 0.2 = 40 connection. Với pool 10, có khoảng 30 request phải xếp hàng ở mỗi thời điểm, và latency đo được sẽ là hàng đợi cộng thời gian query, không phải 200ms nữa.
Bài học ở đây: _____ theo cấp số nhân, nên tối ưu một query chậm thường rẻ hơn tăng pool.
Còn một công thức tham khảo từ HikariCP cho pool của một instance, dựa trên CPU chứ không dựa trên số người dùng:
pool = (số core CPU x 2) + số disk spindle hiệu dụng
Với máy 4 core, SSD: khoảng 9-10 connection. Con số này nhỏ hơn nhiều so với thói quen đặt pool 50 cho mọi thứ. HikariCP mặc định là 10.
Bốn nguyên nhân cạn connection và cách xử lý
1. Connection leak. Code mở connection trong try mà quên finally, hoặc quên đóng khi gặp exception. Triệu chứng: pool đầy dần theo thời gian, restart app thì hết. Cách xử lý: luôn dùng try-with-resources hoặc defer close(), và bật leak detection của pool.
2. Transaction dài giữ connection. Một transaction mở rồi gọi API bên ngoài, chờ 3 giây, mới commit. Trong 3 giây đó connection bị khóa. Cách xử lý: gọi dịch vụ ngoài trước khi mở transaction, giữ transaction ngắn nhất có thể.
3. Background job dùng chung pool với web. Job export báo cáo chiếm 20 connection trong 5 phút, web hết connection. Cách xử lý: tách pool riêng cho job, hoặc đưa job sang replica.
4. Scale ngang không tính toán. Như phần trên. Cách xử lý: giảm pool mỗi instance khi tăng số instance, hoặc đặt giới hạn cứng max instance trong autoscaling.
Nếu vẫn cần nhiều instance hơn ngân sách cho phép, giải pháp là connection pooler bên ngoài như PgBouncer ở chế độ transaction pooling: hàng trăm connection từ app được gom thành vài chục connection thật tới database. Đây là cách _____ cho phép scale app ngang mà không nới max_connections.
Phân biệt hai kiểu cạn connection khi đọc log:
- Cạn ở app:
Connection is not available, request timed out after 30000ms. Request chờ slot trong pool. - Cạn ở database:
too many clients alreadyhoặctoo many connections. Database từ chối mở connection mới.
Kiểu thứ nhất sửa bằng pool size và query. Kiểu thứ hai sửa bằng tổng ngân sách connection.
Những con số cần kiểm tra trước khi tăng instance
- Đặt timeout chờ slot ngắn (1-3 giây) thay vì 30 giây. Chờ lâu chỉ làm request chất đống, che mất triệu chứng.
- Luôn trừ đi connection dự phòng cho superuser và migration, thường 10% hoặc tối thiểu 5. Nếu không, chính bạn cũng không đăng nhập được để sửa khi sự cố.
- Ghi rõ trong tài liệu autoscaling:
số instance tối đa x pool = ngân sách connection. Đây là biến ít người để ý nhất khi cấu hình scale. - Đừng tối ưu cả ba tầng cùng lúc. Thấy latency tăng thì xác định trước: nó đang chờ slot trong pool, hay đang chờ database, hay đang chờ một dịch vụ khác?
- Một instance chết làm các instance còn lại gánh thêm connection. Kiểm tra kịch bản mất một server có còn nằm trong ngân sách không.
Chốt lại
Connection là ngân sách cố định ở database, không phải thứ vô hạn mà scale ngang sinh ra thêm. Con số cần thuộc là pool mỗi instance = floor((max_connections - reserved) / số instance tối đa), và con số thứ hai là nhu cầu thật, tính bằng RPS x thời gian giữ connection. Cạn pool ở app thì request xếp hàng và chậm dần; cạn ở database thì bị từ chối thẳng.
Bài tiếp theo là checkpoint của module: bạn sẽ ghép load balancer và connection pool lại thành một tình huống thực tế để chẩn đoán xem nút thắt nằm ở tầng nào.
Front: max_connections mặc định của PostgreSQL là bao nhiêu? Back: 100 (MySQL mặc định 151).
Front: Vì sao scale ngang app có thể làm cạn connection database? Back: Mỗi instance giữ một pool riêng, tổng connection = pool x số instance, trong khi database chỉ có ngân sách cố định.
Front: Công thức tính pool size mỗi instance từ giới hạn database? Back: floor((max_connections - reserved) / số instance tối đa).
Front: Connection dự phòng (reserved) dùng để làm gì? Back: Dành cho superuser, migration và monitoring để không bị khóa ngoài khi app chiếm hết.
Front: Little's law áp dụng cho connection pool viết thế nào? Back: connection đồng thời ≈ RPS x thời gian giữ connection tính bằng giây.
Front: Query chậm gấp 10 lần ảnh hưởng gì tới nhu cầu connection? Back: Nhu cầu connection tăng gấp 10, nên tối ưu query thường rẻ hơn tăng pool.
Front: Khi pool đầy, request xảy ra chuyện gì? Back: Request xếp hàng chờ slot, latency tăng dần rồi timeout.
Front: Connection leak là gì? Back: Mở connection nhưng không trả về pool, thường do thiếu đóng trong finally hoặc khi gặp exception.
Front: PgBouncer giải quyết vấn đề gì? Back: Gom nhiều connection từ app thành ít connection thật tới database, thường ở chế độ transaction pooling.
Front: Phân biệt cạn connection ở app và ở database qua log? Back: App: timeout chờ slot trong pool. Database: 'too many clients already'.
Front: Vì sao không nên để timeout chờ slot 30 giây? Back: Request chất đống, latency bị che mất và hệ thống sập chậm hơn khó chẩn đoán hơn.
Front: Background job nên dùng connection thế nào? Back: Dùng pool riêng có giới hạn riêng, hoặc chạy trên read replica, để không giành connection với web.
PostgreSQL mặc định cho phép _____ connection, còn HikariCP mặc định mở pool _____ connection cho mỗi instance.