새벽 0시 13분, 커넥션 풀이 죽었다

슬랙 알림이 울렸을 때 나는 넷플릭스를 보고 있었다. 온콜 당번 첫째 날. PagerDuty가 아니라 슬랙이라 0.3초 정도 "무시해도 되나" 싶었다. 그게 실수였다.

00:13 — "payment-api latency spike"

Grafana 대시보드를 열었다. p99 레이턴시가 평소 120ms에서 8초로 치솟아 있었다. 결제 API다. 새벽이라 트래픽이 적을 텐데? 로그를 까봤다.

HikariPool-1 - Connection is not available, request timed out after 30000ms

HikariCP 커넥션 풀이 고갈됐다. 30초 타임아웃에 걸린 요청들이 줄줄이 실패하고 있었다.

00:18 — "이거 왜 풀 사이즈가 10이지?"

kubectl exec으로 파드에 들어가 환경변수를 확인했다. HIKARI_MAX_POOL_SIZE=10. 뭔가 이상했다. 분명 100으로 운영하고 있었는데. git blame을 때렸다.

3일 전 머지된 PR이 나왔다. "불필요한 리소스 정리"라는 커밋 메시지. 스테이징 환경의 설정값을 프로덕션 values.yaml에 그대로 복붙한 거였다. 리뷰어 두 명 다 approve. 누구도 숫자 하나 바뀐 걸 못 잡았다.

00:27 — 14분 경과, $47K 증발

결제 실패 건수가 쌓이고 있었다. 새벽이라 트래픽이 적다는 게 오히려 발견을 늦췄다. 낮이었으면 5분 만에 알람이 터졌을 거다. 이 14분 동안 실패한 트랜잭션 약 3,200건. 추산 매출 손실 $47,000.

핫픽스는 허무할 정도로 간단했다.

# values.yaml
hikari:
  maximum-pool-size: 100  # was: 10

한 줄 고치고 argocd app sync 때리니까 3분 만에 정상화.

5 Whys

사후에 팀 전체가 모여서 5 Whys를 돌렸다.

  1. 왜 장애가 났나? → 커넥션 풀이 고갈됐다

  2. 왜 고갈됐나? → 풀 사이즈가 10으로 줄어 있었다

  3. 왜 줄었나? → 스테이징 값이 프로덕션에 머지됐다

  4. 왜 머지됐나? → 코드 리뷰에서 설정값 변경을 놓쳤다

  5. 왜 놓쳤나? → values.yaml 변경에 대한 자동 검증이 없었다

진짜 원인은 코드가 아니라 프로세스였다. 200줄짜리 PR에서 숫자 하나 바뀐 걸 사람 눈으로 잡으라는 건 무리다.

그래서 뭘 바꿨나

장애 다음 주에 세 가지를 도입했다.

첫째, values.yaml에 프로덕션 크리티컬 값(풀 사이즈, 레플리카 수, 메모리 리밋)이 변경되면 CI에서 diff를 별도 코멘트로 달아주는 GitHub Action을 만들었다. 숫자가 50% 이상 줄어들면 자동으로 블로킹 리뷰를 건다.

둘째, HikariCP 메트릭(hikaricp_connections_active, hikaricp_connections_pending)에 대한 Grafana 알림을 추가했다. active가 max의 80%를 넘으면 warning, pending이 0보다 크면 critical.

셋째, 스테이징과 프로덕션의 values.yaml을 분리했다. 같은 파일에서 환경별 오버라이드를 관리하던 구조가 근본 원인이었다. 지금은 values-staging.yamlvalues-production.yaml로 완전히 나눠져 있다.


한 줄 요약: 설정값 변경은 코드 변경보다 위험하다. 코드는 테스트가 잡아주지만, 100 → 10 같은 설정 변경을 잡아줄 테스트는 직접 만들지 않으면 없다.