\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`[^TINI_PROJECT] 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. [^TINI_PROJECT]: `init` à l'envers : 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` :
``` docker container run --init -d nemunaire/youp0m ```
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 :
``` # Install tini for signal processing and zombie killing ENV TINI_VERSION v0.18.0 RUN wget -O /usr/local/bin/tini "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini" && \ chmod +x /usr/local/bin/tini && \ tini --version ENTRYPOINT ["/usr/local/bin/tini", ...] ```