AI SREClickHouse Workshops

07 Tester, provoquer une panne et réparer

Notes pour le formateur du module 07 — durée, trame d’animation, problèmes courants et procédures de réinitialisation.

Votre ordinateur
Terminal macOS : Exécutez les commandes de l’atelier dans le Terminal avec zsh ou bash.

Document d’accompagnement du formateur pour la leçon participant 07 Tester, provoquer une panne et réparer.

Durée

Environ 20 minutes. Il s’agit de l’exercice d’incident ; préservez assez de temps pour aboutir au diagnostic avant le module 08 et la conclusion. L’exercice porte sur la panne 01 (diagnostic en 5 à 15 min). Les pannes 02 (5 à 10 min) et 03 (10 à 20 min, uniquement avec le jeu complet d’environ 30 millions de lignes) restent disponibles comme incidents facultatifs si le groupe avance vite. Fixez une heure limite ferme pendant la répétition afin de ne pas écourter la conclusion.

Trame d’animation

  • C’est l’aboutissement : utilisez tout ce qui a été construit pour traiter un incident réel.
  • Décrivez le symptôme, pas sa cause, et laissez les agents converger à partir de la télémétrie.
  • Vous pouvez projeter côte à côte les diagnostics de plusieurs participants.
  • L’exercice porte sur la panne 01. Si le temps permet une manche supplémentaire, ajoutez la panne 02 (et la panne 03 uniquement si le jeu de données complet a été amorcé) ; entre les pannes, utilisez la réinitialisation commune par mise à l’abri et changement de branche ci-dessous.

Corrigé

Ne partagez pas cette section avec les participants : elle figure uniquement dans le guide et n’est jamais versionnée dans le dépôt de l’application. Chaque panne correspond à une petite modification isolée dans sa propre branche créée depuis build-workshop-v1 ; la correction consiste à l’annuler. Exécutez une seule panne à la fois. L’incident de l’exercice est la panne 01 (5 à 15 min, avec un piège : le back-end est innocent). Pour un groupe qui avance vite, les compléments facultatifs sont la panne 02 (5 à 10 min, mise en jambes dont la trace révèle tout) et la panne 03, uniquement si le jeu de données complet est disponible (10 à 20 min, raisonnement ClickHouse plus approfondi).

Réinitialisation commune à toutes les pannes (la mise à l’abri conserve les modifications du participant et évite qu’elles ne bloquent le changement de branche) :

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

Panne 02 — erreur 500 des statistiques de zone (fault/02-zone-stats-500)

Message de commit visible par les participants : "backend: align zone stats column names with API params" (modifie uniquement backend/app/query_builders.py zone_stats_sql). La requête regroupe par pickup_zone_id, une colonne absente de taxi_trips, au lieu de pickup_location_id.

  • Symptôme : la carte choroplèthe est vide (les polygones s’affichent, mais toutes les zones restent dans la classe la plus claire) et la légende "Query Nms" manque ; rafales d’erreurs 500 sur GET /api/metrics/zone_stats (React Query effectue trois nouvelles tentatives). Toutes les autres cartes fonctionnent.
  • Emplacement du signal : un span ClickStack clickhouse.query avec error=True, error.category="query_failed" et un db.statement contenant pickup_zone_id AS zone_id ; l’exception enregistrée est le Code 47 UNKNOWN_IDENTIFIER de ClickHouse. Journal ERROR correspondant : "ClickHouse query failed (category=query_failed, elapsed_ms=...): ... | sql=...".
  • Parcours de diagnostic : repérer le span 500 -> lire db.statement -> UNKNOWN_IDENTIFIER sur pickup_zone_id -> DESCRIBE taxi_trips (ou comparer les générateurs de requêtes voisins) -> la véritable colonne est pickup_location_id.
  • Correction : remettre pickup_location_id à la place de pickup_zone_id dans zone_stats_sql (ou appliquer git revert au commit de la panne), puis reconstruire le back-end.
  • Réinitialisation : utilisez la procédure commune ci-dessus.

Panne 03 — tableau de bord lent (fault/03-slow-dashboard)

Message de commit : "backend: window the trend series by bucket in HAVING" (modifie timeseries_sql). Il déplace le prédicat de fenêtre temporelle de WHERE vers une clause HAVING portant sur l’alias du compartiment ; WHERE devient donc 1. ClickHouse ne peut plus élaguer selon la clé primaire (car_type, pickup_datetime) et analyse entièrement taxi_trips avec deux états quantileTDigest, dépassant la limite de 5 s définie par max_execution_time.

  • Symptôme : seule la carte de tendance "What's happening now?" échoue : elle charge, puis affiche "504 Query timed out...". Toutes les cartes voisines restent rapides.
  • Emplacement du signal : un span clickhouse.query dont error.category="timeout", avec un db.elapsed_ms proche de 5000 et un db.statement affichant WHERE 1 ... GROUP BY ts HAVING ts >= ... ; l’exception est le Code 159 TIMEOUT_EXCEEDED. Le contraste avec les spans 200 rapides des autres points de terminaison est révélateur.
  • Parcours de diagnostic : une seule carte lente parmi des voisines rapides -> ouvrir le span arrivé à expiration -> lire le SQL -> le filtre temporel se trouve dans HAVING et WHERE ne contient aucune plage sur pickup_datetime -> anti-patron d’élagage par clé primaire et de descente de prédicat (les requêtes voisines placent la fenêtre dans WHERE).
  • Correction : rétablir la fenêtre temporelle dans WHERE (annuler le commit de la panne), puis reconstruire le back-end.
  • Réinitialisation : utilisez la procédure commune ci-dessus.
  • Mise en garde pour le formateur : la gravité varie selon le volume de données et la taille du service. Avec le jeu complet amorcé (environ 30 millions de lignes) sur un service de niveau atelier, qui peut être en cours de réveil, l’expiration à 5 s se produit de façon fiable ; avec le petit échantillon, la requête est simplement plus lente et ne renvoie pas nécessairement une erreur 504. Les valeurs send_receive_timeout du client et max_execution_time du serveur sont toutes deux de 5 s : une compétition entre les délais d’expiration de socket peut donc parfois produire une erreur 500 au lieu de l’erreur 504 attendue. Cette particularité vient des paramètres de référence, pas de la panne.

Panne 01 — la carte ne charge pas (fault/01-map-not-loading)

Message de commit : "frontend: serve map geojson from /static asset path" (modifie le chemin de récupération dans frontend/src/ui/ZoneMap.tsx). Le code demande /static/taxi_zones.geojson, qui n’existe pas. Nuance essentielle : le repli SPA de nginx (try_files ... /index.html) renvoie le document HTML avec le statut HTTP 200 au lieu d’une erreur 404 ; r.ok réussit donc, puis r.json() lève une SyntaxError telle que Unexpected token '<'.

  • Symptôme : la carte est inopérante ; les tuiles de fond s’affichent, mais aucun polygone de New York ni aucune carte choroplèthe n’apparaît, et un message d’erreur rouge s’affiche dans la carte. Tout le reste fonctionne.
  • Emplacement du signal : rien dans ClickStack côté back-end, car la demande de la ressource n’atteint jamais FastAPI. La console du navigateur affiche l’erreur d’analyse JSON qui cite /static/taxi_zones.geojson ; l’onglet Network montre que cette requête renvoie text/html avec le statut 200 ; le journal d’accès de nginx montre le repli.
  • Parcours de diagnostic : les traces du back-end sont saines -> passer à la console du navigateur ou à l’onglet Network -> une requête .geojson renvoie du text/html avec le statut 200 -> reconnaître le piège du repli SPA -> le fichier se trouve en réalité à la racine web, sous /taxi_zones.geojson.
  • Correction : annuler la modification du chemin de récupération (deux lignes) ou appliquer git revert au commit de la panne ; reconstruire le front-end.
  • Réinitialisation : utilisez la procédure commune ci-dessus.
  • Point pédagogique : toutes les défaillances n’apparaissent pas dans les traces du back-end, et une erreur 404 peut se faire passer pour une réponse 200 derrière un repli SPA. Il faut donc lire le corps réel de la réponse et son type de contenu, pas seulement son code d’état.
  • Remarque (SDK du navigateur) : lorsque le SDK navigateur HyperDX est activé (surcouche du module 05), le front-end fait désormais apparaître cette erreur dans ClickStack lui-même : l’échec d’analyse de ZoneMap est capturé dans un span console.error sous ServiceName=nyc-taxi-frontend, associé au span de ressource de la réponse 200 text/html. L’agent peut donc localiser l’erreur à partir de la télémétrie sans quitter ClickStack ; la console du navigateur ou l’onglet Network constitue une solution de repli, et non le seul parcours possible.

Exemple de réponse de l’agent (panne 01, via le MCP ClickStack) :

Analyse de la cause racine de la panne 01 par l’agent : elle indique que /static/taxi_zones.geojson manque et renvoie l’index.html de la SPA avec le statut 200 et le type text/html ; elle relève l’erreur d’analyse JSON de ZoneMap capturée comme span console.error de nyc-taxi-frontend, l’oppose aux spans /api et back-end sains, écarte les erreurs d’auto-test du SDK comme du bruit et propose de livrer la ressource ou de protéger l’analyse

Diagnostic attendu : l’agent relie l’échec à la requête geojson qui renvoie le statut 200 avec le type text/html (repli SPA), à l’erreur JSON.parse qui en résulte et est capturée sous nyc-taxi-frontend, puis recommande de livrer la ressource sous /static/ ou de protéger l’appel .json().

Problèmes courants

  • La télémétrie de référence est insuffisante pour que l’agent localise l’erreur ; le module 05 doit avoir été exécuté tôt.
  • Les participants passent à la correction avant d’avoir confirmé la cause racine à partir des preuves.
  • Le symptôme de la panne n’est pas encore visible, car la pile vient de démarrer, ou, pour la panne 03, parce que trop peu de données historiques ont été chargées. Mesure effectuée pendant la répétition : avec l’amorçage par défaut sur un seul mois (environ 3,17 millions de lignes), la panne WHERE/HAVING n’est pas observable : l’analyse complète s’exécute en environ 300 à 560 ms, sans différence par rapport à la référence. Amorcez plusieurs mois avant de présenter la panne 03, ou restez sur la panne 01, qui se reproduit immédiatement, et traitez la panne 03 comme une illustration à grande échelle.
  • La panne 01 ne produit aucune trace côté back-end et la mauvaise requête renvoie le statut 200 avec text/html par le repli SPA, au lieu d’une erreur 404 ; les participants risquent de chercher indéfiniment dans les traces. Orientez-les vers la console du navigateur ou l’onglet Network, où apparaissent l’erreur d’analyse JSON et la réponse text/html.
  • Oubli de --build après le passage sur une branche de panne : l’ancienne image continue donc de s’exécuter.

Procédures de réinitialisation

  • La procédure commune de mise à l’abri et de changement de branche rétablit l’application complète sans supprimer la tentative de correction du participant.
  • Réinjectez une panne en activant l’une des branches fault/01-map-not-loading / fault/02-zone-stats-500 / fault/03-slow-dashboard, puis reconstruisez avec les fichiers Compose de l’atelier et d’OTel.
  • Les symptômes, signaux et corrections propres à chaque panne figurent dans le corrigé ci-dessus. Conservez ce corrigé uniquement dans le guide ; il ne doit jamais être versionné dans le dépôt de l’application.

Sur cette page

FR