Lección 1 — Concevoir

Architecture d'un traitement nocturne

8 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

Chez Voyages Astrolabe, agence de voyages en ligne (fictive), les avis clients de la veille arrivent par trois canaux : e-mails, formulaire du site, transcriptions d'appels. Pendant deux ans, un script Python lancé par cron sur une VM les classait chaque nuit. Un matin, le service qualité ne trouve aucun rapport : le script s'est arrêté à 3 h 12 sur un quota Gemini, après avoir traité la moitié des avis. Un collègue le relance à 9 h ; la première moitié est classée une seconde fois et facturée deux fois, et le rapport compte 47 avis au lieu de 31. Personne n'a été prévenu de l'échec, et la VM tourne 24 h sur 24 pour 20 minutes de travail.

Ce projet remplace ce script par trois services managés, chacun pour ce qu'il fait le mieux, et rend l'ensemble relançable à n'importe quel moment sans double traitement.

Le mécanisme

flowchart LR
  SCH["Cloud Scheduler<br/>03:30 Europe/Paris"] -- "POST executions<br/>(jeton OAuth)" --> WF["Cloud Workflows<br/>lab-batch-nocturne"]
  WF -- "1. termine.json existe ?" --> GCS[("Cloud Storage<br/>entrant/ sorties/ rapports/")]
  WF -- "2. un job par fichier,<br/>2 à la fois" --> JOB["Cloud Run job<br/>2 tâches par fichier"]
  JOB -- "lots de 5 avis" --> G["Vertex AI<br/>gemini-2.5-flash"]
  JOB -- "part-NNN.jsonl<br/>puis part-NNN.stats.json" --> GCS
  WF -- "3. synthèse des stats" --> G
  WF -- "4. rapport.json" --> GCS
  WF -- "5. notification" --> PS["Pub/Sub<br/>lab-batch-nocturne-fin"]
  WF -- "6. termine.json" --> GCS

Le partage des rôles :

ServiceResponsabilitéCe qu'il ne fait pas
Cloud Schedulerdécider quand : une expression cron, un fuseau horaire, des réessais si le déclenchement échoueaucun calcul, aucun état
Cloud Workflowsdécider dans quel ordre et que faire en cas d'erreur : étapes, branches parallèles, réessais, notificationpas de calcul lourd : une exécution est facturée au pas, et une expression est limitée à 400 caractères
Cloud Run jobcalculer : un conteneur lancé en N tâches parallèles, chacune avec son index, qui s'arrête quand le travail est finine sait pas ce qui vient avant ou après
Cloud Storagegarder l'état : entrées, résultats, marqueurs de fin

Un job Cloud Run diffère d'un service : il ne répond pas à des requêtes HTTP, il démarre, exécute son conteneur jusqu'au bout et s'arrête. Une exécution du job lance taskCount tâches ; chacune reçoit CLOUD_RUN_TASK_INDEX (0 à N-1) et CLOUD_RUN_TASK_COUNT, et chaque tâche en échec est réessayée jusqu'à --max-retries fois.

L'idempotence à deux niveaux

La leçon du script cron : une relance doit reprendre là où le travail s'est arrêté, sans refaire ce qui est fait.

  • Par tranche : chaque tâche écrit ses résultats, puis en dernier part-NNN-of-MMM.stats.json. Si ce fichier existe, la tâche s'arrête sans appeler Gemini. Une tâche qui tombe avant de l'écrire sera entièrement refaite, ce qui est sûr puisque ses résultats seront réécrits à l'identique.
  • Par date : le workflow écrit rapports/<date>/termine.json en toute dernière étape. S'il existe au démarrage, l'exécution s'arrête avec le statut deja_traite.

Pourquoi pas autre chose

OptionQuand la choisirPourquoi pas ici
Scheduler → job Cloud Run directementun seul calcul, pas d'étape avant ou aprèsni parallélisme par fichier, ni synthèse, ni notification d'échec
Workflows + Cloud Run jobs (ce projet)quelques étapes, des appels d'API, de la reprise
Cloud Composer (Airflow managé)des dizaines de pipelines, des dépendances entre eux, une équipe data qui connaît Airflowun environnement Composer coûte des centaines d'euros par mois même au repos
Vertex AI Pipelinesentraînement, évaluation, lignage d'artefacts MLplus lourd qu'utile pour classer 30 avis
Prédiction par lot Vertex AIdes dizaines de milliers de requêtes, aucun besoin de résultat avant plusieurs heuresmoitié prix, mais asynchrone, jusqu'à 24 h ; à envisager quand le volume grandit

La mise en œuvre

L'arborescence du dépôt (files/) suit ce découpage :

job/batch.py            la tâche : tranche, lots Gemini, marqueur de fin
job/Dockerfile          image du job (Cloud Build)
infra/workflow.yaml     l'orchestration
infra/cloud-up.sh       bucket, Pub/Sub, comptes de service, image, job, workflow, planification
tests/test_batch.py     la tâche, avec un faux Gemini et un faux stockage en mémoire
tests/test_workflow.py  les décisions d'architecture du workflow (ordre des étapes, réessais)
e2e.py                  scénario réel : planification, relance, reprise, échec notifié

Les tests de test_workflow.py ne remplacent pas la validation de Google (gcloud workflows deploy analyse la syntaxe réelle) ; ils empêchent une modification de défaire une décision sans que personne ne le voie, par exemple déplacer l'écriture du marqueur avant la notification :

files/tests/test_workflow.py (extrait)
def test_idempotence_le_marqueur_est_verifie_avant_tout_travail():
    order = [next(iter(s)) for s in WF["main"]["steps"]]
    assert order.index("deja_traite") < order.index("traitement")
    assert MAIN["sortie_idempotente"]["return"]["statut"] == "deja_traite"
cd files && make setup && make test

Résultat obtenu (2026-10-16)

$ make test
..............                                                           [100%]
14 passed in 0.46s

Dont test_reprise_une_tranche_terminee_n_appelle_plus_gemini, test_panne_avant_marqueur_la_relance_refait_la_tranche et test_notification_avant_marqueur_et_notification_d_echec.

Ce qui casse en production

  • Le marqueur écrit trop tôt. Écrire termine.json au début (« en cours ») transforme toute panne en journée perdue : la relance voit le marqueur et ne fait rien. Parade : le marqueur est la dernière écriture, et il signifie « terminé », jamais « commencé ».
  • Deux exécutions simultanées. Le déclenchement planifié et une relance manuelle démarrent à la même minute : les deux voient « pas de marqueur ». Les tâches restent idempotentes (même résultat écrit deux fois), mais Gemini est appelé deux fois. Parade : ne pas relancer pendant qu'une exécution est ACTIVE (gcloud workflows executions list --filter=state=ACTIVE) ; pour une garantie stricte, un verrou par création conditionnelle d'objet (ifGenerationMatch=0).
  • La date « d'hier » calculée en UTC. À 00 h 30 heure de Paris, en été, il est 22 h 30 UTC la veille : une date calculée en UTC traite l'avant-veille. Parade : time.format(…, "Europe/Paris") dans le workflow et --time-zone=Europe/Paris sur la planification.
  • Une machine allumée pour rien. Le script sur VM coûtait une VM permanente. Ici, rien ne tourne entre deux nuits : Scheduler et Workflows sont facturés à l'usage, le job seulement pendant ses tâches.

La règle de décision

Séparez le quand (Scheduler), l'ordre et les erreurs (Workflows) et le calcul (Cloud Run jobs). Rendez chaque unité de travail idempotente par un marqueur écrit en dernier, à chaque niveau où une relance peut arriver. Passez à Composer quand les pipelines se comptent par dizaines et dépendent les uns des autres, à la prédiction par lot quand le volume rend le prix unitaire plus important que le délai.

Pratique

Le traitement d'une nuit a échoué à 3 h 40 sur la synthèse Gemini, après que les 6 tranches ont été écrites. Vous relancez l'exécution à 8 h. Combien d'appels Gemini sont faits par les tâches du job ?

La direction veut ajouter une étape : chaque lundi, comparer la semaine écoulée à la précédente. Où la mettez-vous ?

Exercice. Le volume passe de 30 à 30 000 avis par nuit. Quels réglages changez-vous, et à partir de quand basculez-vous vers la prédiction par lot de Vertex AI ?

Voir la solution

D'abord les réglages, sans changer l'architecture :

  • TASKS_PER_FILE de 2 à, par exemple, 20 : 10 000 avis par fichier, 500 par tâche, 100 lots de 5 ;
  • BATCH_SIZE de 5 à 20 ou 25 : moins d'appels, et le texte de consigne est amorti sur plus d'avis ; à vérifier sur un jeu de référence, la qualité baisse quand les lots grossissent ;
  • --parallelism du job, pour ne pas dépasser le quota Gemini : 20 tâches × 2 jobs simultanés, c'est 40 appels concurrents.

La prédiction par lot devient intéressante quand la facture Gemini dépasse ce qu'on est prêt à payer pour un résultat avant 7 h, puisqu'elle coûte moitié prix mais peut prendre jusqu'à 24 h. Elle s'intègre dans la même architecture : le job prépare un fichier JSONL de requêtes, le workflow soumet le lot et attend, puis le job agrège les sorties.

  • J'attribue à Scheduler, Workflows et Cloud Run jobs leur responsabilité propre.
  • Je conçois une idempotence par tranche et par date, avec des marqueurs écrits en dernier.
  • Je calcule une date « de la veille » dans le bon fuseau horaire.
  • J'argumente Workflows contre Composer et contre la prédiction par lot pour un volume donné.
Siguiente