PostgreSQL 'too many clients already' 5분 진단부터 PgBouncer 해결까지
지금 앱 로그에 이 문구가 찍혀서 들어오셨을 겁니다.
FATAL: sorry, too many clients already번역하면 "연결 슬롯이 다 찼다"는 뜻입니다. PostgreSQL은 max_connections로 동시 연결 수에 하드 리밋을 둡니다. 그 한도를 넘어선 새 연결 요청은 인증 단계 이전에 즉시 거부됩니다. 즉 DB가 죽은 게 아니라, 자리가 없어서 문을 닫은 상태입니다.
당황하지 마시고 순서대로 갑니다. 응급실 매뉴얼처럼 진행하세요.
- 현황 확인 — 누가 연결을 잡아먹는지 본다 (2분)
- 임시 복구 — 위험한 세션을 끊어 서비스를 살린다 (1분)
- 근본 해결 — PgBouncer로 재발을 막는다 (이후 차분히)
1. 5분 진단: pg_stat_activity로 범인 색출
먼저 콘솔에 붙어야 합니다. 일반 연결이 안 되면 3번의 superuser 예약 연결을 먼저 보세요. 붙었다면 아래 3종 SQL을 순서대로 던집니다.
① 전체 연결 수 vs 상한 비교
SELECT count(*) FROM pg_stat_activity;
SHOW max_connections;count가 max_connections에 거의 붙어 있으면 확정입니다.
② 상태별 집계 — 어디서 새는가
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;active: 실제 쿼리 실행 중 (정상 부하일 수도)idle: 연결만 잡고 노는 중 (풀이 과하게 큰 신호)idle in transaction: 가장 위험. 트랜잭션을 열어둔 채 커밋/롤백 안 함 → 락과 슬롯을 동시에 점유
③ 오래된 idle in transaction 범인 정렬
SELECT pid,
now() - state_change AS duration,
usename, application_name,
left(query, 60) AS last_query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY state_change ASC; -- 가장 오래된 순duration이 수 분~수십 분짜리가 줄줄이 보인다면, 애플리케이션이 트랜잭션을 안 닫고 있는 겁니다. 이게 대부분의 진범입니다.
2. 즉시 복구: 서비스부터 살린다
원인 분석은 나중에. 지금은 슬롯을 비워야 합니다.
오래된 idle in transaction 일괄 종료 (5분 넘은 것만)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - state_change > interval '5 minutes'
AND pid <> pg_backend_pid(); -- ★ 자기 세션 제외 필수pid <> pg_backend_pid()를 빼먹으면 명령을 내리는 자기 자신까지 끊을 수 있으니 꼭 넣으세요.
특정 PID 하나만 끊을 때:
SELECT pg_terminate_backend(12345);콘솔조차 못 붙을 때 — 예약 연결 확보
PostgreSQL은 superuser_reserved_connections(기본 3개) 슬롯을 슈퍼유저용으로 비워둡니다. 일반 앱 계정이 다 막혀도 postgres 같은 슈퍼유저로는 들어갈 여지가 있습니다. 평소에 이 값을 5 정도로 잡아두면 장애 시 손발이 묶이지 않습니다.
실무 팁: 저는 장애 때 무작정 다 끊지 않습니다. 먼저 ③ 쿼리로
application_name을 보고 어느 서비스가 범인인지 1초 만에 특정합니다. 배치 서버 하나가 트랜잭션을 열고 죽은 케이스가 압도적으로 많았습니다. 끊은 뒤 그 서버부터 재배포하면 같은 일이 안 반복됩니다.
3. 근본 해결: max_connections를 올리면 안 되는 이유
여기서 90%가 함정에 빠집니다. "그럼 max_connections를 500으로 올리면 되잖아?" — 안 됩니다.
PostgreSQL은 연결 하나당 별도 백엔드 프로세스를 띄웁니다. 실질 메모리는 대략 이렇게 계산됩니다.
peak memory ≈ max_connections × work_mem × (정렬·해시 노드 수)
+ 백엔드당 고정 오버헤드(수~십수 MB)work_mem이 쿼리 하나의 정렬/해시 연산마다 곱해진다는 게 핵심입니다. 복잡한 쿼리는 work_mem을 수 배까지 씁니다. work_mem 16MB에 max_connections 500이면 최악의 경우 메모리가 폭주해 OOM Killer가 DB를 죽입니다. 게다가 백엔드가 200~300개를 넘으면 컨텍스트 스위칭 비용으로 처리량이 오히려 떨어집니다.
참고로 변경은 재시작이 필요합니다.
ALTER SYSTEM SET max_connections = 200;
-- 이후 PostgreSQL 재시작 필수 (reload로는 적용 안 됨)RDS / Aurora / Cloud SQL 같은 매니지드 환경은 인스턴스 등급별로 상한이 고정돼 있어 무한정 못 올립니다. Lambda나 K8s 파드가 스케일아웃되면 연결이 폭증하는데, 이걸 DB 상한으로 막는 건 불가능에 가깝습니다. 그래서 커넥션 풀러(PgBouncer / RDS Proxy)가 사실상 필수가 됐습니다.
PgBouncer transaction 모드 설정
핵심 아이디어: 앱은 수백 개 연결을 PgBouncer에 던지고, PgBouncer는 DB에 소수의 연결만 유지하며 재사용합니다.
pgbouncer.ini:
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction ; 트랜잭션 단위 재사용 (가장 효율적)
max_client_conn = 1000 ; 앱이 붙을 수 있는 최대 클라이언트 수
default_pool_size = 20 ; DB로 나가는 실제 연결 수userlist.txt (SCRAM/md5 해시):
"app_user" "SCRAM-SHA-256$4096:..."앱의 DSN은 포트만 5432 → 6432로 바꾸면 끝입니다.
postgresql://app_user:****@db-host:6432/mydb⚠️ transaction 모드 주의: 연결이 트랜잭션마다 다른 클라이언트에게 재배정되므로, 세션에 묶이는 것들이 깨집니다. 서버사이드 prepared statement,
SET세션 변수, advisory lock,LISTEN/NOTIFY가 대표적입니다. 드라이버에서 prepared statement 캐시를 끄거나(JDBCprepareThreshold=0) 세션 의존 기능은 별도 풀(session 모드)을 쓰세요.
애플리케이션 커넥션 누수 점검 체크리스트
풀러를 깔아도 앱이 연결을 안 돌려주면 똑같이 터집니다.
- HikariCP:
maximumPoolSize가 DB 등급에 맞나?leakDetectionThreshold=5000으로 누수 추적 켜기 - SQLAlchemy:
pool_size+max_overflow합이 상한을 넘지 않나? 세션close()누락 확인 - 트랜잭션을 열고 커밋/롤백 없이 끝내는 경로 (← idle in transaction 주범)
- ORM lazy loading이 요청 종료 후에도 연결을 붙잡는지
- 예외 경로에서
connection.close()누락 —try/finally또는with컨텍스트 강제
4. 결론: 한 장 치트시트
진단 SQL 3종
SELECT count(*) FROM pg_stat_activity; -- 현재 연결
SELECT state, count(*) FROM pg_stat_activity GROUP BY state; -- 상태별
SELECT pid, now()-state_change, query FROM pg_stat_activity
WHERE state='idle in transaction' ORDER BY state_change; -- 범인응급 명령
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE state='idle in transaction'
AND now()-state_change > interval '5 minutes'
AND pid <> pg_backend_pid();풀 사이즈 산정 공식 (출발점)
default_pool_size ≈ (CPU 코어 수 × 2) + 디스크 스핀들 수대부분의 OLTP 워크로드는 DB당 20~50개면 충분합니다. 의외로 작습니다.
재발 방지 모니터링
pg_stat_activitycount를 1분 주기 수집- 알람 임계치:
사용 연결 / max_connections > 80%면 경고 idle in transaction개수에 별도 알람 (락 폭주 조기 감지)
자주 묻는 질문 (FAQ)
Q. max_connections를 그냥 1000으로 올리면 안 되나요? A. 권장하지 않습니다. 연결마다 백엔드 프로세스와 work_mem이 곱셈으로 쌓여 OOM과 컨텍스트 스위칭 오버헤드를 유발합니다. 보통 200~300 이하로 두고 PgBouncer로 다수 클라이언트를 흡수하는 게 정석입니다.
Q. pg_terminate_backend로 끊으면 데이터가 깨지나요? A. 진행 중 트랜잭션은 롤백되므로 커밋되지 않은 변경만 사라집니다. idle in transaction은 어차피 작업이 멈춰 있어 안전한 편이지만, active 세션을 끊을 땐 어떤 쿼리인지 확인 후 끊으세요.
Q. RDS/Aurora에서도 PgBouncer를 써야 하나요? A. PgBouncer를 별도 인스턴스로 띄우거나, AWS라면 관리형 RDS Proxy를 쓰면 됩니다. 매니지드 DB는 등급별 max_connections 상한이 고정이라, Lambda·K8s처럼 연결이 폭증하는 환경에선 풀러가 사실상 필수입니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...