Hai bài trước cho thấy mỗi kết nối tốn RAM (nhất là khi hoạt động) và tốn chi phí thiết lập. Câu hỏi thực dụng tiếp theo: vậy max_connections nên đặt bao nhiêu? Trực giác thường sai ở đây — nhiều người đặt 500 hay 1000 để "phục vụ được nhiều client hơn". Bài này đo thật đường cong throughput và cho thấy vì sao con số đúng nhỏ hơn bạn nghĩ nhiều, và phụ thuộc số core CPU chứ không phải RAM.
Sự thật ngược trực giác: throughput bị chặn bởi số core
Điểm mấu chốt: số truy vấn thực sự chạy song song bị giới hạn bởi số core CPU — máy 10 core chỉ chạy được tối đa 10 truy vấn cùng lúc. Thêm kết nối vượt ngưỡng đó không tạo thêm năng lực xử lý; nó chỉ tạo thêm tiến trình tranh nhau cùng số core, và cái giá là context switch, tranh khóa nội bộ, cache CPU bị nguội.
SHOW max_connections; -- mặc định 100

Hình 1: Throughput bị chặn bởi số core, không phải RAM. Đo bằng pgbench tăng dần số client để thấy đỉnh rồi tụt. Công thức thực tế: kết nối hoạt động ~1,5-3× số core. max_connections là trần an toàn, dùng pooler để giữ số kết nối gần vùng ngọt.
Đo thật: đỉnh ở ~16 client, rồi tụt
Tôi chạy pgbench -S (chỉ SELECT, dữ liệu in-cache nên bám CPU) trên máy 10 core, tăng dần số client đồng thời và ghi TPS:

Hình 2: TPS leo từ 24,7k (1 client) lên đỉnh 280k ở 16 client (~1,6× số core 10), rồi tụt dần: 32→266k, 64→239k, 90→225k (giảm ~20% so với đỉnh). Thêm kết nối sau đỉnh làm throughput giảm.
Đường cong rất rõ: TPS tăng nhanh khi số client còn ít hơn số core, đạt đỉnh 280k ở khoảng 16 client (~1,6 lần số core), rồi giảm dần khi tiếp tục thêm client — xuống 225k ở 90 client, mất ~20% so với đỉnh. Đây là đường cong "knee" kinh điển của mọi hệ thống bị giới hạn bởi tài nguyên chia sẻ.
Vì sao vượt đỉnh lại tụt? Chỉ có 10 core, nên tối đa 10 truy vấn chạy thực sự song song. 90 client nghĩa là 90 tiến trình backend tranh nhau 10 core — hệ điều hành phải liên tục context switch, các backend tranh latch/khóa nội bộ của PostgreSQL, và cache CPU bị nguội liên tục. Tổng throughput vì thế giảm, dù bạn đã thêm rất nhiều kết nối.
Công thức và cách đặt đúng
Kết nối đang hoạt động tối ưu thường nằm quanh 1,5 đến 3 lần số core (tùy tải: đọc thuần bám CPU thì gần số core, tải có chờ I/O thì cao hơn). Công thức cũ hay được trích: connections ≈ (số core × 2) + số trục đĩa. Với máy 10 core, vùng ngọt là khoảng 16-30 kết nối hoạt động — không phải hàng trăm.
Điều then chốt: max_connections là trần an toàn, không phải mục tiêu. Đặt nó vừa phải (100-200) như một lằn ranh chống cạn tài nguyên. Đừng đặt 500-1000 với hy vọng phục vụ nhiều client hơn — như đo ở trên, điều đó chỉ làm throughput giảm. Cách đúng để phục vụ 500 client ứng dụng là dùng connection pooler (PgBouncer): nó gom 500 kết nối client vào ~20-30 kết nối thật tới PostgreSQL, giữ số kết nối hoạt động gần vùng ngọt. Đó là chủ đề bài sau.
Đánh đổi cần cân nhắc
Vùng ngọt phụ thuộc tải, phải đo trên hệ thống thật của bạn. Con số ~16 ở đây là cho tải đọc thuần bám CPU. Tải OLTP thật có ghi (chờ WAL fsync), chờ khóa, chờ I/O — các truy vấn "chờ" không chiếm core, nên vùng ngọt có thể cao hơn (số kết nối hoạt động tối ưu lớn hơn số core). Luôn chạy đường cong pgbench với tải giống thật để tìm đỉnh của chính bạn.
max_connections cao tốn bộ nhớ ngay cả khi không dùng. Mỗi slot kết nối cấp sẵn cấu trúc trong shared memory (lock table, proc array) lúc khởi động. Đặt max_connections=5000 lãng phí bộ nhớ đó dù bạn không bao giờ dùng hết. Thêm nữa, max_connections cao mở đường cho nguy cơ hàng trăm kết nối cùng hoạt động làm sập throughput như đo ở trên.
Không hạ max_connections quá thấp gây từ chối kết nối. Ngược lại, đặt quá thấp thì client bị lỗi "too many clients" khi có đợt tăng đột biến. Trần an toàn nên đủ rộng để hấp thụ đỉnh kết nối (kể cả kết nối idle từ pooler và các job nền), chỉ là không nên coi nó là đòn bẩy throughput.
Ba ý mang về
- Throughput bị chặn bởi số core CPU, không phải RAM hay max_connections: đo thật trên máy 10 core, TPS đạt đỉnh 280k ở ~16 client (~1,6× core) rồi tụt xuống 225k ở 90 client (giảm ~20%) vì context switch và tranh khóa.
- max_connections là trần an toàn, không phải mục tiêu: đặt cao (500-1000) không tăng throughput mà còn giảm nó và lãng phí bộ nhớ shared — giữ số kết nối hoạt động gần vùng ngọt ~1,5-3× số core.
- Dùng connection pooler để phục vụ nhiều client: 500 client ứng dụng nên gom vào ~20-30 kết nối thật qua PgBouncer, không phải đặt max_connections=500 — và luôn đo đường cong pgbench trên tải thật để tìm đỉnh của mình.
Phần sau ta đi vào công cụ giải quyết chính vấn đề này: Phần sau mổ xẻ PgBouncer — cách nó gom hàng trăm kết nối client vào vài chục kết nối thật, ba chế độ pooling, và khi nào dùng chế độ nào.