URL: https://linuxfr.org/users/freem/journaux/gestion-des-services-avec-runit Title: Gestion des services avec runit Authors: freem Date: 2019年04月10日T20:42:17+02:00 License: CC By-SA Tags: debian et selinux Score: 21 # Gestion des services avec runit Tout le monde ici connaît plusieurs des grands intérêts de systemd, qui sont pour moi principalement, et sans ordre particulier: * description des services par déclaration, sans avoir à utiliser de scripts ultra-compliqués qui ont des dépendances dans tous les coins du système; * démarrage des services uniquement après que leurs dépendances soient prêtes; * activation au besoin; Bon, je reconnais être un peu sceptique sur le 3ème, en vrai... je ne suis pas convaincu de l'intérêt actuel d'outils de type inetd, il faut dire que je ne suis pas admin, je n'ai probablement pas rencontré les cas ou il brille. Je voudrais ici parler d'une manière alternative de réaliser, au moins en partie, les 2 autres, avec runit, et en évitant le "thundering herd problem" (de ce que j'ai compris, lancer un process avant qu'une ressource ne soit disponible, le process se mange un refus, se ferme, est relancé immédiatement, et ce jusqu'à mise à disposition de la ressource) reproché par nosh (cela dit, je ne parviens pas à retrouver la citation exacte) aux daemontools. Je ne prétends pas répondre à tous les problèmes que systemd résoud, ni même que runit n'en cause aucun, c'est juste histoire de partager mon expérience sur cet outil. Je ne suis clairement pas un expert sur le sujet, comme n'importe qui devrait pouvoir le constater en lisant la suite. ## Runit? Runit est un système d'initialisation qui se base sur la mécanique des daemontools. Contrairement à sysvinit ou rc.d, il ne «supporte» pas les runlevels et surveille ses enfants. Ceci se base sur l'idée que les enfants ne devraient pas se détacher de leur parent, ce qui est également à ma connaissance requis par systemd. Si l'on regarde ce que contient le paquet Debian runit, on y voit plusieurs ELF (je ne cache pas que j'en découvre 2-3 en écrivant ceci): * /sbin/runit * /sbin/runit-init * /sbin/update-service * /usr/bin/chpst * /usr/bin/runsv * /usr/bin/runsvdir * /usr/bin/sv * /usr/bin/svlogd * /usr/bin/runsvchdir * /usr/bin/utmpset Le binaire d'initialisation proprement dit est `runit`, qui est exécuté par `runit-init` (ne me demandez pas pourquoi, je m'en aperçois en fait en lisant les manpages...). `update-service` sert à... ajouter, supprimer, lister ou vérifier l'existence d'un service. Son véritable intérêt est probablement juste de permettre de gérer les services sans connaître réellement l'endroit du système où ils sont gérés, parce qu'en pratique on peut faire la même simplement à coup de `ln`, `rm`, `ls`... bon, je dirais qu'au moins c'est moins glissant d'utiliser `update-service` que de faire un `rm` en tant que root dans un dossier aussi critique... `chpst` est un utilitaire qui permet de manipuler l'environnement de la commande suivante. Tout comme `nice`, il fonctionne sur l'idée du chaînage de commandes. `runsv` est le watchdog, c'est lui le dernier maillon de la chaîne des processus avant le daemon lui-même, c'est aussi lui qui va rediriger la sortie standard du daemon dans un éventuel logger. `runsvdir` est le «PID2», dans l'idée, même si l'appellation est mauvaise (il ne sera probablement pas PID 2): c'est lui qui va gérer la liste des services à lancer ou arrêter. `sv` est la commande qui permet d'exercer un contrôle sur un ou plusieurs services. À noter que, tout comme systemd il permet d'en manipuler plusieurs d'un coup, contrairement au stupide `service` d'avant systemd. `svlogd` est un logger. Si l'on veut stocker les logs d'un daemon, c'est l'outil que runit propose. Il est évidemment possible d'en utiliser un autre. À noter tout de même qu'il ne reprend absolument pas la logique de `syslogd`: une instance de logger par daemon géré, ce qui évite qu'un bug quelque part détruise ou corrompe les logs de l'ensemble du système (ou, du moins, réduit le risque). L'inconvénient, par contre, c'est qu'il n'est pas possible de récupérer les logs d'une machine sur le réseau qui ne les stockerait pas (j'ai le problème au boulot avec des routeurs que l'on embarque, contourné en 50 lignes de C, mais je me demanderais toujours s'il n'existe pas déjà un outil qui juste écoute sur un port UDP pour cracher le résultat sur la sortie standard...bref.) et la configuration se fait au cas par cas (pas plus mal, en vrai). Cet outil lit l'entrée standard, applique divers traitement au texte, et écrit le tout dans un fichier. Si le fichier dépasse une taille ou un âge, ou si `svlogd` se prend un `kill -ALRM`, une rotation des logs est déclenchée. Un logger, quoi. `runsvchdir` change le dossier ciblé par `runsvdir`... une forme de runlevels, j'imagine, j'avoue ne pas en voir l'intérêt, mais bon... `utmpset` interagit avec la base de données utmp/wtmp, je n'ai ici encore aucun cas d'usage en tête, sans doute parce que je n'ai jamais eu à travailler sur des machines réellement multi-utilisateurs. Le lancement du système, sur une machine qui utiliserait la totalité des outils, est le suivant, grosso modo: 1. le noyau lance `runit-init`, qui se remplace lui-même par `runit` 2. `runit` lance le script `/etc/runit/1` 3. `runit` lance le script `/etc/runit/2`, qui lance `runsvdir` et ne doit pas se fermer (c'est l'étape de fonctionnement normal de la machine) 4. `runsvdir` examine le dossier qui lui est passé en paramètre et, pour chaque sous-dossier ou lien symbolique (max 1000) lance une instance de `runsv`. Si l'une de ces instances se ferme, il la relance, et si le dossier ou le lien est supprimé, il la ferme. 5. `runsv` lance le fichier `./run` de sa cible, ainsi qu'un éventuel `./log/run`. Si l'un de ces fichiers se ferme, il exécute `./finish` ou `./log/finish` 6. quand `/etc/runit/2` se ferme, `runit` lance le script `/etc/runit/3` 7. le système est éteint ## Implémenter un script pour chaque daemon, vraiment? Je pense que ce n'est pas nécessaire. Du moins, dans de nombreux cas, le shell nous donne tous les outils nécessaires l'éviter. Par exemple, voici le fichier `./log/run` que j'utilise: ```sh LOG_DIR="/var/log/$(basename $(dirname $(pwd)))" mkdir -p "${LOG_DIR}" exec svlogd -tt "${LOG_DIR}" ``` Ce script extrait tout simplement le nom du dossier du daemon, crée un dossier spécifique pour les stocker (bon, ça peut péter, si l'on s'amuse à créer des noms avec des '/', mais en théorie celui qui utilise `runit` ne cherche pas à péter son système, si?), et lance le logger, le `exec` étant utilisé pour remplacer le process courant: nul besoin de forker ici après tout. ## Et les dépendances? Manifestement, dans les outils cités, il n'y à rien pour gérer les dépendances des services, ni même pour vérifier que tous les pré-requis pour lancer le daemon final sont présents. Le seul outil qui permette d'éviter de lancer inutilement le processus final est donc l'exécutable `./run`, qui est le plus souvent un script shell. À titre personnel, j'implémente ces fichiers `./run` comme une longue suite de vérifications qui sortent simplement du programme si toutes les conditions ne sont pas remplies. Si une condition critique n'est pas remplie (une configuration erronée, par exemple, un matériel qui est absent, etc), je crée un fichier "down" dans le dossier géré par `runsv`, ce qui empêchera le daemon d'être à nouveau démarré: si la config est foireuse, c'est inutile après tout... sinon, `runsv` attend une seconde et retente. Rien n'empêcherait de boucler par une `until`, ce serait peut-être même plus robuste (sauf si entre temps une autre condition n'est plus remplie, mais même en revérifiant tout le risque existe), je compte de toute façon réécrire mes quelques helpers, pour corriger des points que j'avais mal compris ou ratés. Il est possible de vérifier le statut d'un daemon géré par `runsvdir` par la commande `sv status `, mais de base, cette commande n'indique que l'âge et l'état du processus: dès lors que le fichier `./run` est en cours d'exécution, le daemon est considéré comme en ligne, ce qui est évidemment faux, puisqu'il peut très bien simplement être en train de vérifier l'état de ses dépendances... La parade à ce problème, c'est le fichier `./check` qu'il est possible de mettre dans un dossier de `runsv`: ce fichier permet un traitement afin de vérifier si vraiment le daemon est lancé. Pour être franc, j'étais parti sur une autre solution très bancale à la base, en étant conscient de ses faiblesses, parce que cette information (le fichier `./check`) est pour le moins noyée dans la documentation... Mais du coup, le problème est simplifié drastiquement: `runsv` crée, pour chaque daemon, un dossier supervise, qui contiens (entre autres, il y a aussi des fichiers pipe qui permettent d'émettre des signaux pour le daemon, signaux qu'il est d'ailleurs également possible d'intercepter en écrivant des scripts bien nommés) un fichier `pid`, contenant la valeur de PID du process géré par `runsv`. Dans le cas le plus simple, il suffit donc d'avoir un fichier `./check` dans ce goût la: ```sh #!/bin/sh . ./conf test "${SV_CMD:?"SV_CMD not defined"}" != "$(ps -ocomm --pid $(cat supervise/pid))" && exit 1 exit 0 ``` Ce fichier naïf dépend du fait que la variable `SV_CMD` soit définie, et correctement, mais d'un autre côté, je vois mal comment faire autrement, surtout si l'on veut éviter de ré-implémenter tous les scripts pour chaque service. Si le daemon doit créer un socket, en plus, il est possible de vérifier que le socket est bien créé avec des commandes telles que `test`, `find`, ou autres. ## Admettons, mais tout reste déclaratif, la, ce ne sont que des scripts... Pas forcément, on pourrait les implémenter en prolog, ces fichiers... Je plaisante (quoique, techniquement, ce serait faisable...). Jusqu'ici, j'ai toujours eu des implémentations spécifiques pour chaque fichier `./run`, mais plus le temps passe, et plus ces implémentations ne sont en fait qu'une suite de fonctions qui sortent si un test n'est pas rempli... Rien n'empêcherait en théorie de définir un certain nombre de variables pré-déterminées dans un fichier `./conf`, qui serait sourcé par `./run`, et gérer ainsi de manière déclarative (c'est la fonctionnalité de systemd que j'ai toujours regardé avec envie, vraiment! Mais à mes yeux elle ne compense pas points que je n'apprécie pas.) les dépendances, sans exécuter les processus à l'aveugle. Compte tenu de mes besoins et connaissances actuels, je compte surtout implémenter ces quelques pré-requis: * vérifier/attendre qu'un daemon est «up»; * vérifier/attendre qu'un socket existe; * vérifier/attendre qu'une machine soit contactable; Une implémentation sans trop de prise de tête ressemblerait à ceci: ```sh #!/bin/sh for daemon in ${CHECK_DAEMON} do if test ! sv check $daemon then exit 0 fi done for socket in ${CHECK_SOCKET} do if test ! -S $socket then exit 0 fi done for target in ${CHECK_REMOTE} do #have to silent ping manually, netcat or nmap may avoid that if test ! ping -q -c $PING_COUNT $target 2>/dev/null then exit 0 fi done exec chpst -u "${USER:-root}:${GROUP:-root}" $SV_CMD $SV_ARGS ``` Pour être franc, je n'ai eu cette idée qu'aujourd'hui, je vais sûrement expérimenter dessus dans les prochains jours... En pratique, il faudrait ajouter pas mal de chaînage de commandes, et de la construction d'arguments dynamique sur l'exécution, le résultat final risque de prendre "quelques" lignes de plus si on veut avoir un truc avec une souplesse maximale. Dans les connaissances que je n'ai pas et les besoins que j'ignore avoir, il y a clairement les cgroups et SElinux, et je sais que systemd gère déjà tout ça nativement, inutile de me le rappeler.

AltStyle によって変換されたページ (->オリジナル) /