핵심 한 줄

지연의 범인이 ‘DB가 느림’이 아니라 풀 고갈일 때가 있다. 가르는 신호는 hikaricp_connections_timeout_total↑ AND hikaricp_connections_active ≈ max — DB는 멀쩡한데 풀에서 커넥션을 못 빌리는 것이다. 그리고 그 원인은 풀이 작아서가 아니라 안 돌려준 것이다.

시리즈 — 장애를 '구별 가능'하게 만든 카오스 벤치


1. 배경 — 지연의 수많은 얼굴

운영에서 p99 latency↑ 알림이 뜨면, 원인 후보가 십수 가지다. 락 경합·GC 폭주·N+1·커넥션풀 고갈·CPU 스로틀… 전부 같은 증상으로 도착한다. 이 시리즈의 목표는 각 원인을 다른 원인은 못 내는 단 하나의 신호로 가르는 것이다(0.서문).

W6의 주인공은 HikariCP 풀 고갈(connection pool exhaustion) 이다. 이 장애에는 특별히 교묘한 형제가 있다 — DB 자체 지연. 둘은 증상이 너무 닮아서, 신호를 제대로 설계하지 않으면 DB팀 인시던트로 오보된다. 그런데 DB는 멀쩡하다.


2. 무엇을 망가뜨렸나 — owner 검색이 커넥션을 안 닫는다

chaos 프로파일에서 connectionPoolExhaustion을 arm하면, owner 검색(GET /owners)이 매 요청마다 HikariCP 풀에서 커넥션을 빌리고 절대 돌려주지 않는다. ActiveChaosFaults.leakConnectionIfArmed()가 그 seam이다.

// OwnerController.java (chaos seam — profile-gated)
this.chaosFaults.leakConnectionIfArmed();

seam은 chaos 프로파일에서만 살아난다. NoOpChaosFaults(기본 빌드)는 이 메서드가 no-op이라 운영 코드에는 이 행위가 없는 것과 같다. 부하를 주면 누수가 누적되어 풀이 말라붙는다 — 이후 검색의 쿼리는 커넥션을 기다리다 connection-timeout(2s) 후 5xx로 실패한다. smoke는 chaos-pool 격리 프로파일(풀 크기=5, 이름=petclinic-pool)을 써서 5개 커넥션이 누수되는 순간 풀이 완전히 고갈되도록 결정론적으로 만들었다.


3. 증상 — DB가 느린 것처럼 보인다

풀이 고갈되면 요청이 hikaricp_connections_timeout_total을 올리며 5xx로 떨어진다. 첫 직관은 자연스럽게 “DB가 느리거나 죽었나?” 로 향한다. 둘 다 “DB 쿼리가 안 된다”처럼 보이기 때문이다.

하지만 결정적 차이가 있다 — DB가 느린 거라면 DB 레이턴시 지표가 올라야 한다. 그런데 이 경우 DB 자체는 멀쩡하게 응답한다. 풀에서 커넥션을 꺼내지도 못한 채 타임아웃이 나는 것이다. 요청이 DB에 닿기 전에 이미 실패한다.

문제는, 증상(5xx + 지연)만으로는 둘을 못 가른다는 것이다. DB가 느릴 때도, 풀이 고갈될 때도 똑같이 요청이 늦어지고 실패한다.


4. 판별 신호 — timeout 카운터 + active ≈ max

그래서 두 게이지를 함께 본다. HikariCP가 Micrometer를 통해 자동으로 노출하는 메트릭들이다 — 새로운 계측 코드가 필요 없다.

신호DB 자체 지연풀 고갈(미반환)
hikaricp_connections_timeout_total낮음 또는 0↑ 지속 상승
hikaricp_connections_active정상 순환≈ max (가용 0)
DB 레이턴시 지표높음정상 (DB는 건강)

알림(ConnectionPoolExhausted)의 발화 조건은 increase(hikaricp_connections_timeout_total{service="petclinic"}[5m]) > 0이다. 하지만 진단은 거기서 멈추지 않는다 — hikaricp_connections_active ≈ hikaricp_connections_max를 확인하고, DB 레이턴시가 정상임을 보고, “DB 아님 → 풀 고갈”로 확정한다.


5. 함정 — “풀 크기를 늘리면 되지 않나?”

풀 고갈을 처음 보면 직관적 처방이 따라온다 — “max-pool-size를 늘리면 되지 않나?” 하지만 이 경우 풀 크기 문제가 아니다. 커넥션을 안 돌려주는 누수가 원인이다. 풀을 10개로 늘려도 10개 누수되면 같은 상황이 된다.

두 번째 함정은 오해한 라우팅이다. 5xx + DB 타임아웃처럼 보이기 때문에 DB팀 인시던트로 에스컬레이션된다. hikaricp_connections_active 핀 패턴을 먼저 확인했다면 “DB 인프라 아님 → 앱 코드에서 close() 누락”으로 즉시 좁혀진다.

처방이 틀리면 재발한다

hikaricp_connections_active 가 max에 핀되어 있고 timeout 카운터가 오르는 패턴은 “풀 부족”이 아니라 “누수” 신호다. 풀 크기 조정은 증상 완화일 뿐이다 — 커넥션을 빌린 뒤 close()를 빠뜨린 코드 경로를 찾아야 재발이 없다.


6. 교훈

모자란 게 아니라 안 돌려준 것

풀 고갈의 판별 신호는 크기가 아니라 조합이다 — hikaricp_connections_timeout_total↑ AND hikaricp_connections_active ≈ max AND DB 레이턴시 정상. 이 세 줄이 동시에 성립하면 원인은 풀 부족이 아니라 누수다. 그리고 “누수”는 인프라 문제가 아니라 앱 코드 문제다 — DB팀이 아니라 개발팀이 close()를 찾아야 한다.


관련 글