9.4 KiB
9.4 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| db-connection-pooling-patterns | Connection Pooling — PgBouncer / Pool / Statement | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Connection Pooling Patterns
Postgres connection 가 expensive. App pool (small) + PgBouncer (transaction pool, 1000+ client). Lambda / serverless = HTTP driver / RDS Proxy.
📖 핵심 개념
- App pool: 매 process 가 N connection.
- External pool: PgBouncer 가 multiplex.
- Pool mode: session / transaction / statement.
- Limit: Postgres 의 max_connections.
💻 코드 패턴
App pool (간단)
import { Pool } from 'pg';
const pool = new Pool({
connectionString,
max: 20,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
});
// 모든 query 가 pool 사용
await pool.query('SELECT * FROM users');
→ App instance 당 N. 큰 traffic = 큰 N — Postgres 한계.
Postgres max_connections
SHOW max_connections;
-- Default: 100. 매 connection ~= 10 MB RAM.
-- Heavy production:
ALTER SYSTEM SET max_connections = 200;
→ 매 connection 의 cost (memory + process). Limit 있음.
Pool 크기 결정
규칙 (basic):
max = (CPU 코어 × 2) + effective_spindle
DB 4 core SSD:
- 작은: 5-10 per app instance
- 일반: 20-30
- 큰: 50
App instance × pool max < Postgres max_connections.
e.g. 10 instance × 20 = 200 < 250.
PgBouncer (외부 pool)
# pgbouncer.ini
[databases]
app = host=primary-db port=5432 dbname=app
[pgbouncer]
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction # session / transaction / statement
max_client_conn = 1000 # client → pgbouncer
default_pool_size = 25 # pgbouncer → Postgres
reserve_pool_size = 5
server_idle_timeout = 600
→ App 가 PgBouncer (port 6432) 호출. PgBouncer 가 Postgres connection multiplex.
Pool modes
Session:
- Client 가 connection 점유 (release 까지)
- 모든 feature OK
- Multiplex 안 됨
Transaction:
- Transaction 끝 마다 release
- ~10x more efficient
- 일부 feature X (prepared statements, advisory locks)
Statement:
- 매 statement 끝 release
- 가장 efficient
- 더 많은 feature X (transactions X)
→ 보통 transaction.
Transaction mode 의 함정
Session-bound features 안 됨:
- SET (variable)
- LISTEN / NOTIFY
- Prepared statements (자체 prepare)
- Advisory lock (xact 만 OK)
- Cursor (WITH HOLD)
- Temporary table (보통)
→ 일반 query 는 OK.
// Workaround: prepared statements off
const pool = new Pool({
connectionString: 'postgres://user:pw@pgbouncer:6432/app',
// 자체 prepare 비활성
});
// node-postgres
client.query({ name: 'q', text: '...' }); // pgbouncer transaction mode 깨짐 가능
// Use raw query
client.query('...');
node-postgres + PgBouncer
import postgres from 'postgres';
const sql = postgres(url, {
max: 10,
prepare: false, // pgbouncer transaction mode
});
// pg
const pool = new Pool({
connectionString,
// statement_timeout 매 connection
statement_timeout: 30_000,
});
pool.on('connect', (client) => {
client.query('SET application_name = "my-app"'); // session 별
});
RDS Proxy (AWS)
// Lambda → RDS Proxy → RDS
// 자동 connection pool + auth + IAM
// Same code 가 그냥 endpoint 변경
const pool = new Pool({
host: 'my-proxy.proxy-xxx.rds.amazonaws.com',
// ...
});
→ Lambda + Postgres 의 답.
Hyperdrive (Cloudflare)
// wrangler.toml
[[hyperdrive]]
binding = "HYPERDRIVE"
id = "..."
import postgres from 'postgres';
export default {
async fetch(req: Request, env: Env) {
const sql = postgres(env.HYPERDRIVE.connectionString);
const r = await sql`SELECT * FROM users WHERE id = ${id}`;
return Response.json(r);
},
};
→ Hyperdrive = pool + cache. CF Workers 에서 일반 Postgres.
Neon HTTP driver
import { neon } from '@neondatabase/serverless';
const sql = neon(url);
const users = await sql`SELECT * FROM users`;
→ Connection 없음. HTTP request 만. Edge / Lambda 친화.
Supabase pooler
Supabase 가 PgBouncer 자체 host.
- session: port 5432
- transaction: port 6543
→ App = transaction pool 사용 (default).
Pool stats (모니터링)
setInterval(() => {
log.info('pool', {
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
});
}, 30_000);
→ waiting > 0 자주 = pool 부족 / leak.
PgBouncer 확인
-- PgBouncer admin
\c pgbouncer
SHOW pools;
-- cl_active / sv_active / cl_waiting
SHOW stats;
-- requests, queries, etc
SHOW clients;
SHOW servers;
Connection leak
// ❌ Release 안 함
const client = await pool.connect();
const r = await client.query('SELECT ...');
// 누락: client.release()
// ✅ try-finally
const client = await pool.connect();
try {
await client.query('...');
} finally {
client.release();
}
// 또는 pool.query (자동 release)
await pool.query('...');
Application restart
모든 connection re-create.
Pool warm-up:
- pre-create min connections at startup
- 첫 request 가 빠름
const pool = new Pool({
min: 5, // 시작 시 미리 5개
});
Multiple DBs (read / write split)
const writer = new Pool({ connectionString: PRIMARY_URL, max: 20 });
const reader = new Pool({ connectionString: REPLICA_URL, max: 50 });
async function getOrders(userId: string) {
return reader.query('SELECT * FROM orders WHERE user_id = $1', [userId]);
}
async function createOrder(data) {
return writer.query('INSERT INTO orders ...', [...]);
}
Tenant pool (multi-tenant)
// Approach: per-tenant DB
const pools = new Map<string, Pool>();
function getPool(tenantId: string): Pool {
if (!pools.has(tenantId)) {
pools.set(tenantId, new Pool({ ... }));
}
return pools.get(tenantId)!;
}
// Cleanup unused tenants
→ N tenant × pool size = 큰 — 주의.
DB connection 이 가장 비싼 자원
1 connection ≈ 10 MB Postgres process.
1000 connection ≈ 10 GB.
→ Pool size 작게 + multiplex.
Lambda connection issue
Lambda = 매 invocation 새 container 가능.
1000 concurrent Lambda = 1000 connection.
해결:
1. RDS Proxy (AWS)
2. Hyperdrive (CF)
3. Neon HTTP / Serverless
4. Pool 매 container reuse (warm Lambda)
Idle in transaction
-- App 가 BEGIN 후 외부 호출 hang
SELECT pid, state, query, query_start
FROM pg_stat_activity
WHERE state = 'idle in transaction';
-- Auto kill
ALTER SYSTEM SET idle_in_transaction_session_timeout = '60s';
→ 60s 안 release 안 하면 cancel.
Statement timeout
// Connection 별
client.query('SET statement_timeout = 30000'); // 30s
// 또는 connection string
postgres://user:pw@host:5432/db?statement_timeout=30000
→ Hang query 방지.
Retry on connection error
async function queryWithRetry<T>(query: string, params: any[]): Promise<T> {
for (let i = 0; i < 3; i++) {
try {
return await pool.query(query, params);
} catch (e: any) {
if (e.code === 'ECONNRESET' || e.code === 'ETIMEDOUT') {
await sleep(100 * (i + 1));
continue;
}
throw e;
}
}
throw new Error('max retries');
}
Health check
async function dbHealthy(): Promise<boolean> {
try {
await Promise.race([
pool.query('SELECT 1'),
new Promise((_, reject) => setTimeout(() => reject(), 5000)),
]);
return true;
} catch {
return false;
}
}
PgBouncer alternative
- pgcat (Rust, modern)
- pgpool-II (older, complex)
- Supavisor (Supabase)
- Odyssey (Yandex)
→ PgBouncer 가 가장 인기.
Production setup (typical)
App (10 instance) → PgBouncer (3 instance) → Postgres (primary + replicas)
App pool: 5-10 / instance
PgBouncer: max_client_conn = 1000, pool_size = 25
Postgres: max_connections = 100
→ 1000 client → 25 DB connection (40x multiplex).
Architecture
[App] [App] [App]
↓ ↓ ↓
[PgBouncer]
↓
[Postgres primary]
↕
[Postgres replica]
Cloud manage
RDS Proxy: AWS, supports MySQL / Postgres
Aurora Serverless v2: auto-scale
Neon / Supabase: built-in pool
Cloud SQL: external pool 직접
🤔 의사결정 기준
| 환경 | 추천 |
|---|---|
| 일반 server | App pool |
| 큰 traffic / 많은 instance | PgBouncer |
| Lambda | RDS Proxy / Hyperdrive |
| Cloudflare Workers | Hyperdrive / Neon HTTP |
| Edge | Neon HTTP / Turso |
| Production | App + PgBouncer |
❌ 안티패턴
- App instance × pool > Postgres max: connection 폭발.
- session pool mode + multi-tenant: 격리 약함.
- Transaction pool + session feature 사용: 깨짐.
- Pool 안 release: leak.
- Long-running transaction: pool 다 잡음.
- Idle timeout 길음 NAT 보다: zombie.
- 모니터링 없음: 점진 다운.
🤖 LLM 활용 힌트
- App pool (작게) + PgBouncer (multiplex).
- Lambda = HTTP driver / proxy.
- Transaction mode = default.
- Pool stats 항상 monitor.