AI SREClickHouse Workshops

07 테스트, 실패, 복구

모듈 07 강사 노트 — 타이밍, 토크 트랙, 흔한 실패 사례, 초기화 절차.

Your computer
macOS terminal: Run workshop commands in Terminal using zsh or bash.

학습자 레슨 07 테스트, 실패, 복구에 대응하는 진행자용 안내서입니다.

타이밍

약 20분. 이 모듈이 장애 실습입니다. 모듈 08과 마무리 전에 진단이 마무리될 수 있도록 충분한 활주로를 확보하세요. 실습은 결함 01(진단 515분)을 다룹니다. 결함 02(510분)와 03(10~20분, 그리고 약 3천만 행 전체 데이터셋이 있을 때만)은 진행이 빠른 강의실을 위한 선택 추가 장애로 남아 있습니다. 마무리가 밀리지 않도록 리허설에서 하드 스톱 시각을 정하세요.

토크 트랙

  • 여기가 성과가 드러나는 지점입니다. 지금까지 만든 모든 것을 동원해 실제 장애를 처리합니다.
  • 원인이 아니라 증상을 설명하고, 에이전트가 텔레메트리로부터 스스로 수렴하게 하세요.
  • 여러 참가자의 진단을 프로젝터에 나란히 띄워 보는 것도 고려하세요.
  • 실습은 결함 01을 다룹니다. 시간이 남아 한 라운드를 더 할 수 있다면 결함 02를 추가하세요 (결함 03은 전체 데이터셋이 미리 시드된 경우에만). 결함 사이에는 아래의 공통 stash 후 브랜치 전환 초기화 절차를 사용하세요.

정답지

이 섹션을 학습자와 공유하지 마세요. 플레이북에만 존재하며 앱 저장소에는 절대 커밋되지 않습니다. 각 결함은 build-workshop-v1에서 갈라진 자체 브랜치의 작은 변경 하나이며, 복구 방법은 그것을 되돌리는 것입니다. 한 번에 결함 하나만 실행하세요. 실습의 장애는 결함 01(515분, 변칙구 — 백엔드는 무죄)입니다. 진행이 빠른 강의실을 위한 선택 추가: 결함 02(510분, 워밍업, 트레이스가 전부를 말해줌), 그리고 전체 데이터셋이 있을 때만 결함 03(10~20분, 더 깊은 ClickHouse 추론).

모든 결함에 공통인 초기화 절차(stash는 참가자의 편집을 보존하고 브랜치 전환이 막히는 것을 방지합니다):

git stash push --include-untracked -m "module-07-fix"
git switch build-workshop-v1
docker compose --env-file .env.workshop \
  -f docker-compose.workshop.yml -f docker-compose.otel.yml up -d --build

결함 02 — zone stats 500 (fault/02-zone-stats-500)

참가자에게 보이는 커밋 메시지: "backend: align zone stats column names with API params" (backend/app/query_builders.py의 zone_stats_sql만 건드립니다). 이 변경은 pickup_location_id 대신 taxi_trips에 존재하지 않는 컬럼인 pickup_zone_id로 그룹화합니다.

  • 증상: 지도 단계 구분도가 비어 있고(폴리곤은 렌더링되지만 모든 zone이 가장 밝은 구간에 들어갑니다) "Query Nms" 캡션이 사라집니다. GET /api/metrics/zone_stats에서 500이 연달아 발생합니다(React Query가 세 번 재시도). 다른 카드는 모두 정상입니다.
  • 신호가 있는 곳: error=True, error.category="query_failed"이고 db.statement에 pickup_zone_id AS zone_id가 담긴 ClickStack clickhouse.query 스팬. 기록된 예외는 ClickHouse Code 47 UNKNOWN_IDENTIFIER입니다. 대응되는 ERROR 로그: "ClickHouse query failed (category=query_failed, elapsed_ms=...): ... | sql=...".
  • 진단 경로: 500 스팬을 찾음 -> db.statement를 읽음 -> pickup_zone_id에 대한 UNKNOWN_IDENTIFIER -> DESCRIBE taxi_trips(또는 형제 쿼리 빌더와 비교) -> 실제 컬럼은 pickup_location_id.
  • 복구: zone_stats_sql에서 pickup_zone_id를 다시 pickup_location_id로 바꾸거나 (결함 커밋을 git revert), 백엔드를 다시 빌드합니다.
  • 초기화: 위의 공통 초기화 절차.

결함 03 — 느린 대시보드 (fault/03-slow-dashboard)

커밋 메시지: "backend: window the trend series by bucket in HAVING" (timeseries_sql을 건드립니다). 시간 범위 조건을 WHERE에서 버킷 별칭에 대한 HAVING으로 옮겨서 WHERE가 1이 됩니다. ClickHouse가 더 이상 (car_type, pickup_datetime) 기본 키로 프루닝할 수 없어 quantileTDigest 상태 두 개와 함께 taxi_trips를 전체 스캔하고, 5초 max_execution_time을 넘깁니다.

  • 증상: "What's happening now?" 추세 카드만 실패합니다 — 로딩만 돌다가 "504 Query timed out..."을 표시합니다. 형제 카드는 모두 빠르게 유지됩니다.
  • 신호가 있는 곳: error.category="timeout", db.elapsed_ms가 5000 근처이고 db.statement가 WHERE 1 ... GROUP BY ts HAVING ts >= ...를 보여주는 clickhouse.query 스팬. 예외는 Code 159 TIMEOUT_EXCEEDED입니다. 다른 엔드포인트의 빠른 200 스팬과의 대비가 결정적 단서입니다.
  • 진단 경로: 빠른 형제들 사이에서 느린 카드 하나 -> 타임아웃 스팬을 열어봄 -> SQL을 읽음 -> 시간 필터가 HAVING에 있고 WHERE에는 pickup_datetime 범위가 없음 -> 기본 키 프루닝 / 조건 푸시다운 안티패턴(형제들은 범위 조건을 WHERE에 둡니다).
  • 복구: 시간 범위 조건을 WHERE로 되돌리고(결함 커밋을 revert) 백엔드를 다시 빌드합니다.
  • 초기화: 위의 공통 초기화 절차.
  • 강사 주의 사항: 심각도는 데이터 양과 서비스 크기에 비례합니다. 전체 시드 데이터셋 (약 3천만 행)과 워크숍 등급 서비스(유휴 상태에서 깨어나는 중일 수 있음)에서는 5초 타임아웃이 안정적으로 발생하지만, 작은 샘플 시드에서는 단지 조금 느릴 뿐이며 확실한 504가 아닙니다. 클라이언트의 send_receive_timeout과 서버의 max_execution_time이 모두 5초이므로, 소켓 타임아웃 경쟁 때문에 깔끔한 504 대신 500이 간간이 나타날 수 있습니다 — 결함이 아니라 기본 설정의 성질입니다.

결함 01 — 지도가 로딩되지 않음 (fault/01-map-not-loading)

커밋 메시지: "frontend: serve map geojson from /static asset path" (frontend/src/ui/ZoneMap.tsx의 fetch 경로를 건드립니다). 존재하지 않는 /static/taxi_zones.geojson을 가져옵니다. 핵심 미묘함: nginx의 SPA 폴백 (try_files ... /index.html)이 404가 아니라 HTML 문서와 함께 HTTP 200을 반환하므로 r.ok는 통과하고 r.json()이 Unexpected token '<' 같은 SyntaxError를 던집니다.

  • 증상: 지도 카드가 죽습니다 — 베이스 타일은 렌더링되지만 NYC 폴리곤이나 단계 구분도가 없고, 빨간 인라인 오류가 표시됩니다. 나머지는 모두 정상입니다.
  • 신호가 있는 곳: 백엔드 ClickStack에는 아무것도 없습니다 — 그 asset 요청은 FastAPI에 도달하지 않습니다. 브라우저 콘솔에는 /static/taxi_zones.geojson을 지목하는 JSON 파싱 오류가 보이고, Network 탭에는 그 요청이 상태 200과 text/html로 반환되는 것이 보이며, nginx 액세스 로그에는 폴백이 기록됩니다.
  • 진단 경로: 백엔드 트레이스는 깨끗함 -> 브라우저 콘솔 / Network 탭으로 전환 -> .geojson 요청이 200 text/html을 반환 -> SPA 폴백이라는 함정을 알아차림 -> 파일은 실제로 웹 루트, 즉 /taxi_zones.geojson에 있음.
  • 복구: fetch 경로를 되돌리거나(두 줄) 결함 커밋을 git revert한 뒤 프런트엔드를 다시 빌드합니다.
  • 초기화: 위의 공통 초기화 절차.
  • 교육 포인트: 모든 실패가 백엔드 트레이스에 나타나는 것은 아니며, SPA 폴백 뒤에서 404가 200으로 위장할 수 있습니다 — 그러므로 상태 코드만 보지 말고 실제 응답 본문과 content-type을 읽어야 합니다.
  • 참고(브라우저 SDK): HyperDX 브라우저 SDK가 활성화되어 있으면(모듈 05 오버레이), 이제 프런트엔드가 이 문제를 ClickStack 자체에 드러냅니다 — ZoneMap의 파싱 실패가 ServiceName=nyc-taxi-frontend 아래의 console.error 스팬으로 캡처되고, text/html 200에 대한 리소스 스팬과 짝지어집니다. 따라서 에이전트는 ClickStack을 벗어나지 않고 텔레메트리만으로 문제를 특정할 수 있습니다. 브라우저 콘솔 / Network 탭은 유일한 경로가 아니라 대안입니다.

에이전트 응답 예시 (결함 01, ClickStack MCP 경유):

결함 01에 대한 에이전트의 근본 원인 분석: 누락된 /static/taxi_zones.geojson이 SPA index.html을 200 text/html로 반환한다는 점, nyc-taxi-frontend의 console.error 스팬으로 캡처된 ZoneMap JSON 파싱 오류를 짚고, 정상적인 /api 및 백엔드 스팬과 대비하며, SDK 자체 점검 오류는 노이즈로 표시하고, asset을 배포하거나 파싱을 방어하도록 제안한다

예상 진단: 에이전트는 실패를 200 text/html을 반환하는 geojson 요청(SPA 폴백)과 그로 인해 nyc-taxi-frontend 아래에 캡처된 JSON.parse 오류에 연결하고, asset을 /static/으로 배포하거나 .json() 파싱을 방어하라고 권고합니다.

흔한 실패 사례

  • 에이전트가 문제를 특정할 만한 텔레메트리 기준선이 부족함. 모듈 05가 일찍 실행되었어야 합니다.
  • 참가자가 증거로 근본 원인을 확인하기 전에 복구로 뛰어듦.
  • 스택을 방금 올린 탓에 — 또는 결함 03의 경우 과거 데이터가 너무 적게 로드된 탓에 — 결함 증상이 아직 보이지 않음. 리허설에서 측정됨: 기본 한 달치 시드(약 317만 행)에서는 WHERE 대 HAVING 결함이 관측되지 않습니다 — 전체 스캔이 약 300~560ms에 끝나 기준선과 구별되지 않습니다. 결함 03을 데모하려면 여러 달을 시드하거나, 즉시 재현되는 결함 01을 고수하고 결함 03은 대규모에서의 예시로 다루세요.
  • 결함 01은 백엔드 트레이스를 전혀 만들지 않으며, 잘못된 요청은 404가 아니라 SPA 폴백을 통해 200 text/html을 반환합니다. 참가자가 트레이스를 끝없이 찾을 수 있습니다. JSON 파싱 오류와 text/html 응답이 보이는 브라우저 콘솔 / Network 탭으로 유도하세요.
  • 결함 브랜치를 체크아웃한 뒤 --build를 잊어서 예전 이미지가 계속 실행됨.

초기화 절차

  • 공통 stash 후 브랜치 전환 초기화 절차는 참가자가 시도한 복구를 버리지 않고 앱 전체를 복원합니다.
  • 결함을 다시 주입하려면 fault/01-map-not-loading / fault/02-zone-stats-500 / fault/03-slow-dashboard 중 하나를 체크아웃하고 워크숍과 otel compose 파일로 다시 빌드하세요.
  • 결함별 증상, 신호, 복구 방법은 위의 정답지 섹션에 있습니다. 정답지는 이 플레이북에만 두세요. 앱 저장소에는 절대 커밋되지 않습니다.

이 페이지의 내용

KO