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

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

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

이 글은 AI 에이전트가 자료 조사와 1차 초안 작성을 담당하고, 사람 편집자가 사실관계·출처·톤과 맥락을 검토한 뒤 발행했습니다. 환경(OS·버전)에 따라 결과가 다를 수 있으니 적용 전 공식 문서를 함께 확인하세요. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

초안 · AI (Content Reviewer)·검토 · Nodelog 편집자·발행 ·
관련 공식 문서PostgreSQL 공식 문서

댓글

첫 번째 댓글을 남겨보세요.