- Published on
Spring Boot HikariCP 풀 고갈·DB 타임아웃 10분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 멀쩡해 보이는데도 갑자기 API 지연이 폭증하고, 이어서 SQLTransientConnectionException(커넥션 획득 타임아웃)이나 DB 타임아웃이 연쇄적으로 터지는 경우가 있습니다. 대부분의 시작점은 HikariCP 풀 고갈(pool exhaustion) 입니다.
풀 고갈은 “커넥션이 부족하다”로 끝나지 않습니다. (1) 애플리케이션이 커넥션을 오래 쥐고 있거나, (2) DB가 새 커넥션을 못 받아주거나, (3) 네트워크/인프라가 연결을 지연시키거나, (4) 트래픽/스레드 구조가 풀보다 공격적일 때 발생합니다.
이 글은 원인 추측 대신, 10분 안에 원인을 좁히는 순서로 정리합니다.
0) 증상 패턴 빠르게 확인(1분)
다음 로그/메트릭이 보이면 “풀 고갈” 가능성이 큽니다.
- 애플리케이션 로그
HikariPool-1 - Connection is not available, request timed out after 30000msjava.sql.SQLTransientConnectionExceptionorg.hibernate.exception.JDBCConnectionException
- 지연 패턴
- 특정 엔드포인트만 느린 게 아니라 DB 붙는 요청 전체가 계단식으로 느려짐
- 타임아웃 직전 스레드 대기 증가(Tomcat/Netty worker가 블로킹)
핵심 질문은 하나입니다.
> “커넥션이 부족한가(풀 크기 문제)?”가 아니라 “커넥션이 왜 반환되지 않거나, 왜 새로 못 만드는가?”
1) HikariCP 핵심 설정 5개부터 덤프(2분)
먼저 현재 설정을 확정해야 합니다. 환경별로 값이 달라서 ‘기억’은 의미가 없습니다.
application.yml 예시(기준점 만들기)
spring:
datasource:
hikari:
maximum-pool-size: 20
minimum-idle: 20
connection-timeout: 30000 # 커넥션 획득 대기
idle-timeout: 600000
max-lifetime: 1800000 # RDS/NLB idle timeout보다 짧게
keepalive-time: 0
leak-detection-threshold: 0
validation-timeout: 5000
pool-name: app-hikari
지금 당장 확인할 것
maximumPoolSize: 풀 상한connectionTimeout: 고갈 시 “몇 초 후 예외”로 드러나는지maxLifetime: 네트워크 장비/DB가 커넥션을 끊기 전에 애플리케이션이 먼저 교체하는지leakDetectionThreshold: 커넥션 누수/장기 점유 탐지minimumIdle: 피크 대비 idle 확보 전략
운영에서 흔한 함정:
minimumIdle을maximumPoolSize와 같게 두면, 트래픽이 적어도 커넥션을 계속 유지합니다. DB 커넥션 수 제한이 빡빡한 환경(RDS 소형 등)에서는 다른 서비스까지 밀어내며 장애를 키울 수 있습니다.
DB 자체 커넥션 제한 문제로 번지는 케이스는 아래 글도 함께 보면 좋습니다.
2) “풀 고갈”인지 “DB가 느린 것”인지 1분에 구분
풀 고갈은 보통 두 부류로 나뉩니다.
- 커넥션은 있는데(풀 크기 충분) 쿼리가 느려서 반환이 늦음 → active가 꽉 차고 대기열 증가
- 아예 커넥션을 못 만듦/못 가져옴 → DB/네트워크/인증/최대 커넥션 제한
Actuator + Micrometer로 바로 보기
Spring Boot Actuator를 켜면 Hikari 메트릭을 즉시 볼 수 있습니다.
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
아래 지표를 확인합니다.
hikaricp.connections.activehikaricp.connections.idlehikaricp.connections.pendinghikaricp.connections.timeout
판단 기준(대략):
active == maximumPoolSize이면서pending > 0급증 → 풀 고갈 확정timeout이 증가 → 획득 실패가 실제로 발생idle이 있는데도pending이 높다 → 스레드/락/커넥션 검증 문제 가능(드묾)
3) 커넥션 누수 vs 장기 점유를 3분 안에 잡는 법
3-1) Leak Detection을 “짧게” 켜서 증거 확보
운영에서 영구적으로 켜면 노이즈가 생길 수 있으니, 장애 재현 구간에만 임시로 켭니다.
spring:
datasource:
hikari:
leak-detection-threshold: 5000 # 5초 이상 점유 시 스택트레이스 로그
로그에 “커넥션을 빌린 위치” 스택이 찍히면, 그 경로에서 다음을 의심합니다.
@Transactional범위가 과도하게 큼(외부 API 호출/파일 IO 포함)- 스트리밍 응답/대용량 처리 중 ResultSet을 오래 들고 있음
- 예외 경로에서 close가 누락(직접 JDBC 사용 시)
3-2) 직접 JDBC를 쓴다면 try-with-resources로 강제
try (Connection con = dataSource.getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
// ...
}
} // 여기서 무조건 반환
JPA/Hibernate를 써도, 아래 패턴은 커넥션을 오래 잡는 트리거가 됩니다.
- 트랜잭션 안에서 외부 HTTP 호출
- 트랜잭션 안에서 대량 루프 + flush/clear 없이 누적
3-3) 트랜잭션 범위 축소 예시
@Service
public class OrderService {
private final OrderRepository repo;
private final PaymentClient paymentClient;
public OrderService(OrderRepository repo, PaymentClient paymentClient) {
this.repo = repo;
this.paymentClient = paymentClient;
}
// 1) DB 작업만 트랜잭션
@Transactional
public Order createOrder(CreateOrderCommand cmd) {
Order order = repo.save(new Order(cmd.userId(), cmd.items()));
return order;
}
// 2) 외부 호출은 트랜잭션 밖에서
public void pay(Long orderId) {
Order order = repo.findById(orderId).orElseThrow();
paymentClient.requestPayment(orderId, order.getAmount());
}
}
4) 풀 크기만 늘리면 왜 더 망가질 수 있나(중요)
장애 때 가장 흔한 처방이 maximumPoolSize를 올리는 겁니다. 하지만 다음 조건이면 악화됩니다.
- DB가 이미 CPU/IO 한계 → 커넥션 늘리면 동시 쿼리만 늘어 락/스왑/IO wait 증가
- DB
max_connections가 낮음 → 커넥션 생성/인증 실패가 늘어 타임아웃 가속 - 애플리케이션 스레드가 더 공격적(요청 스레드 수가 훨씬 많음) → 대기열만 커져 응답 지연 길어짐
풀 크기 조정은 “원인 제거”가 아니라 “완충”일 때만 유효합니다.
5) DB/네트워크 타임아웃과 Hikari maxLifetime 정렬(2분)
풀 고갈처럼 보이지만 실제로는 중간 장비가 커넥션을 조용히 끊어 커넥션 재사용 시점에 지연/예외가 발생하는 케이스가 있습니다.
정렬 원칙
maxLifetime은 DB/프록시/NLB의 idle timeout보다 30~60초 짧게connectionTimeout은 요청 SLA보다 짧게(예: API 타임아웃 3초면 커넥션 획득을 30초로 두면 늦게 터짐)
예시(상황에 맞게 조정):
spring:
datasource:
hikari:
connection-timeout: 3000
max-lifetime: 840000 # 14분 (예: 중간 idle timeout 15분이면 더 짧게)
validation-timeout: 1000
인프라 레벨에서 타임아웃/버퍼 이슈가 장애를 증폭시키는 경우도 많습니다. 특히 Ingress/Nginx 타임아웃/버퍼가 요청 재시도를 유발하면 DB 부하가 2차로 증가합니다.
6) 10분 트리아지 체크리스트(현장용)
아래 순서대로 보면 “어디를 파야 하는지”가 빠르게 결정됩니다.
1) Hikari 메트릭
active == max?pending급증?timeout증가?
→ 예: active가 max에 붙고 pending이 증가하면 풀 고갈 확정.
2) DB 커넥션 수/대기 이벤트
- DB에서 현재 커넥션 수가 상한에 근접?
- 대기 이벤트(락, IO, CPU) 급증?
→ DB가 병목이면 풀을 늘리기보다 쿼리/인덱스/락부터.
3) 슬로우 쿼리/락
- 슬로우 쿼리 로그,
pg_stat_activity,information_schema등으로 상위 쿼리 확인 - 장기 트랜잭션/락 홀더 확인
4) 애플리케이션 스레드 덤프
- 요청 스레드가
getConnection()에서 대기? - 특정 서비스 메서드에서 외부 호출/대기?
5) 누수 탐지(임시)
leakDetectionThreshold=5000로 5~10분만 켜서 스택 확보
7) 재발 방지: “풀”이 아니라 “부하 형태”를 제어
풀 고갈은 결국 동시성 제어 실패인 경우가 많습니다.
7-1) 엔드포인트별 동시성 제한(간단한 세마포어)
DB를 강하게 치는 API가 있다면, 애플리케이션에서 먼저 제한을 걸어 “대기열 폭발”을 막을 수 있습니다.
@Component
public class DbHeavyEndpointLimiter {
private final Semaphore semaphore = new Semaphore(30); // 상황에 맞게
public <T> T run(Callable<T> action) throws Exception {
if (!semaphore.tryAcquire(50, TimeUnit.MILLISECONDS)) {
throw new IllegalStateException("Too many concurrent requests");
}
try {
return action.call();
} finally {
semaphore.release();
}
}
}
7-2) 타임아웃을 “짧게” + 재시도는 “신중하게”
- 커넥션 획득 타임아웃이 길면 장애 감지가 늦고 큐가 쌓입니다.
- 무분별한 재시도는 DB를 더 때립니다(특히 5xx 재시도).
마무리
HikariCP 풀 고갈은 증상이 명확하지만, 원인은 다양합니다. 10분 안에 결론을 내려면 순서가 중요합니다.
- 메트릭으로 풀 고갈을 확정하고
- 누수/장기 트랜잭션/슬로우 쿼리/DB 커넥션 상한 중 어디가 병목인지 갈라낸 뒤
- 설정은 maxLifetime/timeout 정렬로 안정성을 올리고
- 재발 방지는 동시성/트랜잭션 범위/재시도 정책에서 잡아야 합니다.
특히 DB 커넥션 상한이 장애의 뇌관이 되는 경우가 많으니, RDS를 쓴다면 아래 글의 점검 항목(최대 커넥션, 풀/서비스별 분배, pgbouncer 등)도 함께 확인해 두면 좋습니다.