Kết nối với Database: Giữ Connection hay Pooling chờ sẵn?
Mỗi lần ứng dụng cần truy vấn database, nó phải thiết lập một connection — một kênh giao tiếp riêng giữa app và database server. Nếu bạn mở và đóng connection sau mỗi query, chi phí đó cộng dồn rất nhanh. Bài viết này sẽ giải thích tại sao, và cách Connection.
Tưởng tượng ứng dụng của bạn đang chạy ngon trên production. Traffic tăng dần, mọi thứ ổn. Rồi một ngày đẹp trời, bạn nhận được alert: response time tăng vọt, một số request bắt đầu timeout. Bạn check log — không có slow query nào cả. Vậy vấn đề ở đâu?
Câu trả lời thường bị bỏ qua: cách app quản lý kết nối tới database.
Mỗi lần ứng dụng cần truy vấn database, nó phải thiết lập một connection — một kênh giao tiếp riêng giữa app và database server. Nếu bạn mở và đóng connection sau mỗi query, chi phí đó cộng dồn rất nhanh. Bài viết này sẽ giải thích tại sao, và cách Connection Pooling giúp bạn giải quyết vấn đề đó.
Database Connection là gì, và tại sao nó “đắt đỏ”?
Một database connection không chỉ đơn giản là một socket TCP.
Với PostgreSQL, mỗi lần client kết nối, database sẽ fork một OS process mới chuyên phục vụ client đó — gọi là backend process.
Mỗi backend process sẽ:
Và để thiết lập được connection đó, phải trải qua một loạt bước:
1. Open the connection (takes 1 ms-200 ms depending on network latency)
→ TCP 3-way handshake
→ SSL/TLS negotiation (nếu bật)
→ Authentication (username/password)
→ Backend process spawning
2. Execute SQL Statement
→ send queries
→ receive results
3. Close the connection
→ backend process killed
→ memory freed
→ socket closed
Nếu một query chỉ mất 2ms để thực thi, nhưng bạn phải mở và đóng connection mỗi lần — chi phí thực tế có thể gấp 10–100 lần con số đó.
Vì một lần mở connection có thể tốn từ 1ms (cùng rack trong data center) đến 200ms (khác data center), thậm chí cả giây nếu cross-region.
How long it takes to opening a connection?
Below are rough reference points:
If you are on same data center, same rack.
It take 1-5ms
If you are on same data center, but different rack.
It take 5-20ms
If you are in different data center.
It take 50-200ms
If you are cross region.
It take 800-1500ms
Lưu ý:
PostgreSQL còn có giới hạn cứng về số connection đồng thời, mặc định thường là 100 (cấu hình qua max_connections). Vượt qua con số này, connection mới sẽ bị từ chối.
Connection Pooling là gì?
Thay vì mở–đóng connection mỗi lần query, Connection Pool duy trì sẵn một tập hợp các connection đã được thiết lập. Ứng dụng chỉ cần mượn connection từ pool, dùng xong trả lại — connection không bị đóng, mà được reset và chờ request tiếp theo.
Vòng đời của một pool trông như thế này:
Nên nhớ, Idle connection không miễn phí, mỗi connection dù đang rảnh vẫn chiếm 10–30MB RAM để duy trì trạng thái mở. Đó là lý do tại sao không nên đặt min hoặc max quá cao một cách tùy tiện — bạn đang đánh đổi bộ nhớ để đổi lấy tốc độ.
Với Node.js và PostgreSQL, bạn dùng pg.Pool như sau:
const { Pool } = require("pg");
const pool = new Pool({
host: "localhost",
database: "myapp",
min: 2, // luôn giữ ít nhất 2 connection sống
max: 10, // tối đa 10 connection đồng thời
idleTimeoutMillis: 30000, // đóng connection rảnh sau 30s
connectionTimeoutMillis: 2000, // báo lỗi nếu chờ quá 2s
});
async function getUser(id) {
// tự động mượn connection, query, rồi trả lại pool
const res = await pool.query("SELECT * FROM users WHERE id = $1", [id]);
return res.rows[0];
}
Lưu ý: pool.query() tự động mượn và trả connection — bạn không cần gọi connect() và release() thủ công trừ khi cần chạy nhiều query trong cùng một transaction.
Điều gì xảy ra khi pool hết connection?
Khi tất cả connection đang bận và một request mới đến, nó sẽ được đưa vào hàng đợi. Nếu chờ quá connectionTimeoutMillis, pool sẽ throw error.
Đây là hành vi đúng — fail nhanh còn hơn để queue phình to không kiểm soát, dẫn đến memory exhaustion.
Nếu pool liên tục bị đầy, có hai nguyên nhân chính:
Và điều ngược lại: tăng pool size một cách mù quáng không giải quyết được vấn đề, đôi khi còn làm mọi thứ tệ hơn.
Little's Law — toán học đằng sau connection pool
Connection pooling không chỉ là một software pattern — nó có nền tảng toán học từ lý thuyết hàng đợi, gọi là Little's Law:
L = λ × W
Trong đó:
Ví dụ: App nhận 50 req/s, mỗi request giữ connection 20ms (0.02s):
L = 50 × 0.02 = 1 connection
Chỉ cần 1 connection tại bất kỳ thời điểm nào!
Nhưng nếu một slow query kéo thời gian lên 200ms:
L = 50 × 0.2 = 10 connections
Đột nhiên cần gấp 10 lần. Đây là insight quan trọng: slow query không chỉ làm user chờ lâu hơn — nó trực tiếp đẩy nhu cầu connection lên cao, có thể làm cạn kiệt pool.
Từ "cần bao nhiêu connection" suy ra "pool hiện tại chịu được bao nhiêu request":
max_throughput = pool_size / avg_service_time
Ví dụ: Pool có 10 connections, mỗi query mất 10ms (0.01s):
max_throughput = 10 / 0.01 = 1000 req/s
Vượt quá 1000 req/s, request bắt đầu xếp hàng chờ. Đây cũng là lý do tối ưu query (giảm avg_service_time) hiệu quả hơn nhiều so với tăng pool size — giảm query từ 10ms xuống 5ms thì throughput tăng gấp đôi mà không tốn thêm connection nào.
Kingman's Formula — tại sao 100% utilisation nguy hiểm
Dù pool có đủ connection trung bình, bạn vẫn có thể thấy queue phình to và latency tăng vọt.
Kingman's Formula giải thích điều này: queue length tăng theo hàm mũ khi utilisation tiến gần 100%.
Ngưỡng an toàn thực tế: đừng để pool chạy quá 70–80% công suất.
Khi vượt ngưỡng đó, chỉ cần một đợt traffic nhỏ tăng đột biến cũng đủ tạo ra hàng đợi dài không tương xứng. Đây không phải đặc thù của database — CPU core, network bandwidth đều có cùng hành vi.
Vì vậy, khi thấy pool exhaustion, hành động đầu tiên không phải tăng max — mà là tìm và tối ưu query chậm trước.
Pool nên có bao nhiêu connection?
Đây là câu hỏi thực tế nhất khi cấu hình pool, và câu trả lời xuất phát từ cách database thực sự hoạt động.
Database chỉ có thể chạy song song nhiều nhất bằng số CPU core. Có nhiều connection hơn số core không giúp query nhanh hơn — ngược lại, OS phải liên tục context-switch giữa các process, làm tốn thêm tài nguyên.
Công thức thực tế hay được dùng:
pool_size = (số CPU core × 2) + số ổ đĩa vật lý
Với SSD hoặc cloud database (RDS, Supabase…), coi số ổ đĩa là 1.
Ví dụ: Database server có 4 CPU core, SSD:
pool_size = (4 × 2) + 1 = 9 connections
Nghe có vẻ ít, nhưng 9 connection đang chạy song song ở tốc độ CPU hoàn toàn đủ phục vụ hàng trăm user — vì phần lớn thời gian user đang chờ I/O, không phải chờ CPU.
Lưu ý thêm: Nếu bạn chạy nhiều instance của ứng dụng (scale horizontally), tổng connection = pool_size × số instance. Với 10 app instance, mỗi cái pool 20 connection → 200 connection tổng, dễ vượt quá max_connections của Postgres.
Best practices & những sai lầm hay gặp
✅ Nên làm:
❌ Không nên làm:
Tóm tắt
Connection pooling là một trong những thứ bạn nên thiết lập đúng ngay từ đầu.
