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

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

This article is not yet available in English. Showing the original Korean version.

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 개수에 별도 알람 (락 폭주 조기 감지)

참고: 공식 문서

이 글에서 다루는 동작·설정·에러의 1차 출처는 다음 공식 문서입니다. 버전별 옵션과 정확한 동작은 여기서 확인하세요.

자주 묻는 질문 (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

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 ·
관련 공식 문서PostgreSQL 공식 문서

Comments

Be the first to comment.