7.7 KiB
7.7 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 | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend-connection-handling | Connection Handling — Pool / Reuse / Timeout | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Connection Handling
매 request 새 connection = 느림 + resource. Pool + keep-alive + 적절 timeout. Database / HTTP / Redis / TCP 모두 적용. Lambda / serverless 는 다른 패턴.
📖 핵심 개념
- Pool: 미리 N 개 connection 보관.
- Keep-alive: 같은 conn 다중 request.
- Idle timeout: 안 쓰면 close.
- Acquire timeout: pool exhausted 시 wait.
💻 코드 패턴
HTTP keep-alive (Node fetch)
import { Agent } from 'undici';
const agent = new Agent({
keepAliveTimeout: 60_000,
keepAliveMaxTimeout: 600_000,
connections: 100, // per host
pipelining: 10,
});
import { fetch as undiciFetch } from 'undici';
const r = await undiciFetch('https://api.example.com', { dispatcher: agent });
→ 매 request 가 같은 TCP 재사용.
Native fetch (Node 18+)
// 자동 keep-alive (default).
// 그러나 default agent — 작은 limit.
// 큰 throughput = undici Agent.
Postgres pool (pg)
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DB_URL,
max: 20, // max connections
min: 2, // 항상 ready
idleTimeoutMillis: 30_000, // 30s 후 close
connectionTimeoutMillis: 5_000, // 5s acquire timeout
statement_timeout: 30_000, // 30s query timeout
query_timeout: 30_000,
application_name: 'my-app',
});
// 사용
const client = await pool.connect();
try {
const r = await client.query('SELECT * FROM users');
} finally {
client.release();
}
// 또는 단일 query
const r = await pool.query('SELECT * FROM users');
Postgres pool — Drizzle / Prisma
// Drizzle
import { drizzle } from 'drizzle-orm/node-postgres';
const db = drizzle(pool);
// Prisma
const prisma = new PrismaClient({
datasources: { db: { url: process.env.DB_URL } },
});
// Prisma 자체 pool 관리 — connection_limit URL param
// postgresql://...?connection_limit=20
PgBouncer (외부 connection pool)
# pgbouncer.ini
[databases]
app = host=primary-db port=5432 dbname=app
[pgbouncer]
pool_mode = transaction # transaction / session / statement
max_client_conn = 1000 # client → pgbouncer
default_pool_size = 25 # pgbouncer → DB
reserve_pool_size = 5
server_idle_timeout = 600
→ 1000 client → 25 DB connection. PG 의 connection limit 회피.
Pool 크기 결정
규칙:
max = (CPU cores × 2) + effective_spindles
일반 DB: 10-30 connections per app instance
큰 cluster: pgbouncer 로 multiplexing
너무 큼: DB OOM, lock contention
너무 적음: queue 늘어남, 502
Acquire timeout 처리
try {
const client = await pool.connect();
// ...
} catch (e) {
if (e.message.includes('connection terminated')) {
// DB down — circuit breaker
}
if (e.code === 'ETIMEDOUT') {
// Pool exhausted — overload
return res.status(503).end();
}
throw e;
}
Connection leak detection
// 모든 connection 사용 중 + acquire 무한 wait
pool.on('error', (err, client) => {
log.error('pool error', err);
});
// 주기적 stats
setInterval(() => {
log.info('pool stats', {
total: pool.totalCount,
idle: pool.idleCount,
waiting: pool.waitingCount,
});
}, 30_000);
→ waiting > 0 자주 = pool 부족 / leak.
Lambda / serverless 패턴
Lambda 매 invocation = 새 container 가능.
→ Pool 의 의미 적음.
해결:
1. Connection 가까이 — RDS Proxy / Hyperdrive (CF)
2. HTTP 기반 DB driver (Neon, Supabase)
3. 재사용 (warm container)
// Lambda — global scope (warm reuse)
import { Pool } from 'pg';
let pool: Pool | null = null;
export const handler = async (event) => {
if (!pool) pool = new Pool({ max: 1, ...config });
const r = await pool.query('SELECT * FROM users WHERE id = $1', [event.id]);
return r.rows[0];
};
→ Same container 재사용 시 pool 재사용.
Neon / Supabase HTTP driver
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
const users = await sql`SELECT * FROM users WHERE id = ${id}`;
→ HTTP — connection pool 불필요. Edge / serverless 친화.
Redis pool
import Redis from 'ioredis';
const redis = new Redis({
host: 'redis',
port: 6379,
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: false,
// 자동 connection pool — 한 instance OK
});
// Cluster
const cluster = new Redis.Cluster([{ host: 'r1' }, { host: 'r2' }], {
scaleReads: 'slave',
});
MySQL pool (mysql2)
import mysql from 'mysql2/promise';
const pool = mysql.createPool({
host: 'db',
user: 'app',
database: 'app',
connectionLimit: 20,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 10000,
});
HTTP outbound — global agent
import http from 'node:http';
import https from 'node:https';
// Default agents
http.globalAgent.keepAlive = true;
http.globalAgent.maxSockets = 100;
https.globalAgent.keepAlive = true;
https.globalAgent.maxSockets = 100;
Connection retry / circuit breaker
import { CircuitBreaker } from 'opossum';
const breaker = new CircuitBreaker(async (id: string) => {
return await fetch(`http://upstream/users/${id}`);
}, {
timeout: 5000,
errorThresholdPercentage: 50,
resetTimeout: 30000,
});
const r = await breaker.fire('u1');
Idle timeout 의 함정
NAT / load balancer 가 60s+ idle conn close.
App 의 pool idle 60s+ 면 — stale conn.
해결:
- pool idle < LB idle (e.g. 30s pool, 60s LB).
- 또는 ping every N seconds.
Database connection death (zombie)
// PG 가 connection 살아있다 가정 — 사실 dead
pool.on('connect', (client) => {
client.query('SET application_name = "my-app"');
});
// Health check 시 ping
async function checkPool() {
try {
await pool.query('SELECT 1');
} catch {
// Pool 재시작
}
}
EADDRINUSE / port 재사용
// 빠른 restart 시 port still LISTEN
server.listen(3000, () => { ... });
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
log.error('port 3000 in use');
process.exit(1);
}
});
// SO_REUSEPORT (Linux) — 여러 process 같은 port
// Node cluster 자동.
측정
PostgreSQL stats:
SELECT count(*) FROM pg_stat_activity WHERE state = 'active';
SELECT count(*) FROM pg_stat_activity WHERE state = 'idle in transaction';
// App level
metrics.gauge('pool.total', pool.totalCount);
metrics.gauge('pool.idle', pool.idleCount);
metrics.gauge('pool.waiting', pool.waitingCount);
🤔 의사결정 기준
| 환경 | 추천 |
|---|---|
| 일반 server | Pool (10-30) |
| Serverless | HTTP driver / RDS Proxy |
| Edge | Neon / Hyperdrive |
| 1000+ client | PgBouncer |
| HTTP outbound | undici Agent |
| Redis | ioredis (자체 pool) |
❌ 안티패턴
- 매 request 새 connection: 슬로우.
- Pool 너무 큰 (100+): DB OOM.
- Pool 너무 작은 (3): queue.
- Idle timeout 길음 LB 보다: zombie conn.
- Lambda 새 client 매번: 재사용 X — global scope.
- Connection release 안 함: leak.
- Statement timeout 없음: hang.
🤖 LLM 활용 힌트
- Pool max = (CPU × 2) + 적절.
- Idle timeout < LB idle.
- Lambda = HTTP driver / RDS Proxy.
- Stats 모니터링 — waiting > 0 alarm.