- Published on
Spring Boot HikariCP 커넥션 고갈 3분 진단
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버가 갑자기 느려지고 API가 타임아웃으로 쓰러질 때, 로그에 아래 문장이 보이면 거의 확정입니다.
HikariPool-1 - Connection is not available, request timed out after 30000ms.
이 글은 “지금 장애 중” 상황에서 3분 안에 HikariCP 커넥션 고갈의 원인을 좁히는 진단 루틴을 제공합니다. 핵심은 (1) 풀이 진짜로 고갈인지, (2) DB가 느린 건지, (3) 앱이 커넥션을 오래/영원히 쥐고 있는지(누수/락/긴 트랜잭션)를 빠르게 구분하는 것입니다.
0) 전제: 커넥션 고갈은 ‘원인’이 아니라 ‘증상’
HikariCP 풀이 고갈되는 대표 원인은 대개 아래 4가지 중 하나로 수렴합니다.
- DB 쿼리가 느려져서 커넥션 점유 시간이 길어짐 (인덱스/플랜/IO/네트워크)
- 트랜잭션이 너무 길거나 외부 호출을 트랜잭션 안에서 수행 (락 유지 + 점유)
- 커넥션 누수 (반납 안 함, 비동기/스트리밍/예외 경로)
- 풀/스레드/DB max_connections 설정 불일치 (풀은 크지만 DB가 못 받음, 또는 톰캣 스레드가 과도)
3분 진단은 이 네 가지 중 어디에 가까운지 빠르게 레이블링하는 과정입니다.
1) 3분 진단 체크리스트(장애 대응용)
아래 순서대로 보면 됩니다. “예/아니오”로 가지치기하세요.
1-1. 30초: HikariCP 메트릭으로 ‘진짜 고갈’ 확인
Actuator + Micrometer가 있다면 가장 빠릅니다.
hikaricp.connections.active가maximumPoolSize에 붙어 있나?hikaricp.connections.pending가 증가하나?hikaricp.connections.idle이 0에 수렴하나?
예시(프로메테우스):
hikaricp_connections_active{pool="HikariPool-1"} 30
hikaricp_connections_pending{pool="HikariPool-1"} 120
hikaricp_connections_idle{pool="HikariPool-1"} 0
이 패턴이면 “풀 고갈”은 사실이고, 다음은 왜 active가 줄지 않는지를 봐야 합니다.
> 운영이 EKS라면, 장애 시점에 readiness가 멀쩡해 보이는데 실제 트래픽은 죽는 케이스가 있습니다. 인프라 관점 점검은 EKS에서 Readiness 실패인데 로그는 정상일 때도 함께 참고하면 좋습니다.
1-2. 1분: 스레드 덤프로 “커넥션 대기” vs “DB에서 대기” 구분
애플리케이션이 커넥션을 못 얻는 상황이면 스레드가 HikariPool.getConnection() 근처에서 대기합니다.
# 컨테이너/서버에서
jcmd <pid> Thread.print > /tmp/threads.txt
# 또는
jstack <pid> > /tmp/threads.txt
threads.txt에서 아래 키워드를 찾습니다.
- 커넥션 대기:
com.zaxxer.hikari.pool.HikariPool.getConnection - DB I/O 대기: JDBC 드라이버의 socket read/write,
org.postgresql.core.v3.QueryExecutorImpl.execute,com.mysql.cj.jdbc등 - 락/트랜잭션 대기:
SELECT ... FOR UPDATE,Lock wait timeout,deadlock관련
판정
- 스레드 대부분이
HikariPool.getConnection에서 BLOCKED/TIMED_WAITING → 풀 고갈이 1차 현상 - 스레드가 JDBC execute/소켓 read에서 오래 머무름 → DB 응답 지연
- 특정 쿼리/락에서 대기 → 락 경합/긴 트랜잭션
1-3. 1분: Hikari leak detection + slow query 로그로 “누수/장기 점유” 확인
장애 중이거나 재현 환경이라면 다음 두 설정이 가장 빠르게 단서를 줍니다.
spring:
datasource:
hikari:
maximum-pool-size: 30
connection-timeout: 30000
leak-detection-threshold: 20000 # 20초 이상 점유 시 스택트레이스 로그
validation-timeout: 5000
max-lifetime: 1800000
idle-timeout: 600000
leakDetectionThreshold가 트리거되면 로그에 **커넥션을 잡은 코드 경로(스택트레이스)**가 찍힙니다. 이게 나오면 3분 진단은 사실상 끝입니다. “어디서 오래 쥐고 있나”가 바로 보이기 때문입니다.
추가로 DB slow query log(또는 APM 쿼리 트레이스)를 켜서, 장애 시점에 상위 N개 쿼리의 latency를 확인하세요.
2) 원인별 ‘즉시 완화’와 ‘근본 해결’
3분 진단으로 원인 범주가 잡히면, 다음은 **즉시 완화(mitigation)**와 **근본 해결(fix)**을 분리해서 접근해야 합니다.
2-1. DB가 느려진 케이스(쿼리/플랜/IO)
징후
- active는 높고, 스레드는 JDBC execute/소켓 read에 오래 머뭄
- DB CPU/IO가 치솟음, slow query가 폭증
즉시 완화
- 트래픽을 줄이기(레이트 리밋/서킷브레이커)
- 문제 쿼리/배치 중지
- 읽기 부하라면 캐시(예: Redis) 임시 활성화
근본 해결
- 인덱스/쿼리 플랜 개선, N+1 제거
- 커넥션당 수행 시간이 긴 API의 페이지네이션/배치 분할
PostgreSQL에서 락/데드락까지 의심되면, DB 레벨에서 대기/교착을 함께 보세요. 관련해서는 PostgreSQL RDS deadlock_detected(40P01) 원인·해결이 직접적인 힌트를 줍니다.
2-2. 긴 트랜잭션(특히 외부 호출을 트랜잭션 안에서)
징후
@Transactional메서드 안에서 HTTP 호출/파일 업로드/메시지 발행 등을 수행- DB 락 대기 증가, 커넥션 점유 시간이 길어짐
즉시 완화
- 트랜잭션 범위를 줄여 빠르게 커밋
- 격리수준/락이 강한 쿼리(예:
FOR UPDATE) 사용 구간 축소
근본 해결 패턴
- “DB 트랜잭션”과 “외부 I/O”를 분리
- Outbox 패턴/이벤트 발행을 트랜잭션 밖으로
예시: 트랜잭션 안에서 외부 호출을 하지 않도록 구조를 바꿉니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
@Transactional
public Long createOrder(OrderCommand cmd) {
Order order = orderRepository.save(Order.create(cmd));
// 트랜잭션에서는 DB 작업만
return order.getId();
}
// 별도 단계에서 외부 호출(비동기/이벤트 기반 권장)
public void requestPayment(Long orderId) {
paymentClient.request(orderId);
}
}
2-3. 커넥션 누수(반납 누락)
징후
- leak detection 로그가 뜸
- active가 서서히 증가하고 절대 내려오지 않음(“점진적 고갈”)
자주 터지는 패턴
JdbcTemplate/Spring Data JPA는 보통 안전하지만, 직접 JDBC를 다룰 때 try-with-resources 누락- 스트리밍 응답/대용량 결과를 커넥션 붙잡은 채로 오래 전송
- 예외 경로에서 close 누락
올바른 JDBC 자원 해제 예시:
public List<User> findUsers(DataSource ds) throws SQLException {
String sql = "select id, name from users";
try (Connection con = ds.getConnection();
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery()) {
List<User> out = new ArrayList<>();
while (rs.next()) {
out.add(new User(rs.getLong("id"), rs.getString("name")));
}
return out;
}
}
근본 해결
- 누수 지점을 수정하고, 재발 방지로
leakDetectionThreshold를 “너무 낮지 않게” 운영에 유지(예: 20~60초) - 스트리밍/대용량 다운로드는 DB 커서 스트리밍 대신 사전 적재/비동기 생성/오브젝트 스토리지로 전환 고려
2-4. 풀/스레드/DB max_connections 불일치
징후
- 애플리케이션 인스턴스 수가 늘어날수록 더 빨리 고갈
- DB에서
too many connections또는 커넥션 생성 지연
핵심 공식(대략)
총 DB 커넥션 = (pod 수) × (maximumPoolSize)- 이 값이 DB
max_connections(또는 RDS 한계)에 근접/초과하면, “언젠가” 장애가 납니다.
권장 접근
- 풀을 무작정 키우지 말고, 요청 처리 스레드 수(톰캣)와 함께 설계
- DB가 버틸 수 있는 총 커넥션 예산을 정하고 pod당 pool size를 역산
Spring Boot에서 톰캣 스레드도 함께 보세요.
server:
tomcat:
threads:
max: 200
min-spare: 20
spring:
datasource:
hikari:
maximum-pool-size: 30
톰캣 max=200, 풀=30이면 동시 요청 200개가 DB 커넥션 30개를 두고 경쟁합니다. 이 자체는 괜찮을 수도 있지만(대부분의 요청이 DB를 안 쓰면), DB 의존도가 높은 API라면 pending이 쉽게 폭증합니다. 이때는 풀을 키우기보다 쿼리 시간 단축/캐시/읽기 분리가 먼저입니다.
3) “3분 안에” 가장 잘 먹히는 관측 포인트 5개
운영에서 매번 재현이 안 되는 경우가 많아, 다음 5개만 갖춰도 진단 속도가 급상승합니다.
- HikariCP 메트릭: active/idle/pending/max
- 커넥션 획득 시간(가능하면 히스토그램): getConnection latency
- DB 쿼리 latency 상위 N개(APM 또는 slow query)
- 스레드 덤프 자동 수집(타임아웃 급증 시)
- leak detection(상시 또는 장애 시 임시)
Kubernetes에서 타임아웃/게이트웨이 에러가 같이 보이면(예: 502/504), 애플리케이션 내부 고갈이 인그레스/프록시 타임아웃으로 증폭되는 경우가 많습니다. 네트워크/프록시 측면의 타임아웃 정합성도 함께 보려면 Kubernetes LLM 서비스 502 504 간헐 장애와 스트리밍 끊김을 끝내는 NGINX Ingress와 Gunicorn Uvicorn 실전 튜닝의 “타임아웃 레이어 정렬” 관점이 도움이 됩니다(서비스 종류는 달라도 원리는 동일).
4) 실전: 장애 로그 한 줄로 시작하는 최소 진단 플로우
마지막으로, 현장에서 바로 복기 가능한 최소 플로우를 정리합니다.
- 로그에서
Connection is not available확인 - 메트릭으로
active=max,pending>0,idle=0확인 - 스레드 덤프 1회 채집
HikariPool.getConnection대기 다수 → 커넥션 부족(점유 과다)- JDBC execute/소켓 read 다수 → DB 지연
- leak detection(가능 시)로 장기 점유 스택 확보
- DB 쿼리 상위 N개/락 대기 확인
이 루틴으로 “풀을 키워야 하나요?” 같은 질문을 3분 내에 반박/확증할 수 있습니다. 대부분의 경우 답은 “풀을 키우기 전에, 커넥션을 오래 잡는 원인을 제거하자”입니다.
5) 체크리스트(복사해서 장애 대응 문서에 붙이기)
[HikariCP 고갈 3분 진단]
1) hikaricp active==max? pending 증가? idle==0?
2) thread dump: getConnection 대기 vs JDBC execute 대기 구분
3) leakDetectionThreshold로 장기 점유 스택 확보
4) slow query / 락 대기 / deadlock 여부 확인
5) pod 수 x maxPoolSize <= DB max_connections 예산 확인
[즉시 완화]
- 트래픽 감소(레이트리밋/서킷브레이커)
- 문제 배치/엔드포인트 차단
- 타임아웃 정합성(클라이언트/서버/프록시) 확인
[근본 해결]
- 쿼리/인덱스/N+1 개선
- 트랜잭션 범위 축소(외부 I/O 분리)
- 누수 제거(try-with-resources, 스트리밍 구조 점검)
- 풀/스레드/DB 커넥션 예산 재설계
이 체크리스트를 갖추면, HikariCP 커넥션 고갈은 “원인 모를 장애”가 아니라 관측 가능한 병목으로 바뀝니다.