5.7 KiB
\newpage
De l'importance d'avoir un système d'init dans son conteneur
Dans la dure vie d'un processus, créé par son processus parent (ppid
),
celui-ci gardera son identifiant pid
durant toute la durée de son
exécution. Puis, lorsqu'il aura terminé, celui-ci sera passé dans un statut
« zombie », en attendant que son parent direct récupère son code de retour
(grâce à l'appel système wait(2)
).
Le rôle d'init
Parmi ses attributions, init
, le PID 1 de notre système, est le processus qui
récupère les processus orphelins du système. Lorsque le parent direct d'un
processus meurt, ses fils sont reparenté sous le processus init
et ils
obtiennent alors comme ppid
1. Ils ne conservent pas le PID de leur défunt
parent.
:::: {.more}
Depuis les noyaux 3.4, n'importe quel processus peut se déclarer child subreaper et récupérer tous les orphelins dans sa descendance de processus.
Pour se déclarer child subreaper, un processus va utiliser prctl(2)
avec
l'argument PR_SET_CHILD_SUBREAPER
.
:::::
Docker procure une isolation, notamment au travers du namespace PID : les processus faisant parti du même namespace ne voient seulement qu'une partie de l'arbre de processus de l'hôte, et notamment, un PID 1 est recréé, il s'agit du premier processus à s'exécuter dans le namespace.
Ceci n'est pas anodin car ce PID 1 de notre conteneur hérite des mêmes
responsabilités qu'init
: il doit wait(2)
les zombies qui se créent autour
de lui.
\
Dans un conteneur, on a tendance à vouloir directement lancer une application, qui ne s'attend sans doute pas à devoir gérer les processus orphelins. Dans certaines circonstances, il peut donc arriver qu'un conteneur en particulier génère beaucoup de zombies, qui vont finir par rendre la machine instable, car le système sera à court de PID à distribuer.
On peut citer notamment la JVM, ou encore les conteneurs de construction de
logiciels (Jenkins, Drone, ...). En effet, si l'on peut blâmer les programmeurs
qui oublient de wait(2)
leurs fils directs, il peut arriver régulièrement
qu'une erreur de programmation crée des zombies dans les conteneurs de
construction. Sans processus pour les arrêter, chaque build risque d'ajouter
de nouveaux zombies.
init
dans mes conteneurs
tini
1 est un projet qui implémente un init
tout à fait
minimaliste, en ce sens qu'il se veut être le plus transparent possible, tout
en jouant pleinement son rôle de collecteur et suppresseur de zombies.
Sans rien ajouter de plus dans nos conteneurs ou nos Dockerfile
, nous pouvons
l'utiliser en ajoutant simplement --init
à nos lignes de docker container run
:
Dans cet exemple, avant de lancer notre ENTRYPOINT
, comme cela devrait être
le cas sans l'option --init
, ici c'est tini
qui sera appelé. Il lancera
ensuite l'ENTRYPOINT
tel qu'il avait été prévu et commencera à jouer son rôle
d'init
le plus transparent possible, en attendant la venue des zombies.
\
Mais ce n'est pas tout, tini
aide également à transmettre les signaux
d'extinction du conteneur (généralement SIGTERM
), lorsque l'on fait un
docker stop
.
::::: {.question}
Ne pourrait-on pas simplement ajouter un thread dans Jenkins/DroneCI/... qui s'occupe de gérer les zombies ? {-}
\
Alors ce n'est pas vraiment une solution idéale... si Jenkins s'exécute en tant
que PID 1, il paraît difficile de différencier les processus qui ont été
re-parentés à Jenkins (et sur lesquels il faut wait(2)
directement) et ceux
qui ont été créés par Jenkins et pour lequel le code de retour est attendu par
d'autres portions du code.
:::::
Transmission de signaux
En fait, il se trouve que bash(1)
est capable de réceptionner les zombies et
de wait(2)
, comme le fait init(1)
, ce qui évite aussi l'invasion que l'on
cherchait à éviter.
Mais bash
pose un problème : il ne transmet pas les signaux à ses fils. S'il
reçoit un SIGTERM
, celui-ci ne sera tout simplement pas traité et encore
moins transmis à ses fils, à moins d'avoir programmé un tel comportement en
shell.
Voici donc une raison supplémentaire de préférer tini
à bash
(ou à rien du
tout). D'autant plus qu'à moins d'avoir préparé la fin d'exécution, bash
ne
retournera pas le code d'erreur de la commande que l'on a lancé, mais plutôt 0.
Intégration dans les Dockerfile
tini
n'est pas nécessaire dans tous les conteneurs. On considère qu'une
application qui ne fork(2)
pas n'a pas besoin de processus init
. De même,
une image contenant un binaire qui gère correctement ses fils et qui n'a pas de
petit enfant n'aura pas besoin de tini
. Par contre dans les autres cas, il
semble particulièrement indiqué.
L'utilisation par le paramètre --init
du run
n'est pas recommandée et
devrait se limiter aux cas où l'image a été construite par quelqu'un qui
n'avait pas en tête ces contraintes. Lorsque l'on sait que des zombies ne vont
pas être géré par leurs parents, le mainteneur se doit d'ajouter tini
dans
son Dockerfile
. La méthode recommandée est de l'installer par les paquets de
la distribution (apt-get install tini
, apk add tini
, ...). Néanmoins, dans
le cas d'une distribution qui ne possèderait pas le paquet, il convient
d'ajouter ces quelques lignes :
-
init
à l'envers : https://github.com/krallin/tini ↩︎