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

Ảnh chụp đoạn mã SQL nền tối minh hoạ max_connections đặt bao nhiêu bị chặn bởi số core không phải RAM, sự thật ngược trực giác throughput bị giới hạn bởi số core CPU nhiều người đặt max_connections cao 500 1000 tưởng phục vụ được nhiều hơn nhưng số truy vấn chạy đồng thời bị chặn bởi số core chỉ ngần đó CPU vượt ngưỡng thêm kết nối làm throughput giảm context switch tranh khoá SHOW max_connections mặc định 100, đo đường cong pgbench tăng dần số client pgbench -S -T 8 -c 1 -j 8 chỉ SELECT in-cache bám CPU pgbench -c 16 pgbench -c 90 quan sát tps đỉnh rồi tụt, công thức thực tế số kết nối đang hoạt động tối ưu khoảng 1,5 đến 3 lần số core tuỳ tải công thức cũ hay dùng connections bằng core nhân 2 cộng số trục đĩa 10 core vùng ngọt khoảng 16-30 kết nối hoạt động không phải hàng trăm, max_connections là trần an toàn không phải mục tiêu đặt max_connections vừa phải 100-200 như một lằn ranh chống cạn tài nguyên không đặt 500-1000 để cho phép nhiều client cách đúng là dùng pooler PgBouncer gom hàng trăm client vào vài chục kết nối thật gần vùng ngọt lưu ý max_connections cao còn cấp sẵn bộ nhớ cấu trúc lock slot lúc khởi động

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:

Ảnh chụp bảng kết quả đo thật nền tối throughput đỉnh ở khoảng 16 client 10 core rồi tụt pgbench -S SELECT in-cache bám CPU 10 core -j 8 PostgreSQL 16 max_connections 100, đường cong TPS theo số client đồng thời 1 client 24,7k 2 client 45,2k 4 client 68,5k 8 client 99,8k 16 client 280k đỉnh khoảng 1,6 lần số core 32 client 266k 48 client 249k 64 client 239k 90 client 225k tụt khoảng 20 phần trăm so với đỉnh, bảng số client 8 TPS 99,8k đang leo 16 khoảng 1,6 lần core 280k đỉnh 32 266k giảm 5 phần trăm 64 239k giảm 15 phần trăm 90 225k giảm 20 phần trăm, vì sao vượt đỉnh lại tụt chỉ có 10 core tối đa 10 truy vấn chạy thực sự song song thêm client bằng thêm tiến trình tranh 10 core đó context switch tranh latch khoá nội bộ cache CPU bị nguội tổng throughput giảm, kết luận cho max_connections đặt max_connections vừa phải trần an toàn không đặt cao để chứa nhiều client giữ số kết nối hoạt động gần vùng ngọt khoảng 1,5 đến 3 lần core bằng connection pooler 500 client ứng dụng pool khoảng 20-30 kết nối thật tới PostgreSQL không phải 500

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ề

  1. 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.
  2. 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.
  3. 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.