Conteneurs et Linux
2026
sentinel
Votre programme tourne en PID 1 dans un conteneur. Le noyau jette tout signal pour lequel PID 1 n'a pas de gestionnaire, donc docker stop ne fait rien et le conteneur finit tué par SIGKILL.
- Un conteneur sous sentinel sort environ 3 ms après le SIGTERM. Le même programme seul en PID 1 ignore le signal et se fait tuer par SIGKILL au bout des dix secondes.
- Il redémarre un enfant mort avec un délai de 1, 2, 4, 8 puis 16 secondes, et sert les compteurs de redémarrages, de plantages et d'échecs à Prometheus.
- 13 cas de test ctest et 5 jobs de CI. La CI monte toute la pile Compose et échoue si Prometheus ne scrute pas ou si Grafana n'a pas le tableau de bord.
- C++17
- Linux
- Signaux POSIX
- Docker Compose
- Prometheus
- Grafana
- GitHub Actions
Ce que PID 1 vous coûte
- Arrêt après SIGTERM
- 2,5 à 3,1 ms
- Enfant qui ignore SIGTERM
- 5,003 s
- Délai entre redémarrages
- 1, 2, 4, 8, 16 s
- Suite de tests
- 13 sur 13
- Empreinte du superviseur
- 3,9 Mo RSS
Sept exécutions, le worker supervisé par sentinel en PID 1 d'un espace de noms de processus, chronométré par wait() pour éviter toute erreur de sondage. Le même worker seul était encore vivant à l'échéance de dix secondes et a fallu le tuer par SIGKILL.
Même montage, worker en mode têtu, trois exécutions à moins d'une milliseconde l'une de l'autre. C'est l'escalade vers SIGKILL qui part à l'heure.
Chronométré depuis le journal du superviseur avec un enfant qui meurt aussitôt. Cinq redémarrages en quarante secondes, et le délai plafonne à seize.
ctest sur une compilation Release, 33 s. La CI lance build, test, shellcheck, docker et compose comme jobs distincts.
Lu dans /proc pendant la supervision, sur deux fils. Le binaire fait 36 Ko, ou 31 Ko une fois dépouillé.
Le noyau ne délivre un signal à PID 1 que si PID 1 a installé un gestionnaire pour ce signal. Le conteneur reste donc bloqué tout le délai de grâce avant de mourir par SIGKILL. sentinel prend la place de PID 1 et laisse votre programme tourner comme un enfant ordinaire. Il sort avec le code de l'enfant, ou avec 128 plus le numéro du signal si l'enfant a été tué. Un conteneur qui sort en 137, c'est 128 plus 9. C'est l'OOM killer.
Les gestionnaires sont installés sans SA_RESTART. Ça compte : avec le drapeau, le noyau relance waitpid après un signal et la boucle de récupération n'a jamais la main. Sans lui, waitpid renvoie EINTR et la boucle garde le contrôle. Le gestionnaire appelle kill() et rien d'autre. Très peu de fonctions sont sûres en contexte de signal. L'escalade vit sous la même contrainte. Le gestionnaire de SIGTERM arme alarm(5), et un gestionnaire de SIGALRM se charge de tuer.
Il y avait une fenêtre entre le fork et la ligne qui installait les gestionnaires. Un SIGTERM qui arrivait dedans tuait sentinel et laissait l'enfant tourner en orphelin. C'est exactement la panne que sentinel existe pour empêcher. Je l'ai notée comme TODO et j'ai vécu avec un moment. La correction, c'est sigprocmask autour du fork. Il bloque SIGINT et SIGTERM avant, et les débloque une fois les gestionnaires en place. L'enfant restaure le masque d'origine avant l'exec. Sinon tout ce que sentinel lance démarre sourd à ces signaux.
Le serveur de métriques a besoin de son propre fil, parce que le fil principal reste bloqué dans waitpid et ne peut pas attendre aussi dans accept. Ce fil bloque tous les signaux pour lui-même. SIGTERM doit continuer d'arriver sur le fil principal, là où vit le gestionnaire, sinon waitpid n'obtient jamais son EINTR. Les compteurs sont atomiques puisque les deux fils y touchent.
Une seule commande compose monte sentinel, un Prometheus qui le scrute toutes les 5 s, et un Grafana avec le tableau de bord déjà provisionné. La pile fait tourner un enfant qui plante exprès, pour que les courbes montrent quelque chose de réel. Deux règles d'alerte l'accompagnent, une pour la boucle de plantage et une pour un enfant absent depuis deux minutes. La CI monte le tout à chaque poussée et échoue si Prometheus ne voit pas la cible, si le compteur de redémarrages n'a pas bougé, si les deux règles ne sont pas chargées ou si Grafana ne sert pas le tableau de bord.
