본문 바로가기

Study

TIL - HikariCP 커넥션 풀과 Cold Start

728x90
반응형

배경

부하 테스트 중 Zipkin 트레이스에서 connection 스팬이 220ms 찍히는 현상을 발견. HikariCP 동작 방식과 cold start 원인을 파악함.


HikariCP란

Spring Boot 기본 JDBC 커넥션 풀. 애플리케이션 ↔ DB 사이의 TCP 연결을 미리 만들어두고 재사용해서 매 요청마다 연결 비용을 없애는 역할.


Cold Start가 발생하는 이유

HikariCP는 기본적으로 lazy 초기화를 사용함. 앱이 뜰 때 커넥션을 미리 만들지 않고, 첫 요청이 들어올 때 비로소 물리 TCP 연결을 생성함.

앱 시작 → 풀 생성 (빈 상태)
첫 요청 → 그때서야 PostgreSQL TCP 연결 생성 → ~220ms
이후 요청 → 이미 만들어진 커넥션 재사용 → ~0ms

lazy가 기본값인 이유: 앱이 뜨자마자 트래픽이 없을 수도 있어서 DB 자원을 미리 점유하지 않기 위함. 서비스가 수십 개면 idle 커넥션 수백 개가 DB를 점유하는 낭비가 생길 수 있음.

Eager 초기화로 변경하려면

spring:
  datasource:
    hikari:
      initialization-fail-timeout: 30000  # 시작 시 커넥션 생성, 최대 30초 대기

앱 시작 시간이 늘어나는 대신 첫 요청부터 0ms.


커넥션 생명주기

핵심: minimumIdle 덕분에 idleTimeout으로 커넥션이 제거돼도 백그라운드에서 즉시 새 커넥션을 만들어 풀을 채움. 다음 요청이 올 때는 이미 준비된 커넥션이 있어서 cold start 없음.


커넥션은 유저별이 아님

흔히 오해하는 부분. HikariCP 커넥션은 서비스 ↔ DB 사이의 연결이지, 특정 유저에 귀속되는 게 아님.

유저 A 요청 → 풀에서 커넥션 꺼냄 → 쿼리 → 커넥션 반납
유저 B 요청 → 같은 커넥션 재사용 → 쿼리 → 커넥션 반납

유저가 달라도 이미 만들어진 커넥션을 공유해서 씀. cold start는 유저 기준이 아니라 앱 재시작 기준.


동시 요청과 풀 사이즈

동시 요청이 들어오면 각각 커넥션 1개씩 사용.

VU 1 → 커넥션 1 사용 중
VU 2 → 커넥션 2 사용 중
VU 3 → 커넥션 3 사용 중

maxPoolSize(기본 10)를 초과하는 동시 요청이 오면 커넥션이 반납될 때까지 대기함 → 트레이스에 connection 대기 시간 증가로 나타남.


커넥션 풀이 존재하는 근본 이유

커넥션 생성 비용 > 유지 비용 이기 때문.

생성 비용: TCP 핸드셰이크 + PostgreSQL 인증 → ~220ms (크다)
유지 비용: TCP 소켓 1개 + 메모리 약간        → 거의 없다

한 번 만들고 계속 재사용하는 게 이득. 이게 커넥션 풀이 존재하는 이유 자체.


minimumIdle vs maximumPoolSize 설정

maximumPoolSize = 10  (최대 10개까지 만들 수 있음)
minimumIdle     = 2   (평소엔 2개만 유지)
  • 트래픽 없을 때 → idle 커넥션 8개 제거 → 2개만 유지 (자원 절약)
  • 트래픽 몰릴 때 → 최대 10개까지 늘림

Spring Boot 기본값: minimumIdle = maximumPoolSize = 10 → 항상 10개 유지 → 자원 낭비지만 항상 빠름.

minimumIdle 낮게 설정 시 cold start 재발 가능

minimumIdle = 2, maximumPoolSize = 10

평소: 커넥션 2개 유지
갑자기 요청 5개 동시에 들어옴
  → 2개는 즉시 처리
  → 나머지 3개는 커넥션 새로 생성 필요 → cold start 재발

 

cold start를 완전히 없애려면 minimumIdle = maximumPoolSize 가 답.


idleTimeout 이후 cold start가 없는 이유 (타이밍 차이)

Cold start (앱 재시작): 요청이 먼저 오고, 그때서야 커넥션 생성 → 요청이 기다림

풀 비어있음 → 요청 도착 → 커넥션 생성 (~220ms) → 처리

idleTimeout 이후: 커넥션 제거되자마자 백그라운드에서 미리 교체 → 요청이 올 때 이미 준비됨

커넥션 제거 → 백그라운드에서 즉시 새 커넥션 생성 → 요청 도착 → 0ms

단, 이는 minimumIdle이 설정된 경우에만 해당. minimumIdle = 0이면 교체 안 함.


커넥션 유지의 트레이드오프

  • 유지 비용: TCP 소켓 점유, PostgreSQL은 커넥션 1개당 프로세스 1개 생성 (~5~10MB)
  • 매번 연결 비용: TCP 핸드셰이크 + PostgreSQL 인증 = 수십~수백ms/요청

HikariCP는 maxPoolSize로 커넥션 수를 제한해 DB 자원 낭비를 막으면서, 재사용으로 연결 비용을 제거하는 균형점.


실제 경험 (부하 테스트)

  • smoke 테스트 (5 VU) 첫 실행 → 응답 avg 896ms
  • Zipkin 분석: coupon-service: connection 220ms, user-service 호출로 gateway 550ms 지연
  • 원인: coupon-service + user-service 둘 다 cold HikariCP
  • load 테스트부터는 JVM 워밍업 + 풀 초기화 완료 → 응답 대폭 감소 예상
반응형