/인프라/PostgreSQL too many clients already 30초 판정 복구 런북
InfrastructurePostgreSQLDB 운영

PostgreSQL too many clients already 30초 판정 복구 런북

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

PostgreSQL 'FATAL: sorry, too many clients already' 에러를 30초 판정표로 원인(스파이크·누수·설정)부터 가르고, 복붙 SQL로 idle in transaction 종료·max_connections 튜닝·pgbouncer 도입까지 즉시 복구합니다.

PostgreSQL too many clients already 30초 판정 복구 런북

지금 이 에러로 검색해서 들어왔죠?

FATAL: sorry, too many clients already — 이 원문을 검색창에 붙여넣고 들어오셨다면, 지금 서비스가 DB에 붙지 못하고 있는 상황일 겁니다. 개념 설명은 뒤로 미루겠습니다. 이 글은 복구 순서입니다. 스크롤을 내리는 순서대로 명령을 치면 장애가 끝납니다.

진행 순서는 이렇습니다.

  1. 30초 판정표로 원인 유형(스파이크 / 누수 / 설정 미스)을 먼저 가릅니다.
  2. 복붙 진단 SQL로 현재 상태를 확인하고, 안전하게 응급 종료합니다.
  3. 원인 유형별 영구 조치로 넘어갑니다.
  4. pgbouncer 도입 판단재발방지 알림으로 마무리합니다.

한 가지만 먼저 짚고 갑니다. MySQL의 ERROR 1040: Too many connections는 대부분 max_connections 단순 상향으로 해결하지만, PostgreSQL은 커넥션 1개당 OS 프로세스 1개를 띄우는 프로세스 모델이라 무작정 상한만 올리면 메모리가 터집니다. 그래서 PostgreSQL은 풀링(pooling)이 핵심입니다. MySQL 사용자라면 별도의 ERROR 1040 런북을 참고하시고, PostgreSQL은 이 글에서 끝냅니다.

적용 범위: PostgreSQL 10 이상, RDS/Aurora PostgreSQL, 자체 호스팅 리눅스 환경 공통. 명령은 psql 접속 후 실행 기준입니다.

먼저 접속부터: 관리 세션 확보

일반 커넥션이 다 차서 psql조차 못 붙을 수 있습니다. PostgreSQL은 superuser_reserved_connections(기본 3개)를 슈퍼유저용으로 예약해 둡니다. 슈퍼유저 계정으로 접속하면 이 예약분을 통해 들어갈 수 있습니다.

Bash
# 슈퍼유저(postgres)로 접속 시도 — 예약 커넥션 사용
psql -U postgres -h <host> -p 5432 -d postgres

접속되면 다음 판정표로 넘어갑니다. 이것마저 안 되면 예약 슬롯까지 소진된 심각한 상황이니, 애플리케이션 인스턴스를 일부 내려 커넥션을 강제로 반납시킨 뒤 재접속하세요.

30초 판정표: 내 상황은 어느 쪽인가

지금 상황의 시간적 패턴을 보면 원인이 갈립니다. 아래 표에서 내 증상을 찾고 해당 섹션으로 점프하세요.

증상 신호판정바로 갈 섹션
배포·배치·트래픽 폭증 직후 몇 분간 터지다 자연 회복① 순간 스파이크응급 복구 후 → max_connections/풀 사이징
시간이 갈수록 활성 커넥션 우상향, idle in transaction이 다수 쌓임② 커넥션 누수idle in transaction 종료 + ORM 트랜잭션 점검
DB 재시작 직후부터 곧바로 상한에 근접③ 설정 미스풀 사이즈 vs max_connections 계산

핵심 구분법: 누수(②)는 시간이 지날수록 나빠지고, 스파이크(①)는 순간에 몰렸다 풀립니다. 설정 미스(③)는 처음부터 잘못돼 있습니다. 어느 쪽인지 애매하면 일단 다음 진단 SQL을 치면 데이터가 답을 줍니다.

즉시 진단 & 응급 복구 SQL

1단계 — state별 커넥션 분포 확인

가장 먼저 이걸 칩니다. 커넥션이 어떤 상태로 몰려 있는지 한눈에 보입니다.

SQL
-- 현재 커넥션을 상태별로 집계 (많은 순)
SELECT state, count(*)
FROM pg_stat_activity
GROUP BY state
ORDER BY count DESC;

결과 해석 분기:

  • active가 대부분 → 실제 쿼리가 몰린 스파이크(①). 느린 쿼리·락 대기를 의심.
  • idle in transaction이 많다 → 누수(②) 확정. 트랜잭션을 열고 커밋/롤백을 안 한 세션들입니다. 아래 2단계로.
  • idle이 많다 → 풀은 잡고 있는데 안 쓰는 상태. 풀 사이즈 과다 설정() 가능성.

2단계 — 오래 방치된 idle in transaction 찾기

idle in transaction은 PostgreSQL 커넥션 고갈의 대표 범인입니다. 트랜잭션을 연 채 애플리케이션이 커밋을 안 하면 그 커넥션은 계속 점유되고, 다른 트랜잭션의 정리(VACUUM)까지 방해합니다.

SQL
-- 오래 방치된 idle in transaction 세션을 오래된 순으로
SELECT pid,
       usename,
       state,
       now() - state_change AS idle_dur,
       query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY idle_dur DESC;

idle_dur이 수 분 이상인 세션들이 보이면, query 컬럼으로 어느 애플리케이션 코드가 트랜잭션을 안 닫았는지 역추적할 단서를 얻습니다.

3단계 — 안전 종료 (반드시 2단계 워크플로우)

⚠️ pg_terminate_backend를 바로 실행하지 마세요. 먼저 SELECT로 종료 대상을 눈으로 확인한 뒤 실행합니다. 시스템 프로세스나 내 세션을 죽이면 안 됩니다.

SQL
-- (1) 종료 대상 먼저 확인: 5분 넘게 방치된 idle in transaction, 내 세션 제외
SELECT pid, usename, now() - state_change AS idle_dur, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
  AND now() - state_change > interval '5 minutes'
  AND pid <> pg_backend_pid();

목록이 예상과 맞는지 확인했다면, 같은 조건으로 종료합니다.

SQL
-- (2) 확인된 대상만 종료
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();

pg_backend_pid() 조건이 내 psql 세션을 보호하는 장치입니다. 종료 후 다시 1단계 집계 SQL을 쳐서 커넥션이 풀렸는지 확인하세요. 커넥션이 반납되면 애플리케이션이 다시 붙기 시작합니다. 여기까지가 응급 복구입니다. 서비스가 돌아왔다면 한숨 돌리고 아래 근본 조치로 넘어가세요.

근본 원인별 영구 해결

(a) max_connections 상향 — 메모리 트레이드오프부터 계산

먼저 현재 값을 확인합니다.

SQL
SHOW max_connections;
SHOW superuser_reserved_connections;
SHOW work_mem;

max_connections를 올리기 전에 반드시 짚어야 할 함정이 있습니다. PostgreSQL은 정렬·해시 연산마다 세션당 work_mem을 할당하며, 복잡한 쿼리는 한 세션이 work_mem을 여러 번 쓸 수 있습니다. 워스트케이스 추정은 대략 이렇습니다.

CODE
워스트케이스 추정 메모리 ≈ work_mem × max_connections × (쿼리당 정렬/해시 노드 수)

예) work_mem 16MB × max_connections 200 = 약 3.2GB
    (쿼리 하나가 정렬을 여러 번 하면 이 값의 몇 배까지)

max_connections를 무작정 200 → 500으로 올리면 트래픽 몰릴 때 OOM으로 DB 프로세스가 죽습니다. RDS/Aurora는 max_connections가 인스턴스 클래스(메모리)에 종속돼 파라미터 그룹으로 관리되므로, 스펙을 넘겨 잡으면 안 됩니다.

결론: 상한을 조금 올리는 건 응급책이고, 근본 해법은 커넥션 수 자체를 줄이는 풀링입니다. 상향이 불가피하다면 superuser_reserved_connections를 최소 3~5로 확보해 관리 접속 경로는 남겨두세요.

(b) 커넥션 풀 설정 오류 — 인스턴스 수 × 풀 사이즈 계산

재시작 직후부터 상한 근접(③)의 전형적 원인은 애플리케이션 풀 총합이 max_connections를 초과하는 것입니다. 계산부터 하세요.

CODE
애플리케이션 인스턴스 수 × 인스턴스당 최대 풀 사이즈 ≤ max_connections − reserved

예) 파드 20개 × HikariCP maximumPoolSize 10 = 200
    → max_connections 200이면 이미 여유 0, 관리 접속조차 불가

서버리스·컨테이너 오토스케일링 환경에서 파드가 40개, 80개로 늘면 이 곱셈값이 폭증해 커넥션이 순식간에 고갈됩니다. 이게 요즘 이 에러가 늘어나는 핵심 배경입니다.

항목HikariCP (Java)psycopg_pool (Python)
최대 커넥션maximumPoolSizemax_size
최소 유지minimumIdlemin_size
누수 감지leakDetectionThreshold(ms)반납 누락 시 로그 확인
권장 시작점인스턴스당 (코어수×2) 내외max_size 보수적으로

HikariCP는 leakDetectionThreshold(예: 30000ms)를 켜두면 반납 안 된 커넥션을 로그로 잡아줍니다. 누수 코드를 찾는 데 결정적입니다.

(c) ORM 트랜잭션 미종료 누수 패턴

idle in transaction이 계속 쌓이는 근본 원인은 코드에 있습니다. 자주 보고되는 패턴은 다음과 같습니다.

  • 트랜잭션 블록 안에서 외부 API 호출·긴 연산을 하며 커넥션을 잡고 대기
  • 예외 발생 시 rollback/close가 누락되는 경로
  • 자동 커밋을 끄고(수동 트랜잭션) 커밋을 잊는 코드
  • 세션 스코프를 요청 단위로 닫지 않는 웹 프레임워크 설정

대응: 트랜잭션은 짧게 열고 짧게 닫고, 외부 I/O는 트랜잭션 밖으로 빼세요. 서버 측 안전장치로 idle_in_transaction_session_timeout을 설정하면 방치된 트랜잭션을 자동 종료할 수 있습니다.

SQL
-- 방치된 트랜잭션을 5분 후 자동 종료 (세션/전역 설정 가능)
SET idle_in_transaction_session_timeout = '5min';

pgbouncer 도입 의사결정표

인스턴스가 많아 커넥션 곱셈값을 감당할 수 없다면, 답은 커넥션 풀러입니다. pgbouncer(또는 RDS Proxy)가 애플리케이션과 DB 사이에서 커넥션을 재사용해, 수백 개의 클라이언트 요청을 소수의 실제 DB 커넥션으로 소화합니다. 모드 선택이 핵심입니다.

조건권장 모드이유·주의점
짧은 웹 트랜잭션이 대부분, 커넥션 재사용 극대화가 목표transaction 모드효율 최고. 단, prepared statement·세션 변수(SET)·advisory lock이 세션에 종속되면 깨질 수 있음
prepared statement, 세션 변수, advisory lock, LISTEN/NOTIFY를 많이 씀session 모드호환성 높음. 대신 풀 효율은 낮아 커넥션 절감 효과 축소
레거시·ORM 호환성이 불확실session 모드로 시작안정화 후 transaction 모드 검토

도입 판단 조건: 애플리케이션 인스턴스 수가 가변적(오토스케일)이고, 인스턴스 수 × 풀 사이즈max_connections에 근접하기 시작했다면 도입 시점입니다. transaction 모드를 쓸 땐 애플리케이션 드라이버의 prepared statement 캐시 동작을 반드시 검증하세요(구성 확인 필요).

재발방지: 알림 쿼리

응급 복구로 끝내지 말고, 같은 장애를 미리 잡는 모니터링을 겁니다.

SQL
-- 활성+대기 커넥션이 max_connections의 80%를 넘으면 경보
SELECT count(*) AS current_conns,
       current_setting('max_connections')::int AS max_conns,
       round(100.0 * count(*) / current_setting('max_connections')::int, 1) AS pct
FROM pg_stat_activity
HAVING count(*) > current_setting('max_connections')::int * 0.8;
SQL
-- idle in transaction이 10개 초과로 쌓이면 감지
SELECT count(*) AS idle_in_tx
FROM pg_stat_activity
WHERE state = 'idle in transaction'
HAVING count(*) > 10;

이 두 쿼리를 모니터링 도구(예: Prometheus postgres_exporter, CloudWatch 커스텀 지표)에 주기 실행으로 걸어두면, 커넥션이 임계에 다가갈 때 터지기 전에 알림을 받습니다.

MySQL 계열에서 ERROR 1040: Too many connections를 만났다면 프로세스 모델과 대응이 다르므로 별도의 MySQL 커넥션 런북을 참고하세요.

자주 묻는 질문 (FAQ)

Q. max_connections만 올리면 안 되나요? A. 임시로는 됩니다. 하지만 PostgreSQL은 커넥션당 프로세스를 띄우고 세션당 work_mem을 할당하므로, 상한을 크게 올리면 트래픽 피크에 OOM 위험이 커집니다. 근본 해법은 커넥션 수를 줄이는 풀링(pgbouncer/RDS Proxy)입니다.

Q. pg_terminate_backend로 세션을 죽이면 데이터가 손상되나요? A. 해당 세션의 진행 중이던 트랜잭션은 롤백됩니다. 커밋되지 않은 작업만 취소되므로 데이터 정합성은 유지됩니다. 다만 종료 전 SELECT로 대상을 반드시 확인해 엉뚱한 세션을 죽이지 않도록 하세요.

Q. idle in transaction과 idle의 차이가 뭔가요? A. idle은 트랜잭션 없이 다음 명령을 기다리는 정상 대기 상태입니다. idle in transaction은 트랜잭션을 연 채 다음 명령을 기다리는 상태로, 커넥션과 락을 계속 점유하고 VACUUM을 방해하므로 누수의 주범입니다.

✦ ✦ ✦
편집 검토 · Editorial Review

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

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

Comments

Be the first to comment.