AI SREClickHouse Workshops

07 テスト、失敗、修復

モジュール 07 のインストラクターノート — タイミング、トークトラック、よくある失敗、リセット手順。

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

学習者向けレッスン 07 テスト、失敗、修復 に対応するファシリテーター用の手引きです。

タイミング

約20分。ここがインシデントラボです。モジュール 08 とまとめの前に診断が着地するよう、十分な滑走路を 確保してください。ラボでは障害 01(診断5〜15分)を扱います。障害 02(5〜10分)と 障害 03(10〜20分。約3000万行のフルデータセットがある場合のみ)は、進行の速い会場向けの オプションの追加インシデントとして残されています。まとめが圧迫されないよう、リハーサルでハードストップを 決めておいてください。

トークトラック

  • ここが成果の見せ場: これまでに構築したすべてを使って、実際のインシデント対応を行う。
  • 原因ではなく症状を説明し、エージェントたちにテレメトリーから収束させる。
  • 複数の参加者の診断をプロジェクターで並べて見せることも検討する。
  • ラボでは障害 01 を扱う。もう1ラウンド分の時間があれば障害 02 を追加する(障害 03 はフルデータセットを 事前にシード投入している場合のみ)。障害の切り替えには、下記の共通の stash して switch する リセット手順を使う。

解答集

このセクションは学習者と共有しないでください。プレイブックの中だけに存在し、アプリのリポジトリには 決してコミットされません。各障害は build-workshop-v1 から分岐した専用ブランチ上の小さな1つの変更であり、 修正はそれを元に戻すことです。障害は一度に1つだけ実行してください。ラボのインシデントは障害 01 (5〜15分、変化球 — バックエンドは無実)です。進行の速い会場向けのオプションの追加分: 障害 02 (5〜10分、ウォームアップ。トレースがすべてを語る)と、フルデータセットがある場合のみの障害 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 のみを変更)。taxi_trips に存在しないカラムである pickup_zone_id で、pickup_location_id の代わりにグループ化している。

  • 症状: 地図のコロプレスが空になる(ポリゴンは描画されるが、すべてのゾーンが最も明るいバケットに入る)。 「Query Nms」のキャプションが表示されない。GET /api/metrics/zone_stats で 500 が連続する (React Query が3回リトライする)。他のカードはすべて正常。
  • シグナルの在り処: error=True、error.category="query_failed"、そして pickup_zone_id AS zone_id を含む db.statement を持つ 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) の primary key で刈り込めなくなり、2つの quantileTDigest の状態を伴って taxi_trips をフルスキャンし、5s の max_execution_time を超える。

  • 症状: 「What's happening now?」のトレンドカードだけが失敗する — スピナーが回った後に 「504 Query timed out...」と表示される。兄弟のカードはすべて高速なまま。
  • シグナルの在り処: error.category="timeout"、約 5000 の db.elapsed_ms、そして WHERE 1 ... GROUP BY ts HAVING ts >= ... を示す db.statement を持つ clickhouse.query スパン。 例外は Code 159 TIMEOUT_EXCEEDED。他のエンドポイントで高速な 200 のスパンとの対比が決め手になる。
  • 診断の道筋: 高速な兄弟の中に1つだけ遅いカードがある -> タイムアウトしたスパンを開く -> SQL を読む -> 時間フィルターが HAVING にあり、WHERE に pickup_datetime の範囲がない -> primary key による 刈り込み/述語のプッシュダウンのアンチパターン(兄弟は範囲を WHERE に置いている)。
  • 修正: 時間範囲を WHERE に戻す(障害のコミットを revert する)。バックエンドを再ビルドする。
  • リセット: 上記の共通リセット。
  • インストラクター向けの注意: 深刻度はデータ量とサービスサイズに比例する。フルにシード投入した データセット(約3000万行)とワークショップ相当のサービス(アイドルから復帰中の可能性もある)では 5s のタイムアウトが確実に発火するが、小さなサンプルシードでは単に遅くなるだけで、はっきりした 504 にはならない。 クライアントの send_receive_timeout とサーバーの max_execution_time はどちらも 5s なので、 ソケットタイムアウトの競合により、きれいな 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 ではなく HTTP 200 で HTML ドキュメントを返すため、 r.ok は通過し、r.json() が Unexpected token '<' のような SyntaxError を投げる。

  • 症状: 地図カードが機能しない — ベースタイルは描画されるが NYC のポリゴンもコロプレスも表示されず、 赤いインラインエラーが出る。それ以外はすべて正常。
  • シグナルの在り処: バックエンドの ClickStack には何もない — アセットが FastAPI に届いていない。 ブラウザのコンソールに /static/taxi_zones.geojson を名指しする JSON パースエラーが出る。 Network タブでは、そのリクエストがステータス 200 で text/html を返している。nginx のアクセスログには フォールバックが記録されている。
  • 診断の道筋: バックエンドのトレースはきれい -> ブラウザのコンソール/Network タブに視点を移す -> .geojson のリクエストが 200 text/html を返している -> SPA フォールバックの落とし穴に気づく -> ファイルは実際には Web ルート、つまり /taxi_zones.geojson にある。
  • 修正: fetch のパスを戻す(2行)か、障害のコミットを 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 として返していること、ZoneMap の JSON パースエラーが nyc-taxi-frontend の console.error スパンとして捕捉されていることを指摘し、正常な /api とバックエンドのスパンと対比し、SDK のセルフテストのエラーをノイズとして扱い、アセットの配置かパースのガードを提案している

期待される診断: エージェントは、geojson のリクエストが 200 text/html を返していること(SPA フォールバック)、 その結果 nyc-taxi-frontend の下で捕捉された JSON.parse のエラーに障害を結びつけ、アセットを /static/ に 配置するか .json() のパースをガードすることを推奨する。

よくある失敗

  • エージェントが問題を特定するためのテレメトリーのベースラインが足りていない。モジュール 05 を早めに 実行しておく必要がある。
  • 参加者が証拠から根本原因を確認する前に修正へ飛びついてしまう。
  • スタックを起動したばかりで障害の症状がまだ見えない。あるいは障害 03 の場合、履歴データの投入量が 少なすぎる。ドライランでの計測値: デフォルトの1か月分のシード(約317万行)では、 WHERE と HAVING の入れ替えによる障害は観測できない — フルスキャンが約300〜560ms で完了し、 ベースラインと区別がつかない。障害 03 をデモするなら数か月分をシード投入するか、 即座に再現する障害 01 に留め、障害 03 は大規模時の例示として扱う。
  • 障害 01 はバックエンドのトレースをまったく生成せず、不正なリクエストは 404 ではなく SPA フォールバック経由で 200 text/html を返す。参加者はトレースを延々と探し続けてしまうことがある。JSON パースエラーと text/html のレスポンスが見えるブラウザのコンソール/Network タブへ促すこと。
  • 障害ブランチをチェックアウトした後に --build を忘れ、古いイメージが動き続ける。

リセット手順

  • 共通の stash して switch するリセットは、参加者が試みた修正を捨てずにアプリ全体を復元する。
  • 障害を再注入するには、fault/01-map-not-loading / fault/02-zone-stats-500 / fault/03-slow-dashboard のいずれかをチェックアウトし、workshop と otel の compose ファイルを指定して再ビルドする。
  • 障害ごとの症状、シグナル、修正は上記の解答集セクションにある。解答集はこのプレイブックの中だけに 留めること。アプリのリポジトリには決してコミットしない。

このページの内容

JA