Files
2nd/10_Wiki/Topics/Coding/DB_Connection_Pooling_Patterns.md
T
2026-05-09 22:47:42 +09:00

9.4 KiB
Raw Blame History

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
database
pool
vibe-coding
language applicable_to
TS / Postgres
Backend
PgBouncer
connection pool
pool mode
statement pool
transaction pool
RDS Proxy

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 ...', [...]);
}

DB_Read_Replica_Patterns.

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.

DB_Lock_Analysis.

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.

🔗 관련 문서