DB 커넥션 풀에 여유 커넥션이 없어 요청이 대기하다 타임아웃하는 상황. 면접 단골 시나리오이자 실제로 매우 자주 발생하는 장애다.
증상
HikariPool-1 - Connection is not available, request timed out after 30000ms
- 특정 API만이 아니라 DB를 쓰는 전부가 느려진다
- CPU·메모리는 멀쩡한데 응답이 안 온다
- 스레드 덤프에
getConnection()에서 대기 중인 스레드가 잔뜩
왜 커넥션은 풀로 관리하나
커넥션 하나를 새로 만드는 비용 = TCP 3-way handshake + 인증 + 세션 설정 ≈ 수십 ms
- 요청마다 만들면 그 자체가 병목이다
- 미리 만들어 두고 빌려 쓴다
원인 — 빈도순
① 커넥션을 오래 쥐고 있다 (가장 흔함)
@Transactional
public void order() {
repo.save(order);
externalApi.call(); // ← 3초. 그동안 커넥션을 쥐고 있다
repo.save(log);
}
트랜잭션이 시작되면 커넥션이 할당되고 커밋까지 반납되지 않는다. 그 안에 외부 API 호출이 있으면 커넥션을 3초씩 낭비한다.
→ 외부 호출을 트랜잭션 밖으로 빼는 것이 정석이다.
② 커넥션 누수
Connection conn = dataSource.getConnection();
// ... 예외 발생
conn.close(); // 실행되지 않는다 → 영영 반납 안 됨
try-with-resources를 쓰면 방지된다. 스프링을 쓰면 대부분 자동이지만,
직접 JDBC를 다루는 코드에서 발생한다.
③ 슬로우 쿼리 쿼리 하나가 10초 걸리면 그 커넥션이 10초간 묶인다.
④ 풀 크기가 실제 부하보다 작다
⑤ REQUIRES_NEW 중첩
- 바깥 트랜잭션 커넥션 + 안쪽 새 트랜잭션 커넥션 = 요청당 2개
- 풀 크기 10에서 동시 10개 요청이 오면 전부 두 번째를 기다리며 데드락
⑥ 비동기 처리에서 커넥션을 넘김
@Async 스레드로 엔티티를 넘기면 지연 로딩이 새 커넥션을 요구한다.
진단
// HikariCP 지표 (Actuator/Micrometer)
hikaricp.connections.active // 사용 중
hikaricp.connections.pending // 대기 중 ← 0이 아니면 경보
hikaricp.connections.usage // 사용 시간 분포
jstack <pid> | grep -A 10 "getConnection" # 어디서 기다리는가
SHOW PROCESSLIST; -- DB 쪽에서 실제로 무엇이 실행 중인지
pending이 지속적으로 0보다 크면 풀이 부족하거나 반납이 늦는 것이다.
풀 크기 — 크게 잡는 것이 답이 아니다
connections = ((core_count × 2) + effective_spindle_count) HikariCP 문서의 권장식이다. 코어 8개면 약 17개가 나온다.
왜 작게 잡나 — DB도 커넥션마다 메모리와 프로세스/스레드를 쓴다. 커넥션이 많으면 DB 쪽에서 컨텍스트 스위칭과 락 경합이 늘어 오히려 전체 처리량이 떨어진다.
- 풀을 100으로 늘렸더니 오히려 느려졌다 — ← 실제로 흔한 일
애플리케이션 인스턴스가 여러 대면 합계가 DB의 max_connections를
넘지 않는지도 봐야 한다.
설정
spring.datasource.hikari:
maximum-pool-size: 20
minimum-idle: 20 # max와 같게 (풀 크기 변동을 없앰)
connection-timeout: 3000 # 대기 3초 후 실패 — 빨리 실패시킨다
validation-timeout: 1000
max-lifetime: 1800000 # DB의 wait_timeout 보다 짧게 ★
leak-detection-threshold: 60000 # 60초 이상 안 돌아오면 로그
★ max-lifetime이 DB의 wait_timeout보다 길면 DB가 먼저 끊은
죽은 커넥션을 풀이 계속 들고 있다가 사용 시점에 실패한다.
"간헐적으로 커넥션 오류가 난다"의 흔한 원인이다.
근본 대책
- ① 트랜잭션을 짧게. 외부 호출을 트랜잭션 밖으로
- ② 슬로우 쿼리 제거
- ③ 읽기 전용은 복제본으로 분리 (풀도 분리 = 벌크헤드)
- ④ connection-timeout 을 짧게 → 전체가 멈추기 전에 빨리 실패
- ⑤ 지표에 알림을 건다 (pending > 0)