← Retour au blog
Outillage

L'erreur à ne pas faire en production : gérer les signaux de terminaison

Comprendre SIGTERM, SIGKILL et le graceful shutdown : pourquoi votre application doit gérer les signaux de terminaison pour éviter la perte de données en production.

📅 ✍️ Antoine Coulon
sigtermsigkillgraceful-shutdownkubernetesnodejs

Quand vous déployez une application en production, vous ne contrôlez plus vraiment quand elle s’arrête. Ce n’est plus vous qui décidez : c’est un orchestrateur. Et le jour où celui-ci décide de couper votre process (pour un redéploiement, une mise à l’échelle, un nœud qui bascule), la seule question qui compte est : votre application a-t-elle eu le temps de finir ce qu’elle faisait ? Dans la majorité des incidents de perte de données « inexplicables » en production, la réponse est non. Et le coupable est presque toujours le même : une application qui ignore les signaux de terminaison.

L’application n’est plus maîtresse de son cycle de vie

En production, votre application tourne la plupart du temps dans un container dont le cycle de vie est piloté par un orchestrateur, Kubernetes étant l’exemple le plus répandu. Le rôle de cet orchestrateur est de lancer, arrêter et redémarrer des containers au gré des besoins, en fonction des règles de supervision que vous avez définies : montée en charge, déploiement d’une nouvelle version, redistribution sur un autre nœud, vérification de santé qui échoue, etc.

Cela change radicalement la donne par rapport à un process lancé à la main dans un terminal. L’arrêt de votre application devient une décision externe, prise par un superviseur, à un moment que vous n’anticipez pas forcément. Pour communiquer cette décision, l’orchestrateur ne fait pas appel à une API applicative : il s’appuie sur le mécanisme le plus universel du système d’exploitation, les signaux de terminaison, envoyés par le process superviseur au process qui exécute votre application.

Comprendre les signaux : SIGTERM et SIGKILL

Deux signaux dominent le sujet de l’arrêt d’un process. Ils ont des intentions opposées et il est essentiel de bien les distinguer.

SIGTERM : la demande polie

SIGTERM est le signal de terminaison le plus courant. On qualifie cette terminaison de « soft » car le signal se contente d’ordonner au process de s’arrêter : le process visé reste libre de l’intercepter, de réagir, voire de l’ignorer. C’est le signal envoyé par défaut sur les systèmes Unix lorsqu’on utilise la commande kill sans préciser de signal.

L’analogie est celle du bouton OFF de votre télécommande : vous exprimez une intention d’éteindre, et l’appareil dispose d’un court instant pour se mettre proprement hors tension.

SIGKILL : la coupure brutale

SIGKILL est, à l’inverse, le signal le plus brutal. Il ne laisse aucune marge au process cible : celui-ci ne peut ni réagir, ni l’intercepter, ni l’ignorer. Le système met fin au process immédiatement. Par définition, SIGKILL ne permet donc aucun arrêt propre, aucun graceful shutdown.

L’analogie ici est celle de la prise secteur que vous débranchez d’un coup : tout ce qui était en cours est interrompu sur-le-champ, sans aucune possibilité de sauvegarde.

Comment un orchestrateur arrête réellement votre application

La plupart des orchestrateurs suivent un protocole d’arrêt simple, en deux temps :

  1. Envoi d’un signal SIGTERM au process qui fait tourner l’application.
  2. Si, après un délai de grâce de X secondes, le process est toujours vivant, alors un SIGKILL lui est envoyé pour forcer sa terminaison.

Ce protocole en deux étapes est délibérément conçu pour vous laisser une chance de bien faire. Le SIGTERM est une invitation à vous arrêter proprement ; le SIGKILL n’est que le filet de sécurité de l’orchestrateur, pour le cas où vous ne répondriez pas. Tout l’enjeu consiste donc à profiter de la fenêtre entre les deux.

Trois points méritent une attention particulière.

Votre application est responsable de la gestion des signaux

C’est le point le plus contre-intuitif : en règle générale, les runtimes ne gèrent pas ces signaux système par défaut. Node.js comme la JVM délèguent logiquement cette responsabilité à l’application elle-même. Autrement dit, si vous n’écrivez aucun code pour réagir à SIGTERM, le comportement par défaut sera une terminaison sèche du process.

Sans gestion explicite du signal, vous n’aurez aucune possibilité d’effectuer un graceful shutdown, c’est-à-dire de nettoyer et libérer vos ressources, terminer les tâches critiques en cours, vider vos buffers, fermer proprement vos connexions ou émettre vos derniers logs.

Le délai de grâce a un coût d’infrastructure

J’ai mentionné plus haut que l’orchestrateur attend X secondes avant d’envoyer SIGKILL. Ce délai n’est pas anodin, et il varie selon les plateformes :

Un delta de 30 secondes peut avoir des répercussions énormes lorsque le process est dans un état défectueux. Pendant tout ce temps, l’orchestrateur ne peut pas redémarrer le process : celui-ci occupe son slot et bloque la reprise. Une application qui ne réagit pas à SIGTERM paie donc deux fois : d’abord en perdant la possibilité d’un arrêt propre, ensuite en immobilisant inutilement les ressources de l’orchestrateur pendant toute la fenêtre de grâce.

SIGKILL peut corrompre vos données

C’est la conséquence la plus grave. Par définition, un SIGKILL qui survient parce que l’application n’a pas tenu compte du SIGTERM peut entraîner une perte ou une corruption de données : écriture interrompue en plein milieu, transaction non validée, message consommé mais jamais acquitté, fichier laissé dans un état incohérent. Toutes ces situations naissent d’un arrêt qui survient au mauvais moment, sans que l’application ait pu atteindre un point de cohérence.

Réagir correctement au signal

La bonne nouvelle, c’est que la mise en place est simple : il suffit d’enregistrer un handler sur le signal SIGTERM pour exécuter votre logique d’arrêt propre avant que le process ne se termine. En Node.js, cela passe par process.on :

// Node.js

process.on("SIGTERM", () => {
  // server, databases, file handles...
  cleanUpRessources();
  logger.fatal("Received SIGTERM. Exiting...");
  process.exit(143);
});

Quelques détails de ce squelette méritent d’être soulignés :

L’idée directrice est toujours la même : utiliser la fenêtre de grâce ouverte par SIGTERM pour atteindre un état cohérent, puis sortir de vous-même, avant que l’orchestrateur n’ait à dégainer son SIGKILL.

Conclusion

En production, l’arrêt de votre application n’est pas un détail d’infrastructure que l’on peut déléguer entièrement à l’orchestrateur : c’est une responsabilité applicative. SIGTERM est une demande polie d’arrêt, assortie d’une courte fenêtre pour faire les choses bien ; SIGKILL est la sanction qui tombe si vous ne réagissez pas à temps, avec à la clé un risque réel de perte ou de corruption de données.

Pensez donc à gérer explicitement le shutdown en réagissant correctement à ces signaux depuis votre application. C’est une poignée de lignes de code, mais elles font la différence entre un redéploiement transparent et un incident de production silencieux.

Réagir à SIGTERM n’est toutefois que la première pièce du puzzle. Encore faut-il que ce signal parvienne réellement à votre application, ce qui n’a rien d’évident selon la façon dont votre container est construit. C’est précisément le sujet de la seconde partie, consacrée au process PID 1 et à l’init system.