IL-0571 STORES · 2024-07-01 (게시일)database
STORES 予約의 스마트리스트 검색 쿼리가 특정 테넌트에서 20분 넘게 DB 프로세스를 붙잡아 504 에러가 급증했다
STORES 予約의 스마트리스트는 고객 검색 조건을 저장해 두고 고객 목록에서 조건에 맞는 고객에게 라벨을 붙이는 기능이다. 팀은 고객 목록을 검색할 때마다, 화면에 보이는 최대 50명만 대상으로 스마트리스트마다 DB를 검색해 라벨을 정하도록 만들었다. 출시 2주 전 QA에서 팀은 예약 페이지별 예약 횟수와 최종 예약일 검색이 의도와 다르게 동작한다는 것을 알았다. 그래서 고객과 예약 페이지별 예약 횟수와 최종 예약일을 모으는 인라인 뷰로 검색을 다시 만들었다. 고객 검색에는 옵티마이저 힌트 MAX_EXECUTION_TIME(1000) 을 넣었고, 검증 환경에서 EXPLAIN과 타임아웃을 확인한 뒤 운영 환경에 배포했다.
그 뒤 사양 변경에 맞춰 미리 만들어 둔 프리셋 데이터의 검색 조건을 고쳐 쓰자 프리셋이 새 검색 쿼리를 타게 됐다. 놓친 것은 세 가지였다. 스마트리스트 대상 고객을 찾는 쿼리에는 MAX_EXECUTION_TIME 힌트를 넣지 않았다. pluck 결과로 ID만 넘겨 힌트가 이어지지 않았기 때문이다. 바깥 쿼리는 고객 ID 50개로 좁혔지만 인라인 뷰 안쪽은 좁히지 않아 테넌트의 모든 고객을 훑었다. 프리셋 이름은 그대로라 엔지니어는 큰 변경이라고 생각하지 않았다. 대부분의 테넌트는 몇 초 안에 응답했지만, 특정 테넌트에서는 쿼리 하나가 20분 넘게 DB 프로세스를 붙잡아 서버가 막혔다.
EXPLAIN에서는 인덱스가 쓰이고 수만 분의 1로 좁혀지는 것처럼 보였다. 그러나 멀티테넌트 환경에서는 테넌트가 1만 개만 돼도 테넌트 ID 인덱스만으로 1만 분의 1로 좁혀지므로, 크게 좁혀진 것처럼 보여도 실제로는 상당한 행을 훑고 있었다. 동작 확인은 두 검색이 모두 통과하는 경우와 모두 실패하는 경우만 봤고, 첫 검색은 통과하고 스마트리스트 검색만 오래 걸리는 경우는 보지 않았다. 모니터링은 라이터 인스턴스만 봤는데 문제의 쿼리는 리더 인스턴스로 갔다. 막힌 요청은 지표를 보내기 전에 프로세스가 중지돼 원인을 찾는 데 시간이 걸렸다. 팀은 이후 DB 설계부터 다시 봤다. 스마트리스트 출시는 5월로 미뤄져 5월 6일에 나왔다.
- 심각도
- –
- 영향 범위
- Requests to STORES 予約 returned 504 errors (Gateway Timeout) while the database was stuck on long queries
- 영향 사용자
- –
- 지역
- –
- 감지까지
- –
- 복구까지
- –
증상
스마트리스트 프리셋 데이터를 고쳐 쓰는 배치를 20분쯤 돌린 시점에 504 Gateway Timeout 에러가 갑자기 늘었다는 알림이 왔다. 작업은 03:00 UTC 조금 넘어 시작했다.
작업 중에는 Web 서버 응답 지연과 쓰기용 DB의 부하 같은 지표를 보고 있었는데 큰 변화가 없었다. 문제의 쿼리는 리더 인스턴스로 갔기 때문에 모니터링하던 라이터 인스턴스의 부하는 전혀 오르지 않았다.
엔지니어들은 원인을 찾지 못한 채 직전 변경을 되돌렸지만 504 에러는 그치지 않았다. DB 부하가 높아 보여 프리셋 고쳐 쓰기 배치도 멈췄지만 역시 그치지 않았다.
막힌 요청의 지표도 볼 수 없었다. Rails의 DB 접근 지표는 요청 처리가 끝날 때 한꺼번에 보내는데, DB 응답을 20분 기다리던 프로세스가 멈춘 것으로 판정돼 지표를 보내기 전에 중지됐다. 이 때문에 원인을 찾는 데 시간이 더 걸렸다.
원인
- 촉발
- 검색 사양 변경에 맞춰 스마트리스트 프리셋 데이터인 初回予約の顧客 와 要フォロー顧客 의 검색 조건을 새 조건으로 고쳐 쓰는 배치를 실행했다. 프리셋 이름은 그대로였지만 검색 파라미터가 달라져 새 검색 쿼리를 타게 됐다.
감지
고쳐 쓰기 배치를 시작하고 20분쯤 지나 504 에러가 늘었다는 알림으로 알았다. 작업 중 보던 라이터 인스턴스 지표에는 변화가 없었다.
조치
–
후속 조치
- DB 설계부터 다시 봤고, 이번 글의 인라인 뷰 검색 로직은 더 이상 쓰지 않는다.
- 이 API의 EXPLAIN을 확인해 훑는 행 수가 예상보다 많지 않은지 검사하는 테스트를 더했다.
- 해당 로직의 실제 실행 시간을 재기 쉬운 환경을 갖췄다.
교훈
실행 계획을 보고 실제 데이터로 동작을 확인해도, 멀티테넌트 환경에서는 테넌트 인덱스만으로 크게 좁혀지는 것처럼 보여 실제로 훑는 양을 놓칠 수 있다. 검증은 양극단만이 아니라 일부만 느린 경우까지 봐야 한다. 작업 중 모니터링하는 대상이 실제 부하가 걸리는 곳인지도 확인해야 한다. 일정을 너무 서두르면 이런 부분이 빠지기 쉽고, 결국 일정이 늦어지고 사용자에게 피해를 준다.
출처
관련 사례
같은 원인 · 공통 데이터베이스
IL-0136Sweden Central 리전에서 내부 서비스 배포가 캐시를 비워 메타데이터 백엔드에 평소 20배의 트래픽이 몰렸고, 데이터베이스 연결에 기대는 헬스 체크가 인스턴스를 계속 재시작시켜 Azure OpenAI 요청이 6시간 가까이 실패했다Microsoft Azure · 2026같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0510MIT Learn 운영 RDS PostgreSQL 의 저장 공간이 임시 파일로 가득 차 learn.mit.edu 가 세 시간 동안 503 과 504 를 냈다MIT Open Learning · 2026같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0547Buildkite 의 Agent API, REST API, GraphQL 이 annotations 테이블 쿼리 타임아웃으로 4시간 가까이 저하됐다Buildkite · 2025같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0324External Storage 데이터베이스 클러스터에 과부하가 걸려 mw-api-int, mw-parsoid, mw-jobrunner 의 지연과 데이터베이스 오류가 늘었다Wikimedia Foundation · 2025같은 원인 · 공통 데이터베이스
IL-0307Supabase us-east-1 에서 Supavisor 2.0 업그레이드 뒤 Postgres 연결 풀러가 테넌트 약 80% 의 연결을 끊었다Supabase · 2025같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0523TANF-app 의 운영 배포가 PostgreSQL 마이그레이션 실패로 두 달 동안 막힘U.S. Department of Health and Human Services · 2023같은 원인 · 공통 데이터베이스
IL-0567쿠키런: 오븐브레이크의 패키지 매출 랭킹 기능을 배포한 뒤 Aurora MySQL CPU가 100%에 이르러 유저가 게임에 접속하지 못했다Devsisters · 2023같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
IL-0524인덱스를 다시 만드는 마이그레이션이 운영에서 Transactions 테이블을 잠가 Open Collective 서비스가 느려짐Open Collective · 2023같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
같은 원인 · 공통 데이터베이스
내 클러스터에도 있는지 확인하기
AI 도구와 스크립트용
- 이 사례의 JSON: https://incidentlib-api.benchlet.com/cases/IL-0571
- 가장 비슷한 사례들: https://incidentlib-api.benchlet.com/cases/IL-0571/similar
- MCP 서버(Streamable HTTP, 키 없음):
https://incidentlib-mcp.benchlet.com/mcp
API 는 키가 필요 없습니다. MCP 서버는 search_incidents, get_incident, list_incidents, get_topic, vocab 도구를 내놓습니다. 사용법은 API 와 MCP.