새벽 3시, PostgreSQL 연결 고갈 장애에 직면했을 때: 'too many clients' 완벽 대응 가이드
운영 중인 서비스의 모니터링 대시보드가 갑자기 빨간불을 켜는 순간, 그 불안감은 이루 말할 수 없습니다. 특히 PostgreSQL에서 FATAL: sorry, too many clients already 에러를 마주했다면, 당장 서비스가 멈춘 상황일 겁니다. 대부분의 엔지니어들은 본능적으로 postgresql.conf 파일을 열어 max_connections 값을 무작정 늘리는 것부터 시도합니다. 하지만 이는 임시방편일 뿐이며, 더 큰 재앙(OOM Killer)을 부를 수 있습니다.
이 글은 단순한 설정값 변경 가이드가 아닙니다. 이 글은 당신이 장애 상황에서 5분 안에 서비스를 정상화하는 '응급처치 매뉴얼'이자, 이 문제가 재발하는 근본 원인을 찾아 영구적으로 차단하는 '아키텍처 설계 가이드'입니다.
🚨 지금 서비스가 멈췄다면? 5분 응급조치 매뉴얼 (Read First!)
장애 상황에서는 이론을 논할 시간이 없습니다. 가장 먼저 해야 할 일은 현재 연결을 점유하고 있는 '좀비 세션'을 찾아 강제 종료하는 것입니다. 아래 쿼리들을 순서대로 실행하며 상황을 진단하고 조치하세요.
1. 현재 최대 연결 제한 확인:
SHOW max_connections;설명: 현재 PostgreSQL이 허용하는 최대 연결 수를 확인합니다.
2. 현재 활성 세션 개수 확인:
SELECT count(*) FROM pg_stat_activity;설명: 현재 실제로 연결을 사용하고 있는 세션의 총 개수를 파악합니다.
3. 비정상적으로 오래 점유된 세션 진단:
SELECT pid, state, query_start, state_change, query
FROM pg_stat_activity
WHERE state IN ('idle','idle in transaction')
ORDER BY state_change;설명: '유휴(idle)' 상태이거나 '트랜잭션 중간(idle in transaction)' 상태로 오래 멈춰있는 세션을 찾아냅니다. 이들이 주범일 확률이 높습니다.
4. 위험 세션 강제 종료 (최후의 수단):
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND state_change < now() - interval '5 minutes';설명: 5분 이상 '트랜잭션 중간' 상태로 방치된 세션의 PID를 찾아 강제 종료합니다. 이 쿼리는 연결 고갈의 가장 흔한 원인인 '커넥션 누수'를 해결합니다.
📉 왜 max_connections를 무작정 올리면 안 되는가? (메모리 폭발의 위험)
많은 분들이 연결이 부족하면 숫자를 늘리는 것이 답이라고 생각합니다. 하지만 PostgreSQL는 연결 하나하나가 메모리를 점유합니다. 이 메모리 점유량은 단순히 연결 수에 비례하지 않고, 각 세션이 사용하는 work_mem 설정값과 연관되어 기하급수적으로 늘어날 수 있습니다.
[메모리 폭증 시나리오 예시]
만약 work_mem이 기본값인 4MB로 설정되어 있고, 500개의 연결이 동시에 복잡한 정렬(Sort)이나 해시(Hash) 연산을 수행한다고 가정해 봅시다. 이론적으로 각 세션이 최대 4MB의 메모리를 추가로 사용하게 되므로, 총 메모리 사용량은 $4\text{MB} \times 500\text{개} = 2000\text{MB}$ (2GB)에 달합니다.
만약 이 숫자가 수천 개로 늘어나면, 서버의 물리적 메모리 한계를 넘어서게 되고, 결국 운영체제(OS)가 가장 메모리를 많이 사용하는 프로세스부터 강제 종료시키는 OOM Killer에 의해 데이터베이스 자체가 다운되는 최악의 상황이 발생합니다. 따라서 연결 증가는 항상 '누수 방지'와 '풀링'이라는 관점에서 접근해야 합니다.
🔍 연결 고갈을 부르는 5가지 근본 원인 진단
응급조치로 서비스를 살렸다면, 이제 왜 이런 일이 발생했는지 근본 원인을 찾아야 합니다.
max_connections의 물리적 한계 도달: 설정 자체가 너무 낮게 잡혀있을 때.- 애플리케이션 레벨의 커넥션 누수: 트랜잭션 완료 후
connection.close()를 호출하지 않아 세션이 계속 점유되는 경우. idle in transaction장기 점유: 개발자가 트랜잭션을 시작(BEGIN)만 하고 커밋/롤백을 잊어버려 세션이 대기 상태로 멈추는 경우. (가장 흔함)- 애플리케이션 풀 미사용: 각 요청마다 DB 연결을 새로 맺고 끊는 비효율적인 방식.
- 슈퍼유저 예약 연결 고갈: 관리자 계정이나 백그라운드 작업이 너무 많은 연결을 독점하여 일반 서비스 연결에 영향을 주는 경우.
🚀 진짜 해결책: PgBouncer 트랜잭션 풀링 도입 (The Game Changer)
단순히 max_connections를 올리는 대신, **커넥션 풀러(Connection Pooler)**를 도입해야 합니다. 그중 가장 널리 쓰이고 효과적인 것이 PgBouncer입니다.
PgBouncer는 애플리케이션과 PostgreSQL 사이에 가상의 계층을 만들어, 수많은 클라이언트 연결 요청을 받아 내부적으로 적은 수의 실제 DB 연결만 유지하게 해주는 '중개자' 역할을 합니다.
💎 PgBouncer 설정 핵심: pool_mode = transaction
가장 중요한 설정은 pool_mode입니다. 단순한 연결 유지(Session) 모드가 아닌, 트랜잭션(Transaction) 모드를 사용해야 합니다. 이 모드는 연결이 트랜잭션 단위로만 사용되도록 강제하여, 세션이 오랫동안 점유되는 것을 원천적으로 차단합니다.
[복붙 가능한 pgbouncer.ini 설정 예시]
[databases]
host=localhost port=5432 dbname=mydatabase pool_size=20 max_client_conn=100 pool_mode=transaction
[auth]
# SCRAM 또는 md5 인증 방식을 사용하세요.
auth-type=scram-sha-256
password-file=/etc/pgbouncer/userlist.txt💡 주의사항: pool_mode = transaction을 사용하면, 애플리케이션에서 PREPARED STATEMENT를 사용하거나, 세션 레벨의 SET 명령(예: SET TIME ZONE 'UTC')을 실행하는 기능은 풀러가 이를 감지하고 에러를 발생시킬 수 있으므로, 애플리케이션 코드를 점검해야 합니다.
🛡️ 재발 방지 및 고급 튜닝 체크리스트
PgBouncer를 도입했더라도, 애플리케이션과 DB 설정 자체를 튜닝해야 합니다.
1. 애플리케이션 풀 크기 산정 공식 (HikariCP 기준)
자바 환경에서 널리 쓰이는 HikariCP의 경우, 풀 크기 산정 공식은 경험적인 규칙을 따릅니다.
$$ \text{Connections} = ((\text{Core Count} \times 2) + \text{Effective Spindle Count}) $$
여기에 애플리케이션 인스턴스 수 $\times$ 풀 크기의 총합이 PgBouncer의 max_client_conn보다 작도록 역산하여 안전 마진을 확보해야 합니다.
2. 세션 타임아웃 설정 (PostgreSQL.conf)
트랜잭션이 끝날 때까지 대기하는 시간을 제한해야 합니다.
# postgresql.conf
idle_in_transaction_session_timeout = '30s'이 설정을 통해 30초 이상 트랜잭션을 열어두고 아무 작업도 하지 않으면 세션이 자동으로 종료됩니다.
3. 슈퍼유저 연결 예약 및 모니터링
관리자 계정(슈퍼유저)이 사용할 연결은 별도로 예약하는 것이 안전합니다.
# postgresql.conf
superuser_reserved_connections = 5 또한, 실시간 모니터링을 위해 아래 쿼리를 주기적으로 실행하여 연결 사용률을 파악하고, 80% 초과 시 알림을 받도록 튜닝하는 것을 강력히 권장합니다.
-- 연결 사용률 (%) 산출 쿼리
SELECT
(SELECT count(*) FROM pg_stat_activity) * 100.0 / (SELECT setting FROM pg_settings WHERE name = 'max_connections');실무자 경험 공유: 저는 과거에 서버리스 환경으로 전환하면서 이 문제를 겪었습니다. 트래픽이 폭발적으로 늘어날 때마다 DB 연결이 기하급수적으로 증가했죠. 결국 PgBouncer를 도입하고, 애플리케이션 레벨에서 try-with-resources 구문을 강제 적용하여 연결 누수를 0%로 만든 것이 가장 큰 전환점이었습니다.
자주 묻는 질문 (FAQ)
Q. PgBouncer를 사용하면 max_connections 설정을 아예 건드리지 않아도 되나요?
A. 아닙니다. PgBouncer는 클라이언트 요청을 받아 내부적으로 실제 DB에 연결합니다. 따라서 max_connections는 PgBouncer가 관리하는 '실제 DB 연결 수'를 감당할 수 있도록 충분히 설정되어야 합니다.
Q. idle in transaction이 발생하는 가장 흔한 원인은 무엇인가요?
A. 가장 흔한 원인은 애플리케이션 로직에서 BEGIN을 호출했지만, 예외 처리 블록이나 정상 종료 로직에서 COMMIT 또는 ROLLBACK을 호출하는 것을 잊어버렸기 때문입니다.
Q. PgBouncer와 RDS Proxy 중 무엇을 써야 하나요? A. 두 가지 모두 커넥션 풀링의 목적은 같습니다. RDS Proxy는 AWS 생태계에 최적화된 관리형 서비스이며, PgBouncer는 오픈소스 기반으로 더 세밀한 커스터마이징과 트랜잭션 모드 제어에 강점을 가집니다. 환경과 요구사항에 맞춰 선택하시면 됩니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...