Khi lượng truy cập website tăng vọt, phản xạ tự nhiên của hầu hết lập trình viên là nâng giới hạn tài nguyên. Nếu hệ thống backend đang nhận 1.000 request đồng thời, ta thường nghĩ rằng connection pool tới database cũng cần 200 hay 500 kết nối để tránh nghẽn. Thế nhưng trên môi trường production, việc đẩy max_connections lên cao thường không làm câu truy vấn chạy nhanh hơn. Thay vào đó, CPU của máy chủ cơ sở dữ liệu vọt lên 100%, độ trễ truy vấn tăng đột biến và hệ thống rơi vào trạng thái tê liệt.
Khả năng xử lý đồng thời của database không tăng tuyến tính theo số lượng kết nối mở. Hiểu rõ kiến trúc vận hành bên dưới của PostgreSQL sẽ giúp chúng ta thấy vì sao một connection pool nhỏ, được tính toán vừa vặn với phần cứng, lại đem lại thông lượng vượt trội hơn nhiều so với một cấu hình quá khổ.
Chi Phí Của Mô Hình Process-Per-Connection
Khác với các web server hiện đại sử dụng coroutine hoặc lightweight thread để xử lý hàng vạn request cùng lúc, PostgreSQL sử dụng kiến trúc truyền thống: mỗi kết nối là một tiến trình riêng biệt của hệ điều hành (process-per-connection). Mỗi khi có client kết nối vào, PostgreSQL sẽ fork một tiến trình backend mới để quản lý trạng thái phiên, phân tích cú pháp SQL và thực thi câu lệnh.
Mỗi tiến trình backend này tiêu tốn từ 5 đến 10 MB RAM chỉ để duy trì phiên, chưa kể dung lượng bộ nhớ work_mem cấp phát động khi thực hiện các phép sắp xếp (sort) hay hash join. Khi ứng dụng mở 400 kết nối đồng thời, ta đang ép nhân Linux phải liên tục điều phối 400 tiến trình nặng nề này qua lại giữa các nhân CPU.
Nếu máy chủ database có 8 vCPU, thì tại một thời điểm vi mô bất kỳ, phần cứng chỉ có thể thực thi chính xác 8 luồng lệnh song song. 392 tiến trình còn lại buộc phải đứng chờ trong hàng đợi của hệ điều hành. CPU lúc này phải liên tục lưu ngữ cảnh, nạp lại thanh ghi, xóa cache L1/L2 và giải quyết tranh chấp khóa bộ nhớ. Thay vì dành toàn bộ sức mạnh để chạy câu lệnh SQL, phần lớn chu kỳ CPU bị lãng phí vào việc chuyển đổi ngữ cảnh (context switching).
Công Thức Tính Size Pool Chuẩn Thực Chiến

Cộng đồng PostgreSQL và tác giả của thư viện HikariCP đã đúc kết một công thức thực nghiệm kinh điển để định cỡ connection pool:
optimal_pool_size = (cpu_cores * 2) + effective_spindle_count
Với hạ tầng đám mây hiện nay dùng ổ cứng thể rắn NVMe SSD, effective_spindle_count thường được tính bằng 1 do SSD không có đầu đọc cơ học quay vòng như ổ HDD truyền thống.
Áp dụng cho một máy chủ database 8 core CPU, kích thước pool lý tưởng chỉ là:
(8 * 2) + 1 = 17 kết nối
Con số 17 hay 20 kết nối cho một hệ thống phục vụ hàng nghìn người dùng nghe có vẻ quá ít và dễ gây lo ngại. Nhưng hãy làm một phép tính: nếu một câu query tối ưu mất trung bình 2 mili-giây để hoàn thành trên CPU không bị nghẽn, một kết nối có thể xử lý xong 500 query mỗi giây. Một pool gồm 20 kết nối hoàn toàn có thể gánh tới 10.000 query mỗi giây mà không gặp bất kỳ hiện tượng suy giảm hiệu năng nào.
Nghẽn Hàng Đợi: Trong App Hay Trong Database?
Khi lưu lượng truy cập tăng đột biến, số lượng query chắc chắn sẽ vượt quá năng lực xử lý tức thời của phần cứng. Vấn đề cốt lõi của kiến trúc là: ta nên để hàng đợi tích tụ ở đâu?
Khi bạn mở connection pool quá lớn, hàng đợi bị đẩy thẳng vào nhân database. Hàng trăm query tranh giành băng thông I/O, khóa bộ nhớ dùng chung (LWLock) và tài nguyên CPU cùng một lúc. Mọi câu lệnh đều chạy chậm lại và kéo theo độ trễ trung bình của toàn bộ hệ thống tăng vọt.
Ngược lại, khi giới hạn pool ở mức vừa phải, hàng đợi sẽ nằm lại an toàn trong bộ nhớ của ứng dụng hoặc middleware trung gian. Các tiến trình ứng dụng chỉ cần chờ vài mili-giây để nhận kết nối trống. Database tập trung xử lý từng đợt truy vấn với hiệu suất tối đa của phần cứng rồi trả kết nối ngay lập tức cho client tiếp theo. Việc xếp hàng trong RAM của Node.js, Go hay Java gần như không tốn chi phí; nhưng xếp hàng bên trong engine PostgreSQL sẽ đánh sập toàn bộ hệ thống.
Giải Quyết Bài Toán Scale Out Bằng PgBouncer
Trong kiến trúc microservice hoặc khi scale nhiều container ứng dụng theo chiều ngang, việc mỗi container giữ 10 kết nối cũng nhanh chóng biến thành 300–400 kết nối thực tới PostgreSQL.
Để giải bài toán này, giải pháp tiêu chuẩn là đặt PgBouncer làm proxy trung gian phía trước database ở chế độ gom nhóm giao dịch (transaction pooling):
; /etc/pgbouncer/pgbouncer.ini
[pgbouncer]
listen_port = 6432
pool_mode = transaction
max_client_conn = 2000
default_pool_size = 20
reserve_pool_size = 5
Với mô hình này, hàng nghìn container có thể giữ kết nối TCP mở liên tục tới PgBouncer. PgBouncer lưu các yêu cầu trong bộ nhớ và luân phiên điều phối qua đúng 20 đến 25 kết nối thực tới PostgreSQL. Ngay khi một transaction kết thúc, kết nối đó lập tức được bàn giao cho request khác mà không cần ngắt kết nối vật lý.
Những Lưu Ý Cần Kiểm Tra Ngay
Để tối ưu hệ thống cơ sở dữ liệu hiện tại, bạn có thể áp dụng ngay những bước sau:
- Đo đạc trước khi tăng giới hạn: Kiểm tra view
pg_stat_activityđể xem tỷ lệ kết nốiactiveso vớiidle. Nếu thấy số kết nối active tăng mà thời gian thực thi query cũng tăng theo, pool của bạn đang bị đặt quá lớn. - Thiết lập Connection Timeout ngắn: Đặt thời gian chờ lấy kết nối ở phía client khoảng 2–3 giây thay vì để vô hạn nhằm tránh tình trạng request client bị treo cứng.
- Dọn sạch trạng thái
idle in transaction: Tránh việc gọi API bên thứ ba (như cổng thanh toán hay email) khi đang giữ mở transaction database. Điều này khóa chặt kết nối trong pool mà không thực hiện công việc nào hữu ích.
Giới hạn kích thước connection pool nghe có vẻ đi ngược lại trực giác, nhưng việc gắn chặt mức độ đồng thời với năng lực vật lý của CPU chính là bí quyết giúp hệ thống đạt thông lượng cao nhất và luôn ổn định dưới tải nặng.



Đang tải bình luận…