docs(architecture): ajoute le runbook de pilotage des traitements automatisés
Carte des sept DAGs et de leurs planifications en UTC, lecture du verdict de dérive, réentraînement sans perdre le modèle en service, rétention et reprise après incident. Renvoie aux vues et aux ADR qui portent le détail.
This commit is contained in:
@@ -0,0 +1,105 @@
|
|||||||
|
# 70 · Pilotage des traitements automatisés
|
||||||
|
|
||||||
|
Ce document dit à l'opérateur quoi surveiller, comment lire ce qu'il voit et quoi faire quand un
|
||||||
|
traitement déraille. Il ne redit pas le fonctionnement : la table des DAGs vit dans
|
||||||
|
[10-infra.md](10-infra.md), la chaîne de données dans [40-data.md](40-data.md), la dérive dans
|
||||||
|
l'[ADR 0013](../adr/0013-surveillance-de-derive-dans-le-backend.md), la rétention dans
|
||||||
|
l'[ADR 0019](../adr/0019-stockage-objet-garage-et-cycle-de-vie-des-mesures.md), la supervision
|
||||||
|
dans [60-observabilite.md](60-observabilite.md).
|
||||||
|
|
||||||
|
## Carte des traitements
|
||||||
|
|
||||||
|
Les planifications sont en **UTC** : Airflow n'a pas de fuseau configuré. En heure de Paris
|
||||||
|
l'été, ajouter deux heures.
|
||||||
|
|
||||||
|
| DAG | Planification (UTC) | Ce qu'il fait | Même traitement à la main |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `mock_api_import` | chaque heure à :45 | importe la mesure de l'heure pile de l'API Mock, une par site | `python -m app.etl.mock_api_import --start-time … --end-time …` |
|
||||||
|
| `ml_score` | chaque heure pile | score le pas horaire suivant, écrit `prediction` | `make ml-score` |
|
||||||
|
| `alertes` | chaque heure à :15 | détecte les alertes internes, puis génère les recommandations | `make detect-alerts`, puis `make recommendations` |
|
||||||
|
| `retention` | 03:20 | exporte vers Garage les chunks de `reading` de plus de 1 095 jours, puis les supprime | `python -m app.etl.reading_retention` |
|
||||||
|
| `derive` | 05:30 | calcule le rapport de dérive du modèle sur 168 h | `python -m app.monitoring.drift` |
|
||||||
|
| `ml_train` | manuel | réentraîne LightGBM et **écrase** le modèle | `make ml-train` |
|
||||||
|
| `historical_import` | manuel | importe le jeu historique CSV | `python -m app.etl.historical_import --csv … --metadata …` |
|
||||||
|
|
||||||
|
Les modules `app.*` se lancent depuis `apps/backend` (`uv run python -m …`). Tous les DAGs ont
|
||||||
|
`max_active_runs=1` et `catchup=False` : un retard ne rejoue pas les heures manquées.
|
||||||
|
|
||||||
|
## Où regarder
|
||||||
|
|
||||||
|
Sur la machine, chaque environnement vit dans `/srv/enervision/<env>` et n'écoute que sur la
|
||||||
|
boucle locale : on y accède par tunnel SSH.
|
||||||
|
|
||||||
|
| Quoi | Production | Recette | Dev |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Interface Airflow | `127.0.0.1:8080` | `127.0.0.1:8082` | `127.0.0.1:8084` |
|
||||||
|
| Grafana (production seulement) | `127.0.0.1:3001` | - | - |
|
||||||
|
| Mailpit, où arrivent les alertes | `127.0.0.1:8025` | `127.0.0.1:8026` | `127.0.0.1:8027` |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -L 8080:127.0.0.1:8080 -L 3001:127.0.0.1:3001 -L 8025:127.0.0.1:8025 root@<IP-VM-G3>
|
||||||
|
```
|
||||||
|
|
||||||
|
En ligne de commande, depuis `/srv/enervision/<env>` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
compose="docker compose -f docker-compose.yml -f docker-compose.prod.yml"
|
||||||
|
$compose exec airflow-apiserver airflow dags list-runs derive # derniers passages
|
||||||
|
$compose exec airflow-apiserver airflow dags trigger ml_train # lancement manuel
|
||||||
|
$compose logs --tail=100 airflow-scheduler # les tâches tournent ici
|
||||||
|
```
|
||||||
|
|
||||||
|
**Aucune alerte ne signale l'échec d'un DAG** ([60-observabilite.md](60-observabilite.md)) :
|
||||||
|
un coup d'œil quotidien à l'interface Airflow reste nécessaire.
|
||||||
|
|
||||||
|
## Lire le rapport de dérive
|
||||||
|
|
||||||
|
`GET /api/v1/monitoring/drift` (rôle `operateur`) rend le dernier rapport par site et une ligne
|
||||||
|
globale. Le verdict suit cet ordre (`app/services/drift.py`) :
|
||||||
|
|
||||||
|
| Verdict | Condition | Ce que ça veut dire | Quoi faire |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `indetermine` | moins de 24 prévisions vérifiées sur la fenêtre | pas assez de recul pour conclure | attendre ; si ça dure, vérifier `ml_score` |
|
||||||
|
| `derive`, couverture | moins de 80 % des prévisions ont trouvé leur mesure réelle | **le pipeline**, pas le modèle | vérifier `mock_api_import` et `ml_score` dans Airflow |
|
||||||
|
| `derive`, erreur | MAE au-delà de 1,25 fois celle de la fenêtre de référence | le modèle se trompe plus qu'avant | réentraîner (ci-dessous) |
|
||||||
|
| `derive`, biais | biais absolu au-delà du seuil, désactivé par défaut | le modèle se trompe toujours du même côté | réentraîner, après en avoir cherché la cause dans les données |
|
||||||
|
| `stable` | aucune des conditions précédentes | rien à faire | - |
|
||||||
|
|
||||||
|
## Réentraîner sans perdre le modèle en service
|
||||||
|
|
||||||
|
`ml_train` reste manuel parce que `train.py` écrase le modèle sans comparer ses métriques à
|
||||||
|
celles de l'ancien ([10-infra.md](10-infra.md)). Garder une copie avant de lancer :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$compose exec airflow-scheduler cp /opt/ml/state/models/lightgbm-consumption.txt \
|
||||||
|
/opt/ml/state/models/lightgbm-consumption.txt.avant
|
||||||
|
$compose exec airflow-apiserver airflow dags trigger ml_train
|
||||||
|
```
|
||||||
|
|
||||||
|
Comparer ensuite les métriques des deux derniers runs, que `train.py` enregistre dans le magasin
|
||||||
|
MLflow du volume (`/opt/ml/state/mlflow.db`). Aucune interface MLflow n'est servie sur la
|
||||||
|
machine : `ml/README.md` décrit `mlflow ui`. Si le nouveau modèle est moins bon, remettre la
|
||||||
|
copie en place : le prochain `ml_score` l'utilisera.
|
||||||
|
|
||||||
|
## Rétention et archives
|
||||||
|
|
||||||
|
`retention` supprime de la base les chunks de `reading` plus vieux que
|
||||||
|
`APP_READING_RETENTION_DAYS` (1 095 jours), **après** les avoir exportés en CSV gzip dans le
|
||||||
|
bucket Garage de l'environnement, chiffrés en SSE-C. Il est idempotent : une reprise ne réécrit
|
||||||
|
pas un objet déjà exporté et ne retrouve plus un chunk déjà supprimé.
|
||||||
|
|
||||||
|
- **La clé `GARAGE_SSE_KEY` du `.env` est la seule qui déchiffre les archives.** Garage ne la
|
||||||
|
garde pas. Perdue, les archives sont illisibles : elle se sauvegarde hors de la machine.
|
||||||
|
- Restaurer un chunk : `get_object` avec la clé SSE-C, `gunzip`, puis
|
||||||
|
`COPY reading FROM STDIN CSV HEADER`. La contrainte `uq_reading_source` refuse les doublons
|
||||||
|
([ADR 0019](../adr/0019-stockage-objet-garage-et-cycle-de-vie-des-mesures.md)).
|
||||||
|
|
||||||
|
## Reprendre après un incident
|
||||||
|
|
||||||
|
| Situation | Reprise |
|
||||||
|
|---|---|
|
||||||
|
| `mock_api_import` a manqué des heures | relancer à la main avec `--start-time` et `--end-time` sur la fenêtre manquante ; le module refuse toute fenêtre qui recouvre le CSV historique |
|
||||||
|
| `alertes` a échoué | relancer : les deux tâches sont idempotentes (`ON CONFLICT DO NOTHING`) |
|
||||||
|
| `derive` a échoué | relancer : un index d'unicité par fenêtre empêche les doublons |
|
||||||
|
| `retention` a échoué | relancer : idempotent, voir plus haut |
|
||||||
|
| `ml_score` en échec répété | lire les journaux du scheduler ; deux tentatives et un plafond de 30 minutes par passage |
|
||||||
Reference in New Issue
Block a user