Redémarrage de services IA et orchestration

LicenceInformatique et Intelligence ArtificielleRedemarrer un ordinateurRedémarrage de services IA et orchestration~16 min de lecture

Du processus au cluster : construire pas à pas la reprise d'un service IA

Un service d'IA devient résistant aux redémarrages par paliers : d'abord un état sauvegardé et un travail reprenable, puis un processus relancé automatiquement (systemd), puis un conteneur, puis Kubernetes avec ses sondes et ses déploiements sans coupure, et enfin des tests de reprise qui prouvent que tout cela fonctionne.

Objectifs

  • Monter, palier par palier, la reprise automatique d'un service d'IA : état, processus, conteneur, orchestrateur, déploiement, tests.
  • Rendre un entraînement et un pipeline de traitement reprenables après un redémarrage (sauvegarde d'état, idempotence).
  • Comprendre ce qu'un redémarrage fait à une session GPU et comment le vérifier.
  • Configurer et calculer le comportement des relances automatiques : systemd, Docker, Kubernetes.
  • Choisir entre rolling update et blue/green, puis écrire des tests de reprise mesurés (RTO, RPO).

1. La feuille de route : six paliers

On ne passe pas directement d'un script lancé à la main à un cluster Kubernetes. Chaque palier règle un problème précis et s'appuie sur le précédent :

flowchart LR
  P0[0 Lancement manuel] --> P1[1 Etat sauvegarde]
  P1 --> P2[2 Relance par systemd]
  P2 --> P3[3 Conteneur]
  P3 --> P4[4 Kubernetes et sondes]
  P4 --> P5[5 Deploiement sans coupure]
  P5 --> P6[6 Tests de reprise]
PalierProblème régléOutil
1. État sauvegardéLe travail en cours est perdu au redémarrageCheckpoints, journal d'avancement
2. Relance du processusPersonne ne relance le service après un plantagesystemd (Restart=)
3. ConteneurL'environnement (bibliothèques, CUDA) diffère d'une machine à l'autreDocker (--restart, HEALTHCHECK)
4. OrchestrateurUne seule machine, et un service bloqué n'est pas détectéKubernetes (Deployment, sondes, Job)
5. Déploiement sans coupureUne mise à jour arrête le serviceRolling update, blue/green
6. Tests de repriseOn croit que la reprise marche sans l'avoir prouvéPannes provoquées et mesurées

2. Palier 1 : rendre le travail reprenable

2.1 Ce qui disparaît au redémarrage d'un processus d'IA

  • La session GPU : chaque processus qui utilise le GPU possède un contexte CUDA. Quand le processus se termine, le pilote libère ce contexte et toute la mémoire GPU associée (poids du modèle, cache, lots en cours). Au redémarrage, il faut tout recharger.
  • L'état d'entraînement : poids du modèle, état de l'optimiseur (par exemple les moments d'Adam), planificateur du taux d'apprentissage, numéro d'époque et de pas, état des générateurs aléatoires, position dans les données.
  • Le travail d'un pipeline : quels fichiers ont déjà été traités, quels résultats sont déjà écrits.
  • Les requêtes en cours d'un service d'inférence : elles échouent si le processus est tué au milieu.

2.2 Deux règles pour reprendre sans erreur

  • Sauvegarde complète et atomique : un checkpoint contient tout l'état listé ci-dessus (en PyTorch, model.state_dict(), optimizer.state_dict(), scheduler.state_dict(), le pas courant). On l'écrit d'abord dans un fichier temporaire, puis on le renomme avec os.replace(tmp, chemin) : sur un même système de fichiers, le renommage est atomique, donc une coupure pendant l'écriture ne laisse jamais un checkpoint à moitié écrit à la place du bon.
  • Idempotence : refaire une étape déjà faite doit donner le même résultat, sans doublon. Par exemple, chaque résultat est écrit sous un nom déterminé par son entrée (facture_0042.json pour facture_0042.jpg) : le retraiter écrase le fichier au lieu d'en ajouter un second.

Pourquoi écrit-on le checkpoint dans un fichier temporaire avant de le renommer ?

  1. Pour que l'écriture soit plus rapide
  2. Pour libérer la mémoire GPU
  3. Pour qu'une coupure pendant l'écriture n'abîme pas le dernier checkpoint valide
Tant que le renommage n'a pas eu lieu, l'ancien checkpoint reste intact. Le renommage atomique remplace l'ancien par le nouveau d'un seul coup.

3. Paliers 2 et 3 : relancer le processus, puis le conteneur

3.1 Palier 2 : systemd

Sous Linux, on décrit le service dans un fichier d'unité, par exemple /etc/systemd/system/factures.service :

[Unit]
Description=Service IA de lecture de factures
StartLimitIntervalSec=120
StartLimitBurst=4

[Service]
ExecStart=/opt/factures/venv/bin/uvicorn app:app --host 0.0.0.0 --port 8000
Restart=on-failure
RestartSec=5
TimeoutStopSec=30

[Install]
WantedBy=multi-user.target
  • Restart=on-failure : systemd relance le service s'il s'arrête en erreur (code de sortie non nul, signal, dépassement de délai).
  • RestartSec=5 : attente de 5 s avant chaque relance.
  • StartLimitIntervalSec=120 et StartLimitBurst=4 : au plus 4 démarrages sur 120 s ; au-delà, systemd abandonne et met l'unité en échec (sinon un service cassé tournerait en boucle). Après correction, sudo systemctl reset-failed factures.service remet le compteur à zéro.
  • TimeoutStopSec=30 : à l'arrêt, systemd envoie SIGTERM et laisse 30 s au service pour finir ses requêtes, puis envoie SIGKILL.

On active le tout avec sudo systemctl daemon-reload puis sudo systemctl enable --now factures.service ; journalctl -u factures.service affiche son journal.

3.2 Palier 3 : le conteneur Docker

Le conteneur emporte avec lui Python, les bibliothèques et le modèle : il démarre de la même façon sur toutes les machines. Docker a ses propres politiques de relance, données par --restart : no (par défaut), on-failure (avec un maximum facultatif, par exemple on-failure:5), always et unless-stopped (toujours, sauf si on l'a arrêté soi-même).

docker run -d --name factures --gpus all --restart unless-stopped -p 8000:8000 factures:2.1

Dans le Dockerfile, une vérification de santé décrit comment savoir si le service répond :

HEALTHCHECK --interval=30s --timeout=5s --retries=3 CMD curl -f http://localhost:8000/health || exit 1

Le service a planté, l'administrateur l'a ensuite arrêté avec docker stop, puis la machine redémarre. Avec --restart unless-stopped, que se passe-t-il ?

  1. Le conteneur ne redémarre pas, car on l'avait arrêté soi-même
  2. Le conteneur redémarre, car il avait planté avant
  3. Docker supprime le conteneur
unless-stopped relance toujours le conteneur, sauf s'il a été arrêté volontairement : dans ce cas il reste arrêté, même après un redémarrage de Docker ou de la machine.

4. Palier 4 : Kubernetes, sondes et Jobs

Kubernetes fait tourner les conteneurs dans des pods, répartis sur plusieurs machines (les nœuds). Pour un service d'inférence permanent, on utilise un Deployment qui maintient un nombre voulu de pods (replicas). Pour un travail qui doit se terminer (entraînement, pipeline par lots), on utilise un Job.

4.1 Les trois sondes

  • startupProbe : laisse au conteneur le temps de démarrer (chargement du modèle sur le GPU). Tant qu'elle n'a pas réussi, les deux autres sondes ne sont pas exécutées.
  • readinessProbe : le pod ne reçoit du trafic que lorsqu'elle réussit. On la fait réussir seulement après le préchauffage du modèle (une première inférence, souvent plus lente).
  • livenessProbe : si elle échoue plusieurs fois de suite, Kubernetes redémarre le conteneur. C'est elle qui règle le cas du modèle bloqué mais vivant.

Un GPU se demande dans les ressources du conteneur, par exemple nvidia.com/gpu: 1 dans resources.limits (avec le plugin GPU de NVIDIA installé sur le cluster).

4.2 Quand un conteneur plante en boucle

Si un conteneur plante sans arrêt, Kubernetes le relance avec un délai qui double à chaque fois (valeurs par défaut : 10 s, 20 s, 40 s… plafonné à 5 minutes). Le pod affiche alors l'état CrashLoopBackOff. On en cherche la cause avec kubectl describe pod nom-du-pod et kubectl logs nom-du-pod --previous (journal du conteneur précédent, celui qui a planté).

4.3 Les Jobs pour les pipelines et les entraînements

Un Job relance son pod en cas d'échec, au plus backoffLimit fois (6 par défaut), avec restartPolicy: OnFailure ou Never. Cette relance n'est utile que si le travail est reprenable (palier 1) : le pod relancé doit relire le dernier checkpoint ou le journal d'avancement, rangé sur un volume persistant et pas dans le conteneur.

Le modèle met 90 s à se charger. Quelle sonde doit absorber ce délai ?

  1. livenessProbe
  2. startupProbe
  3. readinessProbe
La startupProbe est faite pour les démarrages lents : tant qu'elle n'a pas réussi, la livenessProbe n'est pas exécutée et ne peut donc pas tuer le conteneur pendant le chargement.

5. Palier 5 : mettre à jour sans couper

  • Rolling update (stratégie par défaut d'un Deployment) : les anciens pods sont remplacés progressivement par des nouveaux ; kubectl rollout status deployment/factures suit l'avancement et kubectl rollout undo deployment/factures revient à la version précédente. Avec des GPU rares, un nouveau pod peut rester en attente (Pending) faute de GPU libre : on règle alors maxSurge: 0 et maxUnavailable: 1 pour libérer un GPU avant d'en occuper un autre.
  • Blue/green : la version actuelle (bleue) continue de servir pendant qu'une version complète (verte) démarre et est testée. On bascule ensuite le Service en changeant son sélecteur, par exemple kubectl patch service factures -p '{"spec":{"selector":{"app":"factures","version":"vert"}}}'. Le retour arrière consiste à remettre le sélecteur sur la version bleue, gardée en attente.
Bascule blue/green : le Service passe de la version bleue à la version verte Service Bleu · v2.1 3 pods, 3 GPU Vert · v2.2 démarrage tests réussis : prêt bascule gardé pour retour

Étape 1 : la version verte démarre à côté de la bleue, qui sert encore tout le trafic ; il faut des GPU libres pour elle.

Étape 2 : on teste la version verte (sondes prêtes, quelques requêtes de contrôle) sans qu'aucun client ne la voie.

Étape 3 : on change le sélecteur du Service ; tout le trafic passe au vert, et le bleu reste disponible pour un retour arrière immédiat.

6. Palier 6 : prouver la reprise par des tests

Deux mesures guident les tests :

  • RTO (Recovery Time Objective) : durée maximale acceptable entre la panne et le retour du service.
  • RPO (Recovery Point Objective) : quantité maximale de travail ou de données qu'on accepte de perdre, exprimée en temps (par exemple « au plus 10 minutes d'entraînement »).

Un plan de test de reprise, pas à pas :

  1. Noter l'état de départ : nombre de pods prêts (kubectl get pods), dernier checkpoint, mémoire GPU (nvidia-smi --query-gpu=memory.used,memory.total --format=csv).
  2. Provoquer la panne : kubectl delete pod nom-du-pod (arrêt propre), kill -9 du processus (arrêt brutal), redémarrage d'un nœud après kubectl drain.
  3. Observer : kubectl get pods -w et chronométrer le temps jusqu'au retour à l'état prêt.
  4. Vérifier le fond, pas seulement la forme : le service répond-il juste ? L'entraînement est-il reparti du dernier checkpoint ? La mémoire GPU a-t-elle bien été libérée (nvidia-smi --query-compute-apps=pid,used_memory --format=csv ne doit plus montrer l'ancien processus) ?
  5. Comparer au RTO et au RPO, puis corriger et recommencer.

7. Exemples résolus

Exemple 1 : au bout de combien de temps systemd abandonne-t-il ?

Cherche d'abord : calcule la durée d'un cycle « démarrage, plantage, attente ».

Étape 1 : durée d'un cycle. \[ 20 \text{ s} + 5 \text{ s} = 25 \text{ s} \]
Étape 2 : instants des démarrages. \[ t_1 = 0, \quad t_2 = 25, \quad t_3 = 50, \quad t_4 = 75 \text{ s} \] Ce sont 4 démarrages en moins de 120 s : la limite est atteinte.
Étape 3 : le cinquième démarrage. Il serait prévu à \( t_5 = 100 \) s, encore dans la fenêtre de 120 s : systemd le refuse et met l'unité en échec (systemctl status factures.service l'affiche, systemctl --failed la liste).
Étape 4 : réagir. Ce n'est pas un problème de redémarrage : relancer en boucle ne servirait à rien. On lit journalctl -u factures.service, on revient à la version précédente, puis sudo systemctl reset-failed factures.service et sudo systemctl start factures.service.

Exemple 2 : reprendre un pipeline par lots

Cherche d'abord, puis ouvre la solution.

Voir la solution

Nombre de lots :

\[ \frac{12\,000}{500} = 24 \text{ lots} \]

Le pod relancé reprend au lot 15, le premier qui n'est pas marqué terminé. Il reste :

\[ 24 - 14 = 10 \text{ lots, soit } 10 \times 500 = 5\,000 \text{ photos} \]

Les 200 photos déjà faites du lot 15 sont traitées une seconde fois. Grâce à l'idempotence (un résultat par nom de fichier, écrasé s'il existe), cela ne crée aucun doublon. Durée de la fin du travail :

\[ \frac{5\,000}{2} = 2\,500 \text{ s} \approx 41{,}7 \text{ min} \]

Le travail perdu se limite à ces 200 photos, soit \( \frac{200}{2} = 100 \) s de calcul : c'est le RPO réel de ce pipeline, au plus un lot (\( \frac{500}{2} = 250 \) s).

Exemple 3 : combien de temps en CrashLoopBackOff ?

Cherche d'abord.

Voir la solution

Les délais avant les relances successives sont 10, 20, 40, 80, 160 puis 300 s (320 dépasserait le plafond). La sixième relance a donc lieu après :

\[ 10 + 20 + 40 + 80 + 160 + 300 = 610 \text{ s} \approx 10 \text{ min} \]

Leçon : attendre qu'un pod en CrashLoopBackOff « finisse par repartir » fait perdre de longues minutes ; il faut lire kubectl logs nom-du-pod --previous dès les premiers plantages.

Dans l'exemple 2, si le journal d'avancement était gardé dans le système de fichiers du conteneur au lieu d'un volume persistant, que se passerait-il après la relance du pod ?

  1. Le Job reprendrait quand même au lot 15
  2. Le Job s'arrêterait en erreur
  3. Le Job repartirait du lot 1, car le journal serait perdu avec l'ancien conteneur
Le système de fichiers d'un conteneur disparaît avec lui. Le nouveau pod ne trouve aucun journal et recommence tout : 12 000 photos au lieu de 5 000.

8. Erreurs fréquentes

  • Automatiser la relance d'un travail qui n'est pas reprenable : systemd, Docker ou un Job relancent le processus, mais il recommence tout depuis le début.
  • Checkpoint incomplet : poids sans état de l'optimiseur, du planificateur ou du pas courant.
  • Écrire le checkpoint directement à sa place définitive : une coupure pendant l'écriture détruit le dernier état valide.
  • Croire que Docker relance un conteneur « unhealthy » : seul l'arrêt du processus déclenche la politique --restart.
  • Confier le chargement du modèle à la livenessProbe au lieu de la startupProbe.
  • Prévoir un blue/green sans GPU de réserve : la version verte reste en Pending.
  • Ne tester la reprise que sur un arrêt propre : il faut aussi tester l'arrêt brutal (kill -9, redémarrage de nœud).

9. Récapitulatif

  • Palier 1 : un état sauvegardé complet et atomique, et un travail idempotent.
  • Palier 2 : systemd relance le processus (Restart=on-failure) et abandonne au-delà de StartLimitBurst démarrages par StartLimitIntervalSec.
  • Palier 3 : le conteneur rend l'environnement identique partout ; --restart ne réagit qu'à l'arrêt du processus.
  • Palier 4 : Kubernetes détecte aussi les blocages grâce aux sondes ; un Job relance un travail par lots au plus backoffLimit fois.
  • Palier 5 : rolling update (économe) ou blue/green (bascule instantanée, double capacité).
  • Palier 6 : des pannes provoquées et chronométrées, comparées au RTO et au RPO.
Checkpoint complet
Poids, état de l'optimiseur et du planificateur, pas courant, état aléatoire : tout ce qu'il faut pour reprendre à l'identique.
Idempotence
Refaire une étape donne le même résultat, sans doublon : condition pour reprendre sans risque.
Session GPU
Contexte CUDA d'un processus ; libéré à sa fin, il faut recharger le modèle au redémarrage.
startupProbe
Sonde qui laisse le temps au modèle de se charger avant que la livenessProbe ne surveille le conteneur.
Blue/green
Deux versions complètes ; on bascule le trafic d'un coup en changeant le sélecteur du Service.
RTO et RPO
RTO : durée maximale de coupure acceptée. RPO : travail ou données qu'on accepte de perdre.

Teste tes connaissances sur ce cours

Crée ton compte gratuitement pour accéder aux quiz associés et suivre ta progression.