- Published on
RDS PostgreSQL too many connections 원인·해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡한데도 갑자기 API가 500을 뿜고, 애플리케이션 로그에는 FATAL: sorry, too many clients already가 찍히는 순간이 있습니다. RDS PostgreSQL에서는 이 에러가 단순히 “접속이 많다”를 넘어 커넥션 관리(풀링/누수), 트래픽 패턴, 오토스케일링, DB 파라미터 설계가 함께 무너졌다는 신호인 경우가 많습니다.
이 글은 원인별로 무엇을 확인해야 하는지, 그리고 **가장 효과가 큰 해결책(우선순위 포함)**을 RDS PostgreSQL 기준으로 정리합니다.
증상 정리: 무엇이 “too many connections”인가
PostgreSQL은 max_connections를 초과하는 새 접속을 거부합니다. 대표 메시지는 아래 중 하나입니다.
- 애플리케이션:
FATAL: sorry, too many clients already - 드라이버/ORM:
remaining connection slots are reserved for non-replication superuser connections - 장애 양상: 신규 요청만 실패(기존 커넥션으로 처리되는 요청은 간헐적으로 성공), DB CPU는 낮은데도 장애 발생
핵심은 동시 접속 수(세션)가 폭증했거나, 반환되어야 할 커넥션이 반환되지 않아 누적되었거나, 풀링이 없어 트래픽이 곧 커넥션 수로 직결되는 구조일 가능성이 높다는 점입니다.
1차 진단: 지금 누가 커넥션을 잡고 있나
장애 시점에 가장 먼저 해야 할 것은 “커넥션이 어디서, 어떤 상태로 쌓였는지”를 보는 것입니다.
pg_stat_activity로 세션 상태 확인
-- 어떤 유저/DB/클라이언트가 세션을 많이 잡는지
SELECT
usename,
datname,
client_addr,
application_name,
state,
count(*) AS cnt
FROM pg_stat_activity
GROUP BY 1,2,3,4,5
ORDER BY cnt DESC;
-- idle in transaction(트랜잭션 열린 채 대기) 탐지
SELECT
pid,
usename,
datname,
client_addr,
application_name,
state,
now() - xact_start AS xact_age,
now() - query_start AS query_age,
left(query, 200) AS query
FROM pg_stat_activity
WHERE state IN ('idle in transaction', 'active')
ORDER BY xact_age DESC NULLS LAST
LIMIT 50;
여기서 특히 위험한 패턴은 다음입니다.
state = 'idle in transaction'이 많다 → 커넥션이 트랜잭션을 물고 반환되지 않음- 특정
application_name/client_addr가 압도적으로 많다 → 특정 서비스/배포 버전/배치 잡이 범인 active가 많고 쿼리 시간이 길다 → 슬로우 쿼리로 커넥션이 오래 점유
RDS/Aurora 지표로 “풀 고갈”인지 확인
CloudWatch에서 다음을 같이 봅니다.
DatabaseConnections가max_connections근처에서 천장에 붙는지CPUUtilization은 낮은데DatabaseConnections만 높은지(풀 미설정/누수 가능성)FreeableMemory급감(커넥션당 메모리 사용량 누적)
원인 1: 커넥션 풀 미사용(요청당 새 연결)
가장 흔한 원인입니다. 특히 서버리스/컨테이너 환경에서 “요청마다 DB 연결 생성 → 요청 끝나면 종료” 패턴은 트래픽이 조금만 올라가도 DB가 연결 생성/종료 비용과 동시 세션 수에서 먼저 무너집니다.
해결: 애플리케이션 레벨 풀 적용(필수)
Java/Spring(HikariCP) 예시
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 5
connection-timeout: 3000
idle-timeout: 600000
max-lifetime: 1800000
leak-detection-threshold: 20000
maximum-pool-size를 무작정 키우는 게 답이 아닙니다. (Pod 수 × 풀 크기) ≤ DB가 감당 가능한 커넥션 수가 되도록 설계해야 합니다.leak-detection-threshold는 커넥션 누수 탐지에 매우 유용합니다.
Node.js(pg) 예시
import pg from 'pg';
export const pool = new pg.Pool({
host: process.env.PGHOST,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
database: process.env.PGDATABASE,
port: 5432,
max: 20, // 풀 크기
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 3000,
});
// 반드시 finally에서 release
export async function query(text, params) {
const client = await pool.connect();
try {
return await client.query(text, params);
} finally {
client.release();
}
}
원인 2: 커넥션 누수(반환 누락) 또는 예외 경로 누락
코드에서 close()/release()가 예외 경로에서 호출되지 않거나, 스트리밍/커서/트랜잭션 처리 중 반환이 누락되면 커넥션이 계속 누적됩니다.
해결: 누수 탐지 + 안전한 패턴 강제
- 자바: Hikari leak detection, AOP로 트랜잭션 경계 점검
- Node:
try/finally로release()강제 - Python: context manager 사용
import psycopg2
from psycopg2.pool import SimpleConnectionPool
pool = SimpleConnectionPool(1, 20, dsn="...")
def run(sql, params=None):
conn = pool.getconn()
try:
with conn:
with conn.cursor() as cur:
cur.execute(sql, params)
return cur.fetchall()
finally:
pool.putconn(conn)
원인 3: 트랜잭션이 길다(특히 idle in transaction)
웹 요청 처리 중 외부 API 호출, 파일 업로드, 긴 비즈니스 로직을 트랜잭션 안에서 수행하면 커넥션이 오래 점유됩니다. 트래픽이 동일해도 동시 커넥션 수가 증가합니다.
해결: 트랜잭션 범위 축소 + 타임아웃 설정
- 트랜잭션은 DB 작업만 감싸고 외부 I/O는 밖으로 빼기
- PostgreSQL 파라미터로 방어(가능하면 파라미터 그룹 적용)
-- 세션 단위로도 가능(애플리케이션에서 커넥션 획득 후 실행)
SET statement_timeout = '5s';
SET idle_in_transaction_session_timeout = '30s';
RDS에서는 보통 DB Parameter Group에 idle_in_transaction_session_timeout을 넣어 “트랜잭션 물고 잠수”를 강제 종료하는 전략이 효과적입니다.
원인 4: 오토스케일링/배포로 Pod 수가 늘며 풀 총량 폭주
Kubernetes/ECS에서 HPA/오토스케일링이 걸리면, 각 인스턴스가 동일한 풀 크기를 갖는 순간 총 커넥션 수는 선형 증가합니다.
예: Pod 30개 × 풀 20개 = 600 커넥션. DB max_connections가 300이면 반드시 터집니다.
해결: “총량” 기준으로 풀 설계 + HPA 상한 설정
- 풀 크기를 줄이고(예: 5~10), 필요한 경우 RDS Proxy/pgBouncer로 풀링을 중앙화
- HPA maxReplicas를 DB 수용량과 함께 결정
HPA/Pod 이슈로 장애가 연쇄되는 경우, DB만이 아니라 클러스터 디버깅도 같이 필요합니다. 예를 들어 장애 중 kubectl exec/logs가 안 되거나, 네트워크가 비정상이라 재시도가 폭주하면 커넥션이 더 빨리 고갈됩니다. 이런 경우는 아래 글들도 함께 참고할 만합니다.
원인 5: max_connections 자체가 낮거나(혹은 메모리로 제한)
PostgreSQL에서 max_connections는 무한정 올릴 수 없습니다. 커넥션은 메모리/백엔드 프로세스 비용이 있고, RDS 인스턴스 클래스가 작으면 max_connections가 낮게 잡히기도 합니다.
해결: 올리기 전에 “왜 필요한지”부터 줄이기
우선순위는 보통 다음이 안전합니다.
- 애플리케이션 풀링 적용/수정
- 누수 제거
- 트랜잭션/쿼리 최적화
- 그래도 부족하면 그때
max_connections조정 또는 인스턴스 업그레이드
max_connections를 올리는 것은 즉효가 있지만, 슬로우 쿼리/누수/풀 미설정이 있으면 더 큰 폭발로 돌아옵니다.
가장 강력한 해결책: RDS Proxy(또는 pgBouncer)로 커넥션 풀링 중앙화
트래픽이 가변적이고(스파이크), 서비스가 많고, Pod가 자주 늘었다 줄었다 하는 환경에서는 애플리케이션 풀만으로 안정성을 확보하기 어렵습니다. 이때 RDS Proxy(Aurora/RDS 지원) 또는 pgBouncer를 도입하면:
- DB로 들어가는 실제 커넥션 수를 제한
- 애플리케이션에서 짧은 커넥션을 많이 만들어도 프록시가 흡수
- 장애 시 재연결 폭주를 완화
다만 프록시 도입 시에는 트랜잭션/세션 특성(세션 고정 필요 여부), prepared statement, 세션 변수 사용 패턴 등을 점검해야 합니다.
장애 중 응급조치(재발 방지와 별개)
장애를 즉시 완화해야 할 때는 “원인 제거”가 아니라 “압력 낮추기”가 목적입니다.
1) 문제 세션 종료
-- 오래된 idle in transaction 우선 종료(주의: 롤백 발생)
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle in transaction'
AND now() - xact_start > interval '1 minute';
2) 애플리케이션에서 재시도 폭주 차단
- DB 에러에 대한 무한 재시도 제거
- 지수 백오프 + jitter 적용
- 서킷 브레이커로 DB 호출 자체를 잠시 차단
재시도 폭주는 네트워크 이슈와 결합하면 더 심해집니다. EKS에서 egress가 막혀 타임아웃이 늘어나면 워커가 “연결 시도 → 타임아웃 → 재시도”를 반복하며 커넥션/스레드를 소진할 수 있습니다.
재발 방지 체크리스트(운영 기준)
애플리케이션
- 풀 사용 여부(없으면 즉시 도입)
- 풀 크기 산정:
풀 크기 × 인스턴스 수가 DB 한계를 넘지 않는가 try/finally로 반환 강제, 누수 탐지 활성화- 트랜잭션 범위 최소화, 외부 I/O 트랜잭션 밖으로
- 타임아웃: connection/query/transaction 타임아웃 설정
데이터베이스(RDS)
idle_in_transaction_session_timeout설정- 슬로우 쿼리 로깅 및 상위 N 쿼리 개선
max_connections는 최후에 조정(메모리/성능 영향 고려)
인프라/스케일링
- HPA 상한을 DB 수용량 기반으로 설정
- 스파이크가 큰 서비스는 RDS Proxy/pgBouncer 고려
- 배포 시 커넥션 폭증(웜업, readiness, 점진 롤아웃) 제어
결론: “커넥션 수”가 아니라 “커넥션 수요”를 줄여야 한다
RDS PostgreSQL의 too many connections는 대개 DB가 약해서가 아니라, 애플리케이션이 커넥션을 비싸게 쓰고(요청당 생성), 오래 쥐고(긴 트랜잭션), 반환하지 않거나(누수), 스케일링으로 총량이 폭주해서 발생합니다.
가장 효과가 큰 순서는 보통 (1) 풀링 적용/수정 → (2) 누수 제거 → (3) 트랜잭션/쿼리 최적화 및 타임아웃 → (4) RDS Proxy/pgBouncer → (5) max_connections/인스턴스 업그레이드입니다. 이 순서로 접근하면 “일단 올려서 버티기”가 아니라, 장애를 구조적으로 없애는 방향으로 안정화할 수 있습니다.