Files
2nd/10_Wiki/Topics/Coding/Backend_Connection_Handling.md
T
2026-05-09 21:08:02 +09:00

340 lines
7.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend-connection-handling
title: Connection Handling — Pool / Reuse / Timeout
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [backend, connection, pool, vibe-coding]
tech_stack: { language: "TS", applicable_to: ["Backend"] }
applied_in: []
aliases: [HTTP keep-alive, agent, pgbouncer, connection pool, idle timeout, EADDRINUSE]
---
# 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)
```ts
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+)
```ts
// 자동 keep-alive (default).
// 그러나 default agent — 작은 limit.
// 큰 throughput = undici Agent.
```
### Postgres pool (pg)
```ts
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
```ts
// 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)
```ini
# 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 처리
```ts
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
```ts
// 모든 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)
```
```ts
// 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
```ts
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
```ts
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)
```ts
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
```ts
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
```ts
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)
```ts
// 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 재사용
```ts
// 빠른 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';
```
```ts
// 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.
## 🔗 관련 문서
- [[DB_Connection_Pool]]
- [[Backend_Service_Discovery]]
- [[DB_Lock_Analysis]]