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 CPU 를 사용하는 요청이 무엇인지 요청 로그를 집계해 보니, 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