/인프라/PostgreSQL 'too many clients already' 5분 진단부터 PgBouncer 해결까지
인프라PostgreSQLPgBouncer

PostgreSQL 'too many clients already' 5분 진단부터 PgBouncer 해결까지

PostgreSQL 'too many clients already' 에러를 pg_stat_activity 진단 SQL과 pg_terminate_backend로 5분 안에 응급 복구하는 법, max_connections를 무작정 늘리면 안 되는 이유와 PgBouncer 커넥션 풀링 설정까지 단계별로 정리했습니다.

PostgreSQL 'too many clients already' 5분 진단부터 PgBouncer 해결까지

PostgreSQL 'too many clients already' 5분 진단부터 PgBouncer 해결까지

지금 앱 로그에 이 문구가 찍혀서 들어오셨을 겁니다.

CODE
FATAL: sorry, too many clients already

번역하면 "연결 슬롯이 다 찼다"는 뜻입니다. PostgreSQL은 max_connections로 동시 연결 수에 하드 리밋을 둡니다. 그 한도를 넘어선 새 연결 요청은 인증 단계 이전에 즉시 거부됩니다. 즉 DB가 죽은 게 아니라, 자리가 없어서 문을 닫은 상태입니다.

당황하지 마시고 순서대로 갑니다. 응급실 매뉴얼처럼 진행하세요.

  1. 현황 확인 — 누가 연결을 잡아먹는지 본다 (2분)
  2. 임시 복구 — 위험한 세션을 끊어 서비스를 살린다 (1분)
  3. 근본 해결 — PgBouncer로 재발을 막는다 (이후 차분히)

1. 5분 진단: pg_stat_activity로 범인 색출

먼저 콘솔에 붙어야 합니다. 일반 연결이 안 되면 3번의 superuser 예약 연결을 먼저 보세요. 붙었다면 아래 3종 SQL을 순서대로 던집니다.

① 전체 연결 수 vs 상한 비교

SQL
SELECT count(*) FROM pg_stat_activity;
SHOW max_connections;

count가 max_connections에 거의 붙어 있으면 확정입니다.

② 상태별 집계 — 어디서 새는가

SQL
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count(*) DESC;
  • active: 실제 쿼리 실행 중 (정상 부하일 수도)
  • idle: 연결만 잡고 노는 중 (풀이 과하게 큰 신호)
  • idle in transaction: 가장 위험. 트랜잭션을 열어둔 채 커밋/롤백 안 함 → 락과 슬롯을 동시에 점유

③ 오래된 idle in transaction 범인 정렬

SQL
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분 넘은 것만)

SQL
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 하나만 끊을 때:

SQL
SELECT pg_terminate_backend(12345);

콘솔조차 못 붙을 때 — 예약 연결 확보 PostgreSQL은 superuser_reserved_connections(기본 3개) 슬롯을 슈퍼유저용으로 비워둡니다. 일반 앱 계정이 다 막혀도 postgres 같은 슈퍼유저로는 들어갈 여지가 있습니다. 평소에 이 값을 5 정도로 잡아두면 장애 시 손발이 묶이지 않습니다.

실무 팁: 저는 장애 때 무작정 다 끊지 않습니다. 먼저 ③ 쿼리로 application_name을 보고 어느 서비스가 범인인지 1초 만에 특정합니다. 배치 서버 하나가 트랜잭션을 열고 죽은 케이스가 압도적으로 많았습니다. 끊은 뒤 그 서버부터 재배포하면 같은 일이 안 반복됩니다.


3. 근본 해결: max_connections를 올리면 안 되는 이유

여기서 90%가 함정에 빠집니다. "그럼 max_connections를 500으로 올리면 되잖아?" — 안 됩니다.

PostgreSQL은 연결 하나당 별도 백엔드 프로세스를 띄웁니다. 실질 메모리는 대략 이렇게 계산됩니다.

CODE
peak memory ≈ max_connections × work_mem × (정렬·해시 노드 수)
            + 백엔드당 고정 오버헤드(수~십수 MB)

work_mem이 쿼리 하나의 정렬/해시 연산마다 곱해진다는 게 핵심입니다. 복잡한 쿼리는 work_mem을 수 배까지 씁니다. work_mem 16MB에 max_connections 500이면 최악의 경우 메모리가 폭주해 OOM Killer가 DB를 죽입니다. 게다가 백엔드가 200~300개를 넘으면 컨텍스트 스위칭 비용으로 처리량이 오히려 떨어집니다.

참고로 변경은 재시작이 필요합니다.

SQL
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:

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 해시):

CODE
"app_user" "SCRAM-SHA-256$4096:..."

앱의 DSN은 포트만 5432 → 6432로 바꾸면 끝입니다.

CODE
postgresql://app_user:****@db-host:6432/mydb

⚠️ transaction 모드 주의: 연결이 트랜잭션마다 다른 클라이언트에게 재배정되므로, 세션에 묶이는 것들이 깨집니다. 서버사이드 prepared statement, SET 세션 변수, advisory lock, LISTEN/NOTIFY가 대표적입니다. 드라이버에서 prepared statement 캐시를 끄거나(JDBC prepareThreshold=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종

SQL
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;     -- 범인

응급 명령

SQL
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();

풀 사이즈 산정 공식 (출발점)

CODE
default_pool_size ≈ (CPU 코어 수 × 2) + 디스크 스핀들 수

대부분의 OLTP 워크로드는 DB당 20~50개면 충분합니다. 의외로 작습니다.

재발 방지 모니터링

  • pg_stat_activity count를 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처럼 연결이 폭증하는 환경에선 풀러가 사실상 필수입니다.

✦ ✦ ✦
편집 검토 · Editorial Review

이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.

작성 · Content Reviewer·검토 · 사람 편집자·발행 · 2026년 6월 12일

댓글

불러오는 중...