Sommaire
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:
- le noyau lance
runit-init
, qui se remplace lui-même parrunit
-
runit
lance le script/etc/runit/1
-
runit
lance le script/etc/runit/2
, qui lancerunsvdir
et ne doit pas se fermer (c'est l'étape de fonctionnement normal de la machine) -
runsvdir
examine le dossier qui lui est passé en paramètre et, pour chaque sous-dossier ou lien symbolique (max 1000) lance une instance derunsv
. Si l'une de ces instances se ferme, il la relance, et si le dossier ou le lien est supprimé, il la ferme. -
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
- quand
/etc/runit/2
se ferme,runit
lance le script/etc/runit/3
- 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:
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 <nom du daemon>
, 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:
#!/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:
#!/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.
# Netcat?
Posté par Benoît Sibaud (site web personnel) . Évalué à 10.
[^] # Re: Netcat?
Posté par freem . Évalué à 2.
Maintenant que tu le dis, je me demande pourquoi j'ai pas pensé à netcat… après tout, le but était juste d'écouter un port pour sortir le résultat dans stdout… je me sens encore plus bête, du coup.
[^] # Re: Netcat?
Posté par Benoît Sibaud (site web personnel) . Évalué à 5.
Avec bash aussi:
[^] # Re: Netcat?
Posté par freem . Évalué à 0.
hum, entre un code C standard de 50 lignes et une ligne de bash "illisible" (de mon point de vue, ce concentré de caractères sans espaces est pénible à lire du moins) et qui risque de se retourner contre moi, je préfère les 50 lignes de C standard.
En plus, ici netcat est toujours nécessaire in fine.
Aussi, j'ai appris à me méfier d'echo, et une pratique que j'essaie de faire au max est de toujours utiliser printf, pour éviter les surprises.
Ça doit être lié au fait que je suis un dev qui joue aux admins, et pas un vrai admin (au moins, j'en suis conscient).
Par contre, je bloque sur la construction "foo & bar". Le '&' remplace le ';' c'est ça? Si oui, pourquoi ne pas mettre sur 2 lignes, que ce soit lisible?
[^] # Re: Netcat?
Posté par Benoît Sibaud (site web personnel) . Évalué à 4. Dernière modification le 10 avril 2019 à 23:31.
foo;bar => foo est exécuté, puis bar est exécuté une fois que foo est terminé
foo&bar => foo est exécuté, bar est exécuté
foo&&bar => foo est exécuté et si ça se passe bien (foo a retourné 0) bar aussi
Donc notre cas il faut quand même que le netcat soit en écoute avant de pouvoir recevoir un message du réseau. Message qui est ensuite envoyé.
[^] # Re: Netcat?
Posté par freem . Évalué à 1.
Certes, mais
me semble plus lisible et fait ce que fait
foo & bar
non?[^] # Re: Netcat?
Posté par BAud (site web personnel) . Évalué à 4.
oui, mais tu y perds la beauté des one-liners du shell :-)
[^] # Re: Netcat?
Posté par Michaël (site web personnel) . Évalué à 9. Dernière modification le 10 avril 2019 à 23:59.
Pour bien connaître (ou avoir connu) les deux, chacun se défend bien en termes d'illisibilité. Pour améliorer les choses quand on programme le shell, il faut utiliser des fonctions dont les noms sont explicatifs, je fais la traduction:
Et le programme s'écrit
Cela démarre en arrière-plan (
&
) un serveur qui écrit tous les messages reçus sur UDP puis envoie un message sur ce serveur.La “bonne pratique” est de n'utiliser echo que sur un texte constant (c'est le cas ici) qui de plus ne commence pas par un tiret. Pour tout le reste on utilise
printf
en utilisant une chaîne de format constante.On a le droit de bien connaître le shell même sans être admin. :-)
[^] # Re: Netcat?
Posté par freem . Évalué à 4.
Je crains que ça ne vaille pour tous les langages, ça :D
Le truc, c'est que j'essaie de cultiver des idiomes, et l'usage de printf me semble en être un bon: quand ça pose un problème de performance avéré alors j'utilise autre chose, mais si ce n'est pas le cas, une fonction qui as un comportement bien définit me semble préférable. J'applique la même politique en C et C++, quitte a ne pas utiliser certains conforts, si ça peut rendre mon code plus fiable et portable. Me suis fait avoir quelques fois déjà sur des flous de standard, alors maintenant, je me méfie.
Je fais mon maximum pour être bon dans les deux domaines, mais je n'ai pas la prétention de l'être, ni dans ma spécialité, et encore moins dans ce que je sais ne pas être ma spécialité.
Il me reste tant à apprendre, et ce n'est pas en me croyant bon que je le deviendrais.
[^] # Re: Netcat?
Posté par Michaël (site web personnel) . Évalué à 3.
C'est exact, personnellement je préfère aussi utiliser
printf
systématiquement. echo n'a presque que des inconvénients… pourtant on l'utilise et le rencontre encore souvent.Ce que je voulais dire est que le fait de programmer le shell n'a pas forcément à voir avec celui d'être admin! Les sources qui m'ont fait faire beaucoup de progrès sont la lecture des divers scripts shell qu'on trouve dans FreeBSD notamment.
[^] # Re: Netcat?
Posté par Eh_Dis_Mwan . Évalué à 1.
Ah rien qu'en disant ça, tu avoue ne pas être admin (ou bossant dans l'exploit /prod) :
Qu'un admin dise "il connait bien quelque chose", c'est en général astreinte sur astreinte, donc non, aucun admin ne connait bien quoique ce soit
# Type=forking
Posté par kna . Évalué à 5.
Pas forcément, voir man systemd.service
[^] # Re: Type=forking
Posté par freem . Évalué à 2.
Intéressant. Du coup, comment ça marche pour relancer le daemon s'il se ferme? Je veux dire, me semble que coup de forker sers à se détacher du parent… c'est rattrapé par le PID1 qui le refourgue à la partie du système qui gère les daemons? Ou peut-être que c'est le PID qui le fait, et donc ça ne pose pas de problème?
[^] # Re: Type=forking
Posté par wismerhill . Évalué à 2.
C'est grace aux cgroup.
Systemd crée (par défaut) un cgroup pour chaque service, il sait donc quels sont tous les processus qui ont été lancé par le service. Si ce dernier ne s'arrête pas correctement (typiquement grace à la commande ExecStop), tu peux faire un systemctl kill qui enverra un signal à tous les processus du cgroup associé au service.
[^] # Re: Type=forking
Posté par freem . Évalué à 2.
Autrement dit, c'est lié à ce passage là de moi-même:
En gros, me reste à savoir comment faire ça en shell (si je veux vraiment pouvoir créer une alternative).
Bon, la ou je suis curieux, c'est sur comment nosh s'y prend, puisqu'il prétend être capable de porter les units systemd, et fonctionner sur du BSD… les BSD implémentent-ils des mécanismes de ce type?
Si oui, existe-t-il un moyen portable, que ce soit en shell ou en C, d'être équi-fonctionnel?
# Void Linux
Posté par Arthur Accroc . Évalué à 2.
Sur quelle distribution utilises-tu runit ?
Il me semble que Void Linux utilise runit nativement.
Cette distribution peut donc être un bon choix si on veut utiliser runit (on peut supposer qu’il est déjà configuré, au moins en bonne partie).
À défaut, ça peut toujours être intéressant de l’installer sur une machine virtuelle, pour regarder comment runit est configuré dessus. Ou même carrément d’en récupérer la configuration et les unités runit.
(Tout ça, à moins que tu n’utilises déjà Void Linux et ne parles là que de ce qu’il reste à configurer ; dans ce cas Void Linux serait moins fini que ce que j’aurais pensé.)
« Le fascisme c’est la gangrène, à Santiago comme à Paris. » — Renaud, Hexagone
[^] # Re: Void Linux
Posté par freem . Évalué à 2.
Debian (avec le paquet runit-sysv, du coup je n'utilise pas vraiment le système d'init, et pour l'anecdote, il existe un paquet runit-systemd, pour utiliser la gestion des daemons de runit au-dessus de systemd, comme quoi systemd n'est pas si monolithique que ça. On est vendredi, c'est permis :D), mais je l'utilise aussi sur Devuan et Void.
Enfin, j'utilise runsvdir, parce qu'en vrai, à part sur void, je n'utilise pas l'init réellement, bien que je me souvienne avoir tenté de porter une de mes Debian perso sur runit.
J'avais réussi, mais il restait pas mal de trucs sur lesquels je me basais sur les usines à gaz de scripts init.d de Debian et leurs dépendances spaghetti, vu que je n'avais pas (et n'ai toujours pas, en fait) assez de connaissances sur udev, principalement.
Void utilise runit comme système d'initialisation et de gestion des daemons par défaut, oui.
J'aime beaucoup Void pour plusieurs raisons (déjà, elle implémente runit de base, et de manière minimale ce qui est excellent pour comprendre comment marche le système, contrairement au foutoir debianesque auquel je suis «habitué»… mais en plus, il est possible d'utiliser une variante musl, ce qui séduit vachement mon côté qui veut tout faire différemment des autres), mais elle à plusieurs défauts à mon goût:
grep -r "sv check "
). Il est probable qu'un système qui démarre un serveur NFS se retrouve donc avec une courte phase de "yoyo" pendant laquelle plusieurs des services vont se lancer pour rien. Et NFS, c'est un schéma de dépendances simple, qui n'implique absolument pas que la machine soit capable d'en contacter une autre… auquel cas les choses pourraient être autrement plus chaotique.Tout ceci étant dit, je ne connaît pas tant que ça d'applications qui ont vraiment une forte liste de dépendances. En libre, je ne vois que NFS, et c'est parce que je me suis creusé la tête pour te donner un exemple pas trop mauvais…
Si je parviens à mettre en place un système assez simple et efficace, il n'est pas dit que je ne le partagerai pas avec les gens de Void, bien au contraire.
Il faut garder à l'esprit que c'est une petite distro relativement jeune (11 ans…) et peu connue, peut-être que personne n'a eu le temps, la motivation ou l'idée de proposer un tel mécanisme: après tout, une des idées les plus intéressantes de systemd est justement d'avoir une configuration déclarative, c'est à ma connaissance le 1er à le faire sous linux.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.