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

112 lines
3.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: db-connection-pool
title: DB Connection Pool — 사이즈와 누수
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [database, connection-pool, postgres, vibe-coding]
tech_stack: { language: "Postgres / pgbouncer / Prisma", applicable_to: ["Backend"] }
applied_in: []
aliases: [pool size, max_connections, pgbouncer, transaction mode]
---
# DB Connection Pool
> "pool size = CPU 코어수 × 2" 가 좋은 출발점. 수백으로 키우면 DB 가 죽는다. **누수 패턴**(connection 안 반환)이 throughput 폭발 원인의 90%.
## 📖 핵심 개념
- DB 한 connection = 메모리 ~10MB (Postgres) + 한 backend process. 수천이면 OOM.
- App pool 사이즈 vs DB max_connections 균형.
- 분산 환경: pgbouncer / RDS Proxy 로 multiplex.
## 💻 코드 패턴
### node-postgres 기본
```ts
import { Pool } from 'pg';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // pool size
idleTimeoutMillis: 30_000,// idle 30s 후 닫음
connectionTimeoutMillis: 5_000, // pool 가득이면 5s 대기 후 throw
statement_timeout: 30_000, // 쿼리 자체 30s
});
export async function withTx<T>(op: (c: Client) => Promise<T>): Promise<T> {
const client = await pool.connect();
try {
await client.query('BEGIN');
const r = await op(client);
await client.query('COMMIT');
return r;
} catch (e) {
await client.query('ROLLBACK');
throw e;
} finally {
client.release(); // 누수 방지
}
}
```
### Pool size 계산
```
pool_size = ((core_count * 2) + effective_spindle_count)
// 8코어 SSD: 16~20
```
HikariCP / pg / mysql2 모두 비슷한 휴리스틱.
### pgbouncer transaction mode
```ini
[databases]
mydb = host=postgres dbname=mydb pool_size=100
[pgbouncer]
pool_mode = transaction # 트랜잭션 끝나면 즉시 반환
default_pool_size = 25 # backend 1개당 25 connection
max_client_conn = 1000 # 클라이언트 측 1000 가능
```
client 1000개 → backend 25개. 단 `LISTEN/NOTIFY`, prepared statement 일부 호환 X.
### 누수 감지
```ts
pool.on('connect', () => log.debug('connect'));
pool.on('remove', () => log.debug('remove'));
setInterval(() => {
log.info('pool stats', { total: pool.totalCount, idle: pool.idleCount, waiting: pool.waitingCount });
}, 10_000);
```
`waitingCount > 0` 이 지속되면 pool 부족 또는 누수.
## 🤔 의사결정 기준
| 환경 | 설정 |
|---|---|
| 단일 인스턴스 + 가벼운 트래픽 | max=20, no pgbouncer |
| 다중 인스턴스 / 서버리스 | pgbouncer transaction mode 또는 RDS Proxy |
| Lambda / Edge | RDS Proxy 또는 Hyperdrive — 매 cold start 새 connection 안 만들기 |
| Long-running job | 별도 pool / 별도 user (격리) |
| Read replica 사용 | 읽기/쓰기 분리 pool |
## ❌ 안티패턴
- **release() 누락**: 매 요청 connection 누수 → 곧 모두 점유 → 새 요청 timeout. try/finally.
- **트랜잭션 안에서 외부 API 호출**: connection 묶임. 외부 latency = pool 점유 시간. API 먼저, 그 후 트랜잭션.
- **pool size 1000**: DB 다운. 코어수 × 2~4 권장.
- **prepared statement 캐시 + pgbouncer transaction mode**: 다른 connection 으로 가서 statement 못 찾음. session mode 또는 disable cache.
- **idleTimeout 너무 김**: 사용 안 하는 connection 점유.
- **statement_timeout 미설정**: 한 슬로우 쿼리가 connection 영구 점유.
- **connection 재시도 무한**: DB 다운 시 폭주.
## 🤖 LLM 활용 힌트
- pool size = 코어수 × 2 출발.
- 트랜잭션은 withTx wrapper 패턴 + finally release.
- pgbouncer 면 prepared statement 정책 확인.
## 🔗 관련 문서
- [[DB_Migration_Safety]]
- [[DB_Transaction_Isolation]]