virli/tutorial/docker-internals/tini.md

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

tini1 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 :

``` 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", ...] ```