HikariPool-1 Connection is not available 30000ms 타임아웃 해결법
새벽 3시 알람을 멈추는 30초 진단법
HikariPool-1 - Connection is not available, request timed out after 30000ms이 로그가 슬랙에 뜨는 순간, 십중팔구 트래픽이 갑자기 튀었거나 어딘가에서 커넥션을 반납하지 않고 있습니다. 가장 먼저 마음을 다잡아야 할 핵심 관점은 이것입니다.
DB가 죽은 게 아니라, 애플리케이션의 커넥션 풀이 고갈된 것이다.
DB 서버의 too many clients / Too many connections 에러가 "DB 쪽 한계"라면, 이 에러는 "내 애플리케이션이 가진 풀 안에 빌려줄 커넥션이 없다"는 신호입니다. 30초(connectionTimeout 기본값)를 기다려도 누구도 커넥션을 돌려주지 않아 요청이 포기한 것이죠. Spring Boot 3.x는 기본 DataSource가 HikariCP이므로, 이 글의 설정은 별도 의존성 없이 바로 적용됩니다.
이 글은 이론을 최소화하고 로그 판독 → 원인 분기 → 복붙 설정 → 검증 순서로 즉시 복구하는 데 집중합니다.
1. 에러 로그 정확히 읽기: Pool stats 한 줄 해부
HikariCP는 타임아웃 직전 보통 이런 통계 라인을 함께 찍습니다. 이 한 줄에 진단의 90%가 들어 있습니다.
HikariPool-1 - Pool stats (total=10, active=10, idle=0, waiting=12)- total: 현재 풀이 보유한 전체 커넥션 수
- active: 지금 사용 중(빌려간) 커넥션 수
- idle: 놀고 있는(빌려줄 수 있는) 커넥션 수
- waiting: 커넥션을 못 받아 대기 중인 스레드 수
패턴별 판독표
| active | idle | waiting | 해석 | 1차 의심 원인 |
|---|---|---|---|---|
| = max | 0 | > 0 | 풀 고갈 확정 | ①누수 / ②사이즈 과소 / ③슬로우쿼리 |
| < max | 많음 | > 0 | 빌려줄 게 있는데 못 빌려줌 | DB 신규 연결 지연, ④max_connections |
| = max | 0 | 0인데 점점 안 줄어듦 | 반납이 안 됨 | ①커넥션 누수 강력 의심 |
| 들쭉날쭉 + 주기적 timeout | - | - | 죽은 커넥션 사용 | ⑤네트워크/idle 타임아웃 |
active=max이고 waiting>0이면 일단 "고갈"로 확정하고, 그 다음 5대 원인 분기로 들어갑니다.
2. 5대 원인 분기와 원인별 진단·설정
① 커넥션 누수 (미반납)
증상: active가 max에 붙은 채 시간이 지나도 idle로 돌아오지 않음. 트래픽이 줄어도 회복이 안 됨.
진단: leakDetectionThreshold 설정 후 로그에서 "Apparent connection leak detected" 확인 (4장 참고).
② 풀 사이즈 과소
증상: 평소엔 괜찮다가 피크 타임에만 waiting 급증. active는 정상적으로 반납됨. 진단: APM/메트릭에서 active가 늘 max에 붙어 있는지 확인.
③ 슬로우 쿼리·롱 트랜잭션
증상: active=max인데 각 커넥션이 오래 점유됨. DB CPU/슬로우로그에 흔적. 진단(MySQL):
SHOW PROCESSLIST;
SHOW STATUS LIKE 'Threads_connected';진단(PostgreSQL) — 트랜잭션을 연 채 놀고 있는 세션 찾기:
SELECT pid, state, now() - xact_start AS duration, query
FROM pg_stat_activity
WHERE state = 'idle in transaction'
ORDER BY duration DESC;④ DB max_connections 한계
증상: 풀은 더 늘리려는데 DB가 신규 연결을 거부. 마이크로서비스에서 서비스 수 × 풀 사이즈가 DB 한계를 넘는 전형적 사고.
진단(PostgreSQL): SHOW max_connections; 와 현재 연결 수 비교.
⑤ 네트워크/방화벽 idle 타임아웃
증상: 한가한 새벽에 첫 요청에서만 간헐적 timeout. 죽은 커넥션을 풀이 살아있다고 착각.
해결: maxLifetime을 DB/방화벽 타임아웃보다 짧게 (6장 도식).
복붙용 application.yml
spring:
datasource:
hikari:
# 동시에 빌려줄 수 있는 최대 커넥션. 풀 사이즈 공식으로 산정(아래 참고)
maximum-pool-size: 20
# 항상 유지할 최소 유휴 커넥션. 보통 max와 같게 두면 풀 출렁임이 줄어듦
minimum-idle: 20
# 커넥션을 못 받을 때 대기 시간(ms). 30초는 너무 길다 → 빠르게 실패시켜 스레드 회수
connection-timeout: 10000
# 유휴 커넥션 정리 시간(ms). minimum-idle == max면 무의미
idle-timeout: 600000
# 커넥션 최대 수명(ms). 반드시 DB wait_timeout/방화벽보다 짧게! (예: 30분)
max-lifetime: 1800000
# 미반납 의심 임계(ms). 운영에서 30초 설정해 누수 스택트레이스 확보
leak-detection-threshold: 30000실무 팁:
connection-timeout을 기본 30초로 두면 장애 시 스레드들이 30초씩 묶여 톰캣 워커가 통째로 마비됩니다. 저는 10초로 줄여 "빨리 실패하고 재시도"하는 편을 선호합니다. 사용자에게 30초 흰 화면보다 즉시 에러 후 재시도가 낫습니다.
3. 풀 사이즈 산정 공식
무작정 maximum-pool-size: 100은 최악의 처방입니다. 풀을 키우면 DB로 동시 쿼리가 몰려 오히려 DB가 먼저 죽습니다. HikariCP 위키가 권장하는 출발점은 이렇습니다.
connections = (core_count * 2) + effective_spindle_countcore_count: DB 서버의 CPU 코어 수effective_spindle_count: 디스크 수(SSD/클라우드면 보통 작게 또는 0~1로 근사)
8코어 DB라면 대략 8*2 + 1 = 17 정도가 합리적 시작점입니다. 마이크로서비스 환경이라면 여기서 한 번 더 곱셈을 해야 합니다. 서비스 10개 × 풀 20 × 오토스케일 인스턴스 5개 = 1,000 커넥션이 한 DB로 쏟아집니다. 컨테이너 오토스케일링으로 인스턴스가 늘면 풀 총합도 함께 폭증하므로, 풀 사이즈는 "인스턴스 한 대 기준"이 아니라 "총합 기준"으로 관리하세요.
4. 누수 범인 잡기: leakDetectionThreshold
leak-detection-threshold: 30000을 켜면, 30초 안에 반납되지 않은 커넥션에 대해 HikariCP가 스택트레이스를 찍어줍니다.
Apparent connection leak detected:
java.lang.Exception
at com.example.order.OrderService.process(OrderService.java:42)
...스택트레이스 맨 위 OrderService.java:42가 바로 커넥션을 빌리고 안 돌려준 범인입니다.
Before / After
케이스 A — try-with-resources 누락
// Before: 예외 발생 시 close()가 호출되지 않아 누수
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql);
ps.executeQuery(); // 여기서 예외 나면 conn 영영 안 돌아옴
// After: try-with-resources로 자동 반납 보장
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.executeQuery();
}케이스 B — @Transactional 범위 과대
// Before: 외부 API 호출이 트랜잭션 안에 있어 커넥션을 수초간 점유
@Transactional
public void order() {
save(entity);
slowExternalApiCall(); // 3초 동안 DB 커넥션 붙잡고 대기
}
// After: DB 작업만 트랜잭션으로 좁히고, 외부 호출은 밖으로
public void order() {
saveInTx(entity); // @Transactional 메서드
slowExternalApiCall(); // 커넥션을 이미 반납한 상태
}JPA/Spring을 쓴다면 대부분 누수는 "수동 getConnection()"이나 "트랜잭션 범위가 너무 넓은" 두 패턴에서 나옵니다.
5. maxLifetime ↔ DB idle timeout 충돌 도식
⑤번 원인의 핵심입니다. DB의 wait_timeout이나 방화벽이 유휴 커넥션을 조용히 끊었는데, 풀은 그 커넥션이 살아있다고 믿고 빌려줍니다. → 빌린 쪽에서 쓰자마자 끊긴 커넥션이라 실패 → 재시도가 몰려 타임아웃.
방화벽 idle cut: 600초
DB wait_timeout : 28800초(MySQL 기본 8시간)
HikariCP maxLifetime: 1800초 ← 가장 짧게!
[OK] 풀이 1800초마다 스스로 커넥션을 폐기·재생성 → 죽은 커넥션을 빌려주지 않음
[BAD] maxLifetime > 방화벽/DB timeout → 풀이 죽은 커넥션을 살아있다고 착각규칙: maxLifetime은 항상 DB wait_timeout과 방화벽 idle 타임아웃 중 가장 작은 값보다 짧게. 보통 그 값의 80% 이하로 잡습니다.
결론: 재발 방지 체크리스트
-
leak-detection-threshold를 운영에서 켜뒀는가 (누수 조기 경보) -
maximum-pool-size를 공식 기반으로 산정했는가 (무작정 키우지 않기) -
maxLifetime < DB wait_timeout / 방화벽 idle timeout인가 - 풀 stats(active/idle/waiting)를 메트릭으로 수집·알림 거는가
- 마이크로서비스 총합(서비스×풀×인스턴스)이 DB max_connections를 넘지 않는가
자주 묻는 질문 (FAQ)
Q. connection-timeout(30초)을 더 늘리면 해결되나요? A. 아니요. 타임아웃을 늘리면 에러가 잠시 미뤄질 뿐, 고갈된 풀은 그대로입니다. 오히려 대기 스레드가 더 오래 묶여 전체 응답이 느려집니다. 원인(누수·사이즈·슬로우쿼리)을 잡아야 합니다.
Q. 풀 사이즈는 무조건 크게 잡으면 안전한가요?
A. 위험합니다. (core_count * 2) + spindle을 시작점으로 삼으세요. 풀을 키우면 DB로 동시 쿼리가 몰려 DB가 먼저 죽고, 마이크로서비스에선 서비스 수만큼 곱해져 max_connections를 초과합니다.
Q. 누수인지 슬로우 쿼리인지 어떻게 구분하나요?
A. 트래픽이 줄어도 active가 안 내려오면 누수, 피크 때만 active=max로 붙고 평소 회복되면 슬로우 쿼리/사이즈 부족입니다. leak-detection-threshold 로그와 pg_stat_activity(idle in transaction)를 함께 보면 확실합니다.
이 글은 AI 에이전트가 1차 초안을 작성한 뒤, 사람 편집자가 사실관계·출처·톤과 맥락을 검토하여 발행했습니다. 오류나 부정확한 내용이 확인되면 24시간 이내에 정정합니다.
댓글
불러오는 중...