AWS RDS 커넥션 부족? max_connections 계산부터 HikariCP·PgBouncer 설정까지

 

AWS RDS에서 갑자기 'Too many connections' 에러가 뜬다면? PostgreSQL과 MySQL 환경에서 커넥션 풀이 필요한 이유, max_connections 계산법, 그리고 PgBouncer·HikariCP 등 실전 튜닝 방법까지 한 번에 정리했어요.

 

새벽에 슬랙 알림이 울려서 확인해보니 RDS 대시보드에 "Too many connections" 에러가 빼곡하게 찍혀 있었어요. 트래픽이 갑자기 몰린 것도 아닌데 왜 커넥션이 가득 찼는지, 그때는 정말 감이 안 잡히더라고요. 😓

알고 보니 문제는 DB 자체가 아니라 애플리케이션 쪽 커넥션 풀 설정에 있었어요. 이 경험 이후로 커넥션 풀 튜닝의 중요성을 뼈저리게 느꼈는데요, 오늘은 그때 배운 것들을 정리해서 공유해볼게요!

 

깔끔하고 현대적인 플랫 테크니컬 일러스트레이션 스타일로 데이터베이스 연결 풀 아키텍처를 보여준다. 전문적인 블루와 그레이 색상 구성을 사용하며, 왼쪽에는 여러 개의 애플리케이션 서버 아이콘이 있고, 중앙에는 깔대기 모양의 데이터베이스 연결 풀('ACTIVE POOL', 'MAX CONNECTIONS' 표시)이 있다. 이 풀은 오른쪽의 AWS RDS 데이터베이스 실린더('AWS RDS DATABASE CYLINDER', 'DATA STORAGE' 표시)에 연결된다. 연결선의 대부분은 녹색(healthy)이지만, 몇 개의 빨간색 연결(overflow)이 'OVERFLOW', 'WAITING', 'REFUSED'로 표시되어 있다. 16:9 비율.

'Too many connections' 에러, 왜 발생하나요? 🤔

이 에러는 말 그대로 DB 서버가 허용하는 최대 동시 연결 수를 초과했을 때 발생해요. RDS에서 이 한도는 max_connections 파라미터로 결정되는데, 인스턴스 클래스(메모리 크기)에 따라 기본값이 달라요.

근데 진짜 문제는, max_connections 자체보다 커넥션을 낭비하는 구조인 경우가 훨씬 많아요. 흔한 원인들을 보면요:

  • 커넥션 풀 없이 매 요청마다 새 연결을 맺고 끊는 경우
  • 커넥션 풀 사이즈를 서버 인스턴스 수만큼 곱해서 생각하지 않은 경우
  • 슬로우 쿼리가 커넥션을 오래 점유하면서 풀이 고갈되는 경우
  • 커넥션 누수(leak) — 사용 후 반환하지 않는 코드 버그
💡 알아두세요!
커넥션 풀(Connection Pool)은 미리 일정 수의 DB 연결을 만들어두고 재사용하는 기술이에요. 매번 연결을 새로 맺는 비용(TCP 핸드셰이크 + 인증)을 절약할 수 있어서, 사실상 모든 프로덕션 환경에서 필수예요.

 

RDS max_connections 기본값 확인하기 📊

먼저 내 RDS가 허용하는 최대 커넥션 수를 알아야 튜닝 기준이 잡혀요. RDS는 인스턴스 메모리에 따라 자동으로 기본값을 계산해요.

📝 max_connections 계산 공식

MySQL RDS: {DBInstanceClassMemory/12582880} (메모리(바이트) ÷ 12MB)

PostgreSQL RDS: LEAST({DBInstanceClassMemory/9531392}, 5000)

인스턴스 클래스 메모리 MySQL 기본값 PostgreSQL 기본값
db.t3.micro 1GB ~85 ~112
db.t3.medium 4GB ~340 ~450
db.r6g.large 16GB ~1365 ~1802
db.r6g.xlarge 32GB ~2730 ~3604

현재 사용 중인 커넥션 수는 SQL로 직접 조회할 수 있어요.

MySQL:

SHOW STATUS LIKE 'Threads_connected';

PostgreSQL:

SELECT count(*) FROM pg_stat_activity;

⚠️ 주의하세요!
RDS 파라미터 그룹에서 max_connections를 무작정 올리면 안 돼요! 커넥션 하나당 메모리를 소비하기 때문에, 인스턴스 메모리가 부족해지면 OOM이나 성능 저하로 이어질 수 있어요.

 

커넥션 풀 사이즈, 어떻게 정하나요? 🧮

커넥션 풀 사이즈를 정하는 건 생각보다 과학적인 접근이 필요해요. 무조건 크게 잡는다고 좋은 게 아니에요. 오히려 작은 풀이 더 나은 성능을 보여주는 경우가 많아요.

📝 커넥션 풀 사이즈 계산 가이드

기본 공식 = (CPU 코어 수 × 2) + 디스크 수

예를 들어 4코어 서버에 SSD 1개라면 (4 × 2) + 1 = 약 9~10개가 시작점이에요.

그리고 반드시 기억해야 할 건, 전체 커넥션 수 = 풀 사이즈 × 앱 서버 인스턴스 수라는 점이에요.

실전 계산 예시 📝

  • RDS 인스턴스: db.t3.medium (max_connections ≈ 340)
  • 앱 서버: ECS 컨테이너 10개
  • 모니터링/관리용 예비 커넥션: 약 40개

1) 사용 가능 커넥션: 340 - 40(예비) = 300개

2) 컨테이너당 풀 사이즈: 300 ÷ 10 = 30개

→ 각 앱 서버의 커넥션 풀을 최대 30으로 설정하면 안전해요.

💡 알아두세요!
오토스케일링을 사용한다면 최대 인스턴스 수 기준으로 계산해야 해요. 스케일아웃 시 순간적으로 커넥션이 폭증하면서 에러가 재발할 수 있거든요.

 

프레임워크별 커넥션 풀 설정 가이드 🛠️

실제로 설정을 바꾸려면 사용하는 프레임워크와 커넥션 풀 라이브러리에 따라 방법이 달라요. 대표적인 것들을 정리해볼게요.

Java (HikariCP) — Spring Boot 환경

spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.idle-timeout=300000 spring.datasource.hikari.connection-timeout=20000 spring.datasource.hikari.max-lifetime=1200000

설정값 역할 권장값
maximum-pool-size 최대 커넥션 수 10~30
minimum-idle 유휴 시 유지할 최소 커넥션 5~10
idle-timeout 유휴 커넥션 제거까지 대기 시간 300000ms (5분)
max-lifetime 커넥션 최대 수명 1200000ms (20분)

Node.js (pg-pool / mysql2)

const pool = new Pool({ max: 20, idleTimeoutMillis: 30000, connectionTimeoutMillis: 5000, });

Python (SQLAlchemy)

engine = create_engine( DATABASE_URL, pool_size=10, max_overflow=20, pool_timeout=30, pool_recycle=1800, )

 

PostgreSQL이라면 PgBouncer를 고려하세요 🐘

PostgreSQL은 커넥션 하나당 프로세스를 fork하는 구조라서, 커넥션 수가 많아지면 메모리와 CPU 오버헤드가 꽤 커져요. 이때 PgBouncer라는 경량 커넥션 풀러를 중간에 두면 효과가 확실해요.

PgBouncer는 애플리케이션과 DB 사이에서 커넥션을 중계해주는데요, 수백 개의 앱 커넥션을 수십 개의 실제 DB 커넥션으로 압축해서 전달해요. AWS에서도 RDS Proxy라는 관리형 서비스를 제공하고 있어서, 인프라 관리가 부담스럽다면 이쪽을 추천해요.

구분 PgBouncer (자체 운영) RDS Proxy (관리형)
운영 부담 EC2에 직접 설치·관리 AWS가 자동 관리
비용 EC2 비용만 vCPU 시간당 추가 과금
설정 유연성 세밀한 풀 모드 선택 가능 제한적 (핀닝 이슈 존재)
적합한 경우 세밀한 제어가 필요한 팀 운영 간소화를 원하는 팀
⚠️ 주의하세요!
RDS Proxy는 Prepared Statement를 많이 사용하는 앱에서 핀닝(pinning) 현상이 발생할 수 있어요. 핀닝이 되면 커넥션 다중화 효과가 사라져서 오히려 성능이 떨어질 수 있으니, 도입 전에 꼭 테스트해보세요.

 

커넥션 누수, 이렇게 잡으세요 🔍

설정을 아무리 잘 해도 커넥션 누수가 있으면 소용없어요. 누수 의심 징후와 확인 방법을 알아볼게요.

  1. 징후 파악 — CloudWatch에서 DatabaseConnections 지표가 시간이 갈수록 계단식으로 올라간다면 누수를 의심해보세요.
  2. 유휴 커넥션 조회 — PostgreSQL에서 SELECT * FROM pg_stat_activity WHERE state = 'idle';로 놀고 있는 커넥션을 확인해요.
  3. 코드 점검 — try-finally 또는 try-with-resources 패턴으로 커넥션이 반드시 반환되도록 코드를 확인해요.
📌 알아두세요!
HikariCP를 쓴다면 leak-detection-threshold를 설정해보세요. 커넥션이 일정 시간 이상 반환되지 않으면 경고 로그를 찍어줘서 누수 지점을 빠르게 찾을 수 있어요.

 

마무리: 핵심 내용 요약 📝

'Too many connections'는 단순히 max_connections를 올린다고 해결되는 문제가 아니에요. 전체 아키텍처를 고려한 체계적인 접근이 필요해요.

  1. 현황 파악 먼저: 현재 커넥션 수와 max_connections 기본값을 확인하세요.
  2. 풀 사이즈 계산: 전체 앱 인스턴스 수를 곱한 총 커넥션이 DB 한도를 넘지 않도록 설계하세요.
  3. 풀러 도입 검토: PostgreSQL은 PgBouncer나 RDS Proxy로 커넥션 다중화를 고려해보세요.
  4. 누수 방지: leak-detection 설정과 모니터링으로 커넥션 누수를 조기에 발견하세요.

커넥션 풀 튜닝은 한 번 잘 잡아두면 한동안 신경 쓸 일이 없는데, 방치하면 서비스 장애로 직결되는 영역이에요. 혹시 다른 DB 환경에서 겪은 커넥션 이슈가 있다면 댓글로 공유해주세요~ 😊

🗄️

커넥션 풀 튜닝 핵심 요약

📏 풀 사이즈 공식:
(CPU 코어 × 2) + 디스크 수 → 앱 인스턴스 수로 나누어 배분
🔢 총 커넥션 관리: 풀 사이즈 × 인스턴스 수 < max_connections 반드시 확인
🐘 PostgreSQL 최적화: PgBouncer 또는 RDS Proxy로 커넥션 다중화
🔍 누수 방지: leak-detection-threshold 설정 + CloudWatch 모니터링

자주 묻는 질문 ❓

Q: RDS 파라미터 그룹에서 max_connections를 바꾸면 즉시 적용되나요?
A: max_connections는 정적 파라미터라서 변경 후 RDS 인스턴스를 재부팅해야 적용돼요. 운영 중이라면 유지보수 시간대에 적용하는 걸 권장해요.
Q: 커넥션 풀 사이즈를 크게 잡으면 왜 오히려 느려지나요?
A: 동시 쿼리가 늘어나면 DB의 CPU, 메모리, I/O 경합이 심해져서 각 쿼리의 처리 시간이 길어져요. 적은 커넥션으로 순서대로 처리하는 게 전체 처리량(throughput) 면에서 더 효율적인 경우가 많아요.
Q: HikariCP의 max-lifetime은 왜 설정해야 하나요?
A: RDS는 네트워크 장비나 프록시 레벨에서 오래된 유휴 커넥션을 끊을 수 있어요. max-lifetime을 설정하면 커넥션이 끊어지기 전에 풀에서 먼저 교체하므로, 예기치 않은 연결 오류를 방지할 수 있어요.
Q: MySQL에서도 PgBouncer 같은 풀러가 필요한가요?
A: MySQL은 스레드 기반이라 PostgreSQL보다 커넥션 오버헤드가 작아요. 대부분의 경우 애플리케이션 레벨 풀(HikariCP 등)로 충분해요. 다만 서버리스(Lambda) 환경이라면 RDS Proxy 도입을 고려해보세요.
Q: Lambda에서 RDS 연결 시 커넥션 문제가 심한데, 해결 방법이 있나요?
A: Lambda는 호출마다 새 인스턴스가 생길 수 있어서 커넥션 폭증의 대표적 사례예요. RDS Proxy를 사용하면 Lambda의 수많은 연결 요청을 소수의 DB 커넥션으로 다중화해줘서 문제를 효과적으로 해결할 수 있어요.