donghakim.dev — zsh
← ls ../blog

오픈하지 않은 기능이 트래픽의 97%가 되어 결제를 막았다

커넥션 풀 고갈의 범인이 풀 크기가 아닐 때가 있습니다. writer 는 1.8ms 로 멀쩡했고 reader CPU 가 99%였습니다

infra · cover image

프로모션 트래픽이 몰린 날 오후, "결제는 됐는데 예약이 안 된다" 는 고객 문의가 들어오기 시작했습니다. 결제 서비스 로그에는 이게 4시간 동안 약 9,400건 쌓여 있었습니다.

CannotCreateTransactionException:
  ... Connection is not available, request timed out after 30000ms

HikariCP 풀 상태는 total=5, active=5, idle=0, waiting=95~197 이 45분 넘게 지속. 전형적인 커넥션 풀 고갈입니다. 여기서 반사적으로 나오는 대응이 "풀 크기를 늘리자" 인데, 그 전에 지표를 몇 개 더 봤습니다.

풀 고갈 ≠ DB 병목

지표 (장애 구간)해석
Aurora writer WriteLatency1.8ms 평탄쓰기는 안 느림
writer DatabaseConnections45→54커넥션 여유
writer Deadlocks0락 경합 없음
Aurora reader CPU39% → 99% 고정진짜 병목

DB 쓰기는 멀쩡했습니다. 문제는 reader 였고, 풀이 마른 건 결과이지 원인이 아니었습니다. 이 상태에서 풀 크기만 키웠다면 포화된 reader 에 동시 쿼리를 더 밀어 넣어 오히려 악화됐을 겁니다.

트래픽의 97%가 오픈 안 한 기능이었습니다

reader 를 태우는 게 뭔지 요청 로그를 집계해 보니, 48분 동안 결제 서비스 호출 11,315건 중 97% 가 포인트 조회였습니다. 포인트 기능은 아직 오픈 전인데도요.

원인은 프론트였습니다. 네비게이션 헤더가 잔액 표시용으로 포인트 조회 API 를 부르고 있었고, 헤더는 모든 페이지에 있으니 페이지를 열 때마다 호출됐습니다. 기능이 안 열렸어도 API 는 열려 있었던 겁니다. "미오픈 기능이니 트래픽이 없겠지" 는 코드가 아니라 요청 로그로 확인해야 하는 가정이었습니다.

그리고 이 API 의 내역 쿼리가 무거웠습니다. 행마다 누적 합계를 다시 계산하는 상관 서브쿼리(O(n²))에, 회원과 무관하게 예약 테이블 전체를 materialize 하는 파생테이블. 콜당 고정 비용이 큰 쿼리가 전 페이지에서 호출되며 전부 단일 reader 로 갔습니다.

인과 사슬

  1. 트래픽 급증 → 포인트 조회가 reader CPU 를 99% 로 고정
  2. 쿼리가 평소 0.5초 → p50 30초 (HikariCP 타임아웃 30초와 겹치는 값)
  3. 조회 하나가 풀 커넥션을 30초씩 점유 → 5개짜리 풀이 순식간에 고갈
  4. 결제 확정이 같은 풀을 공유 → 커넥션을 못 얻어 타임아웃
  5. 결제 확정이 통짜 트랜잭션이라 PG 승인 성공 후 DB 저장이 실패 → catch 의 자동취소마저 같은 고갈 풀을 써서 실패 → "과금됐는데 예약도 결제 기록도 없음"
  6. 느려지니 사용자가 재접속·새로고침 → 포인트 조회가 평소의 10배로 증폭 → 악순환

돌아보면 5번이 제일 아픕니다. 조회 부하가 결제를 죽인 건 둘이 같은 5개짜리 풀을 공유했기 때문이고, 외부 과금과 DB 저장이 한 트랜잭션에 묶여 있어 부분 실패가 최악의 형태(과금만 성공)로 남았습니다.

조치

  • 즉시: 포인트 조회를 DB 를 안 타는 더미 응답으로 핫픽스. 기능 미오픈이라 프론트가 실값을 쓰지 않아 무해했고, reader 부하가 사라지며 결제 풀이 즉시 회복됐습니다. 프론트의 전 페이지 호출도 제거.
  • 근본: 슬로우 쿼리들을 재작성(별도 글에서 다뤘습니다), API 를 잔액/내역으로 분리해 헤더가 무거운 내역 쿼리를 부를 일 자체를 없앴습니다.
  • 남은 것: 결제 경로와 조회가 풀을 공유하는 구조(격벽 분리), PG 호출을 트랜잭션 안에 감싼 통짜 구조. 쿼리가 빨라져 완화됐지만 구조는 그대로라 별도 과제로 남겼습니다.

진단 시그니처

  • 커넥션 풀 고갈 알람 → 풀 크기를 만지기 전에 writer/reader 지표를 분리해서 볼 것. 풀 고갈은 증상이고, 커넥션을 오래 잡게 만드는 원인(느린 쿼리, 포화된 reader, 느린 외부 호출)이 따로 있는 경우가 많습니다
  • 쿼리 p50 이 풀 타임아웃 값(30초)에 수렴 → 쿼리가 느린 게 아니라 어딘가 포화됐다는 신호
  • "미오픈 기능" 의 실제 트래픽은 요청 로그로 확인 — 프론트의 부수 호출(헤더, 프리페치)이 실트래픽 1위일 수 있습니다
#aurora#connection-pool#hikari#incident#payment#performance#reader#troubleshooting