pgBouncer
PostgreSQL Connection Pooler
PostgreSQL을 위한 경량 커넥션 풀러입니다. 애플리케이션의 수천 개 연결을 수십 개의 실제 DB 연결로 다중화하여 PostgreSQL 서버의 메모리 사용량과 연결 오버헤드를 크게 줄입니다.
PostgreSQL Connection Pooler
PostgreSQL을 위한 경량 커넥션 풀러입니다. 애플리케이션의 수천 개 연결을 수십 개의 실제 DB 연결로 다중화하여 PostgreSQL 서버의 메모리 사용량과 연결 오버헤드를 크게 줄입니다.
pgBouncer는 PostgreSQL 앞단에서 동작하는 경량 커넥션 풀러입니다. PostgreSQL은 프로세스 기반 아키텍처로 연결당 하나의 프로세스가 생성되며, 연결당 약 5-10MB의 메모리를 소비합니다. pgBouncer는 이 문제를 해결합니다.
풀링 모드: (1) Session 모드 - 클라이언트 세션 동안 연결 유지, 가장 호환성 높음, (2) Transaction 모드 - 트랜잭션 단위로 연결 공유, 가장 효율적이며 권장, (3) Statement 모드 - 문장 단위 공유, 다중 문장 트랜잭션 불가로 제한적 사용.
동작 원리: 클라이언트는 pgBouncer(기본 6432 포트)에 연결하고, pgBouncer가 내부적으로 PostgreSQL 연결 풀을 관리합니다. Transaction 모드에서 트랜잭션이 끝나면 연결이 풀로 반환되어 다른 클라이언트가 재사용합니다.
성능 효과: 10,000개의 동시 클라이언트 연결을 50개의 실제 DB 연결로 처리 가능합니다. 서버리스 환경(AWS Lambda, Vercel)에서 연결이 자주 생성/해제될 때 특히 효과적입니다. 연결 생성 오버헤드(TCP 핸드셰이크, 인증)를 제거합니다.
Supabase, Neon, Railway 등 PostgreSQL 기반 클라우드 서비스들이 pgBouncer를 기본 제공하며, 서버리스 환경에서 필수적인 구성 요소로 자리잡았습니다.
# pgBouncer를 통한 PostgreSQL 연결 - Python
import psycopg2
from psycopg2 import pool
from contextlib import contextmanager
class PgBouncerConnection:
"""
pgBouncer를 통한 PostgreSQL 연결 관리
Transaction 모드 사용 시 주의사항 반영
"""
def __init__(self, pgbouncer_host: str, port: int = 6432):
# pgBouncer 포트로 연결 (기본 6432)
self.connection_string = f"""
host={pgbouncer_host}
port={port}
dbname=mydb
user=app_user
password=secret
"""
# 애플리케이션 레벨 풀 (pgBouncer가 있어도 권장)
self.pool = psycopg2.pool.ThreadedConnectionPool(
minconn=5,
maxconn=20,
dsn=self.connection_string
)
@contextmanager
def get_connection(self):
"""연결 획득 및 반환"""
conn = self.pool.getconn()
try:
yield conn
conn.commit()
except Exception as e:
conn.rollback()
raise e
finally:
self.pool.putconn(conn)
def execute_query(self, query: str, params: tuple = None):
"""단순 쿼리 실행"""
with self.get_connection() as conn:
with conn.cursor() as cur:
cur.execute(query, params)
if cur.description:
return cur.fetchall()
return None
# Transaction 모드에서의 주의사항
class TransactionModeWarnings:
"""
pgBouncer Transaction 모드 제한사항
- PREPARE/DEALLOCATE: 트랜잭션 간 유지 안됨
- SET 명령: 세션 변수 유지 안됨
- LISTEN/NOTIFY: 작동 안함
- Cursors: WITH HOLD 커서 사용 불가
"""
def __init__(self, conn_string: str):
self.conn_string = conn_string
def wrong_approach(self):
"""❌ 잘못된 사용 - SET이 다음 쿼리에서 유지 안됨"""
conn = psycopg2.connect(self.conn_string)
# 트랜잭션 1
with conn.cursor() as cur:
cur.execute("SET search_path TO myschema")
conn.commit() # 여기서 연결이 풀로 반환됨
# 트랜잭션 2 - 다른 연결을 받을 수 있음!
with conn.cursor() as cur:
# search_path가 초기화되어 있을 수 있음
cur.execute("SELECT * FROM mytable") # 오류 가능
def correct_approach(self):
"""✅ 올바른 사용 - 같은 트랜잭션 내에서 처리"""
conn = psycopg2.connect(self.conn_string)
with conn.cursor() as cur:
# 같은 트랜잭션 내에서 SET과 쿼리 실행
cur.execute("SET search_path TO myschema")
cur.execute("SELECT * FROM mytable")
results = cur.fetchall()
conn.commit() # 모든 작업 후 커밋
return results
def use_query_hints(self):
"""✅ 쿼리 레벨에서 스키마 명시"""
conn = psycopg2.connect(self.conn_string)
with conn.cursor() as cur:
# SET 대신 스키마를 쿼리에 직접 명시
cur.execute("SELECT * FROM myschema.mytable")
return cur.fetchall()
# pgBouncer 관리 명령어 실행
class PgBouncerAdmin:
"""pgBouncer 관리 인터페이스"""
def __init__(self, admin_conn_string: str):
# pgbouncer 데이터베이스로 연결
self.conn_string = admin_conn_string
def show_pools(self):
"""풀 상태 확인"""
conn = psycopg2.connect(self.conn_string)
conn.autocommit = True # 관리 명령은 autocommit 필요
with conn.cursor() as cur:
cur.execute("SHOW POOLS")
columns = [desc[0] for desc in cur.description]
rows = cur.fetchall()
for row in rows:
pool_info = dict(zip(columns, row))
print(f"DB: {pool_info['database']}")
print(f" - Active: {pool_info['cl_active']}")
print(f" - Waiting: {pool_info['cl_waiting']}")
print(f" - Server connections: {pool_info['sv_active']}")
def show_stats(self):
"""통계 확인"""
conn = psycopg2.connect(self.conn_string)
conn.autocommit = True
with conn.cursor() as cur:
cur.execute("SHOW STATS")
return cur.fetchall()
def reload_config(self):
"""설정 리로드"""
conn = psycopg2.connect(self.conn_string)
conn.autocommit = True
with conn.cursor() as cur:
cur.execute("RELOAD")
print("pgBouncer config reloaded")
# 사용 예시
if __name__ == "__main__":
db = PgBouncerConnection("pgbouncer.example.com")
# 일반 쿼리
users = db.execute_query(
"SELECT * FROM users WHERE created_at > %s",
("2024-01-01",)
)
print(users)
// pgBouncer를 통한 PostgreSQL 연결 - Node.js
const { Pool } = require('pg');
class PgBouncerClient {
constructor(config) {
// pgBouncer 포트(6432)로 연결
this.pool = new Pool({
host: config.host,
port: config.port || 6432, // pgBouncer 기본 포트
database: config.database,
user: config.user,
password: config.password,
// pgBouncer 사용 시 권장 설정
max: 20, // 클라이언트 풀 크기
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
// Transaction 모드 호환성
statement_timeout: 30000,
});
}
/**
* 단순 쿼리 실행
*/
async query(text, params) {
const client = await this.pool.connect();
try {
const result = await client.query(text, params);
return result.rows;
} finally {
client.release();
}
}
/**
* 트랜잭션 실행 - Transaction 모드 호환
*/
async transaction(callback) {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
// 트랜잭션 내에서 모든 작업 수행
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
}
// Transaction 모드 주의사항 예제
class TransactionModeExamples {
constructor(pool) {
this.pool = pool;
}
/**
* ❌ 잘못된 사용: SET 명령이 다음 쿼리에서 유지 안됨
*/
async wrongUsage() {
// 쿼리 1: SET 실행
await this.pool.query("SET timezone TO 'Asia/Seoul'");
// 쿼리 2: 다른 연결로 갈 수 있음!
const result = await this.pool.query("SELECT NOW()");
// timezone 설정이 적용 안 될 수 있음
return result.rows;
}
/**
* ✅ 올바른 사용: 같은 트랜잭션 내에서 처리
*/
async correctUsage() {
const client = await this.pool.connect();
try {
await client.query('BEGIN');
// 같은 연결에서 SET과 쿼리 실행
await client.query("SET timezone TO 'Asia/Seoul'");
const result = await client.query("SELECT NOW()");
await client.query('COMMIT');
return result.rows;
} finally {
client.release();
}
}
/**
* ✅ 쿼리 레벨 설정 사용
*/
async queryLevelTimezone() {
// SET 대신 AT TIME ZONE 사용
const result = await this.pool.query(`
SELECT NOW() AT TIME ZONE 'Asia/Seoul' as local_time
`);
return result.rows;
}
/**
* ❌ PREPARE는 Transaction 모드에서 작동 안함
*/
async preparedStatement() {
// pgBouncer Transaction 모드에서는
// prepared statement가 트랜잭션 간 유지 안됨
// 대신 일반 파라미터 쿼리 사용
const result = await this.pool.query(
'SELECT * FROM users WHERE id = $1',
[123]
);
return result.rows;
}
}
// Prisma에서 pgBouncer 사용
// prisma/schema.prisma
/*
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
// pgBouncer Transaction 모드 사용 시 필수
directUrl = env("DIRECT_URL") // 마이그레이션용 직접 연결
}
// .env
// DATABASE_URL="postgresql://user:pass@pgbouncer:6432/mydb?pgbouncer=true"
// DIRECT_URL="postgresql://user:pass@postgres:5432/mydb"
*/
// 사용 예시
async function main() {
const db = new PgBouncerClient({
host: 'pgbouncer.example.com',
port: 6432,
database: 'mydb',
user: 'app_user',
password: 'secret'
});
// 트랜잭션 예제
const result = await db.transaction(async (client) => {
await client.query(
'UPDATE accounts SET balance = balance - $1 WHERE id = $2',
[100, 'ACC001']
);
await client.query(
'UPDATE accounts SET balance = balance + $1 WHERE id = $2',
[100, 'ACC002']
);
return { success: true };
});
console.log(result);
}
main().catch(console.error);
# ============================================
# pgbouncer.ini - 기본 설정
# ============================================
[databases]
# 데이터베이스 별칭 = 실제 연결 정보
mydb = host=postgres.internal port=5432 dbname=production
# 여러 DB 설정 가능
analytics = host=analytics-db.internal port=5432 dbname=analytics
# 와일드카드로 모든 DB 허용
# * = host=postgres.internal port=5432
[pgbouncer]
# 리스닝 설정
listen_addr = 0.0.0.0
listen_port = 6432
# 인증 설정
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
# auth_query = SELECT username, password FROM pgbouncer.users WHERE username=$1
# ============================================
# 풀링 모드 (가장 중요한 설정)
# ============================================
# session: 세션 동안 연결 유지 (가장 호환성 높음)
# transaction: 트랜잭션 단위 공유 (권장, 가장 효율적)
# statement: 문장 단위 공유 (제한적)
pool_mode = transaction
# ============================================
# 연결 풀 설정
# ============================================
# 데이터베이스당 기본 풀 크기
default_pool_size = 25
# 전체 최대 클라이언트 연결
max_client_conn = 10000
# 최소 풀 크기 (사전 연결)
min_pool_size = 5
# 예약 풀 크기 (긴급용)
reserve_pool_size = 5
reserve_pool_timeout = 3
# 사용자당 최대 연결
max_user_connections = 100
# 데이터베이스당 최대 연결
max_db_connections = 50
# ============================================
# 타임아웃 설정
# ============================================
# 서버 연결 유휴 타임아웃
server_idle_timeout = 60
# 클라이언트 유휴 타임아웃 (0 = 무제한)
client_idle_timeout = 0
# 클라이언트 로그인 타임아웃
client_login_timeout = 60
# 쿼리 타임아웃
query_timeout = 120
# 서버 연결 수명
server_lifetime = 3600
# ============================================
# 로깅 설정
# ============================================
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1
stats_period = 60
# 로그 파일
logfile = /var/log/pgbouncer/pgbouncer.log
pidfile = /var/run/pgbouncer/pgbouncer.pid
# ============================================
# 관리자 설정
# ============================================
admin_users = pgbouncer_admin
stats_users = pgbouncer_stats
# ============================================
# userlist.txt 예시
# ============================================
# 형식: "username" "password" 또는 "username" "md5hash"
# "app_user" "plain_password"
# "app_user" "md5d8578edf8458ce06fbc5bb76a58c5ca4"
# SCRAM-SHA-256 (PostgreSQL 10+)
# "app_user" "SCRAM-SHA-256$iterations:salt$StoredKey:ServerKey"
# ============================================
# Docker Compose 예시
# ============================================
# docker-compose.yml
version: '3.8'
services:
pgbouncer:
image: edoburu/pgbouncer:1.21.0
ports:
- "6432:6432"
environment:
- DATABASE_URL=postgres://user:pass@postgres:5432/mydb
- POOL_MODE=transaction
- DEFAULT_POOL_SIZE=25
- MAX_CLIENT_CONN=1000
volumes:
- ./pgbouncer.ini:/etc/pgbouncer/pgbouncer.ini
- ./userlist.txt:/etc/pgbouncer/userlist.txt
postgres:
image: postgres:16
environment:
- POSTGRES_PASSWORD=secret
volumes:
- pgdata:/var/lib/postgresql/data
# ============================================
# 관리 명령어
# ============================================
# pgbouncer DB로 접속
# psql -h localhost -p 6432 -U pgbouncer_admin pgbouncer
# SHOW POOLS; -- 풀 상태 확인
# SHOW CLIENTS; -- 클라이언트 연결 확인
# SHOW SERVERS; -- 서버 연결 확인
# SHOW STATS; -- 통계 확인
# SHOW CONFIG; -- 설정 확인
# RELOAD; -- 설정 리로드
# PAUSE mydb; -- 특정 DB 일시정지
# RESUME mydb; -- 재개
"max_connections 에러가 반복되는데 무작정 늘리면 안 됩니다. PostgreSQL은 연결당 메모리를 많이 써요. pgBouncer를 앞에 두면 클라이언트 연결 1만 개를 DB 연결 50개로 처리할 수 있어요. Transaction 모드로 설정하면 트랜잭션이 끝날 때마다 연결을 반환해서 효율이 극대화됩니다."
"Lambda에서 직접 PostgreSQL 연결하면 함수 인스턴스마다 연결이 생겨서 금방 max_connections에 도달해요. pgBouncer나 RDS Proxy를 써야 합니다. Supabase 같은 서비스는 pgBouncer를 기본 제공하니까 pooler URL로 연결하세요."
"여기서 SET search_path 후에 커밋하고 다음 쿼리를 실행하는데, pgBouncer Transaction 모드에서는 커밋 후 다른 연결을 받을 수 있어서 SET 효과가 사라집니다. 같은 트랜잭션 안에서 처리하거나, 쿼리에 스키마를 명시해주세요."
PREPARE, LISTEN/NOTIFY, 세션 변수(SET), WITH HOLD 커서가 트랜잭션 간 유지되지 않습니다. 이런 기능이 필요하면 Session 모드를 사용하세요.
userlist.txt를 별도 관리해야 합니다. SCRAM-SHA-256 사용 시 auth_query로 PostgreSQL에서 직접 인증하는 방식도 고려하세요.
pgBouncer 다운 시 모든 연결이 끊깁니다. 프로덕션에서는 HAProxy나 Keepalived를 사용해 HA 구성을 권장합니다.
SHOW POOLS로 cl_waiting(대기 중인 클라이언트)을 모니터링하세요. 0이 아니면 풀 크기를 늘리거나 쿼리 최적화가 필요합니다.