Tubes nommés et redirections en parallèle

La ligne de commande est bien faite. Cette phrase est souvent entendue dans la bouche des informaticiens. J’en ai encore vérifié la véracité ses jours derniers.

Ma problèmatique était de constituer des statistiques sur des accès HTTP en consultant un fichier de log. La volumétrie des log étant de l’ordre de plusieurs gigaoctets et je ne disposais que de ma ligne de commande en bash.

Je me suis alors dirigé vers une serie de grep avec les bons motifs de recherche couplé à un bon vieux wc:

grep motif fichier.log | wc -l

Pour compliquer il y avait plusieurs motifs differents à compter et il fallait produire un fichier par jour avec le comptage de chaques motifs.

ACCES_COUNT=`grep -e motif_date -e motif_acces fichier.log | wc -l`

Autant de ligne de ce genre que de motif. L’inconvenient est que l’on parcourt la log autant de fois qu’il y a de motif d’acces pour une date donnée. Dans mon cas j’avais 15 motifs soit 1h30 pour obtenir les statistiques pour un jour donné.

Or, une fois que l’on est sur la bonne date dans le fichier de log c’est dommage de le reparcourir depuis le debut pour le motif d’accès suivant. D’où l’idée d’externaliser le parcours du fichier à la recherche de la bonne date puis de dispatcher la ligne aux grep qui compte les motifs d’accès.

tee

On a donc envie de faire un premier grep dont on va diriger la sortie sur d’autres grep en arborscence. La première pièce du puzzle est la commande tee. Voici l’idée :

grep -e motif_date fichier.log | tee -a entree_proc_grep1 entree_proc_grep2 … > /dev/null

Il manque la seconde piece qui est celle qui permet de definir un tube entre le premier grep et les autres qui doivent s’exécuter en parallèle. C’est là que les tube nommés interviennent.

named pipe

La commande mkfifo crée un tube nommé que l’on donne en entrée au grep de motif d’accès. Ces grep seront alors bloqués en attente de lire des donnés dans leurs tubes respectifs.

mkfifo entree_proc_grep1

grep -e motif_acces < entree_proc_grep1 | wc -l > fichier_temp1` &

PID_LIST+=” $!”

Autant de lignes de ce genre qu’il y a de motif d’accès à compter. Remarquons qu’à ce stade on à seulement créé des processus pour faire les grep de comptage et qu’ils sont tous bloqués en attentes sur leur tube. On alimente ensuite les tubes. Voici l’ordre

  1. créer les tube
  2. lancer en arrière plan les grep prenant les tubes en entrée
  3. lancer le grep sur la date pipé avec le tee vers les tubes

Subtilites

On ne peut plus récupérer la valeur des wc car c’est un sous shell qui l’exécute. On passe par un fichier temporaire.

On stocke la liste des pid des grep de comptage car le shell courant qui est leur parent doit attendre qu’ils soient tous finis avant d’aller lire les resultats dans les fichiers temporaires.

wait $PID_LIST

cat fichier_temp1

cat fichier_temp2

….

Dans mon cas particulier je suis passé de 1h30 de traitement pour avoir les statistiques d’un jour donné à seulement 3 minutes !

Finitions

Pour être propre il faut terminer en supprimant les fichiers temporaires (de comptage et les tubes).

Pour pouvoir lancer plusieurs instance du script sur des dates differentes en parallèle, ii faut ajouter le pid du script dans les noms des fichiers temporaires ( tubes nommés et comptage).

Dans mon cas particulier il me fallait les statistiques de plusieurs jours: chaque jour ayant sa propre stat. On peut lancer le script manuellement pour chaque jour ou bien intérer sur les jours, ce qui donne a peu près ceci:

for jour in 1 2 3 4

do

mkfifo entree_proc_grep1

grep -e motif_acces < entree_proc_grep1 | wc -l > fichier_temp1` &

PID_LIST+=” $!”

…..

grep -e motif_date fichier.log | tee -a entre_proc_grep1 entree_proc_grep2 … > /dev/null

wait $PID_LIST

cat fichier_temp1 >> fichier_stat_$jour

echo “” >> fichier_stat_$jour

…..

rm entree_proc_grep1

….

done

On remarque encore que pour chaque jour en parcourt l’ensemble de la log d’accès. Or dès le premier passage on a deja passé sur toutes les dates possibles. Une seule passe doit suffire ! L’idée est que le grep sur le motif de la date ne prenne plus le fichier de log en entrée mais un tube nommé…

for jour in 1 2 3 4

do

mkfifo entree_proc_grep1_$jour

grep -e motif_acces < entree_proc_grep1_$jour | wc -l > fichier_temp1_$jour` &

PID_LIST+=” $!”

…..

mkfifo entree_proc_$jour

PROC_JOUR_LIST+=” entree_proc_$jour”

grep -e motif_date entree_proc_$jour| tee -a entre_proc_grep1_$jour entree_proc_grep2_$jour … > /dev/null &

PID_LIST+=” $!”

done

cat fichier_log | tee -a $PROC_JOUR_LIST > /dev/null

wait $PID_LIST

for jour in 1 2 3 4

do

cat fichier_temp1_$jour >> fichier_stat_$jour

echo “” >> fichier_stat_$jour

…..

rm entree_proc_grep1_$jour

….

rm entree_proc_$jour

done

C’est un peu plus compliqué car pour distinguer tous ces tubes nommés on doit ajouter le numero de jour à leur nom.

Eviter ainsi de reparcourir la log depuis le début pour chaque jour permet d’aller plus vite. Dans mon cas particulier pour les statistiques de 3 jours je suis passé de 2mn50 à 0mn45s et pour les statistiques de 14 journées je suis passé de 22mn à 5mn !

Moralité de l’histoire, l’algorithmie alliée à la ligne de commande et la puissance du shell ça fait des étincelles.

Du bonne usage de la commande LS

Sous Linux, le ls est sans doute une des commandes les plus utilisées. En effet une fois arrivé dans un répertoire, qui ne lance pas un ls pour visualiser son contenu ? Cette commande est un peu le coup d’oeil qu’on lance autour pour juger son environnement.

Usage du ls

La pluspart du temps l’utilisateur fait

ls

ou alors pour avoir plus d’information sur les fichiers

ls -l

Cela correspond à un usage normal de la commande ls qui est d’obtenir la liste du contenu d’un répertoire.

Détournement du ls

Malheureusement cela se gâte quand on est dans un répertoire avec de très nombreux fichiers et que l’on veut juste des information sur certains d’entre eux. On utilise alors un motif de complétion en * pour effectuer ce genre de commande:

ls -l toto*

On obtient ainsi la liste des fichiers (i.e.les informations sur) commençant par toto. L’utilisateur a alors l’impression que ls fait une recherche des fichiers commençant par toto. C’est faux. Pour s’en convaincre il suffit d’activer le débug du shell par

set -x

Cela permet de voir ce que le shell exécute, puis lancez la commande:

ls *

On constate que l’expansion de shell remplace l’étoile par tous les fichiers présents dans le répertoire avant d’evaluer la commande ls suivie des paramètres ainsi constitués. Donc a en fait lancé la commande :

ls fichier1 fichier2 fichier3

C’est l’expansion de shell qui fait la soit disant recherche et ls ne fait que donner des informations sur les fichiers trouvés. Comme on ne passe pas de commutateur tel que -l, on n’obtient ni plus ni mpoins confirmation que les fichiers existent bien (travail effectué par l’expansion de shell).

Mauvais usage du ls

De la se servir de ls pour tester l’existense de fichier, il n’y a qu’un pas qu’il ne faut pas franchir. En effet on vu que c’est l’expansion de shell qui fait le boulot. Cela conduit à un effet désagréable quand l’expansion de shell ramène beaucoup trop de fichiers si bien que le shell ne peut exécuter la commande ls avec tant d’argument. Sur un linux récent faire n ls * dans un répertoire avec 100000 fichiers dont le nom fait 20 caractères chacun, conduit à:

ls *
bash: /bin/ls: Liste d'arguments trop longue

Laissons à ls ce qui appartient à ls: donner des informations sur des fichiers (taille, droits etc.). Pour la recherche de fichier il y a find

find -name "toto*"

En mettant entre guillemet le motif du nom de fichier, on empêche l’expansion de shell. On garde une ligne de commande courte. find interprête le meta-caractère étoile pour faire la recherche.

Existence de fichier selon un motif

Si on veut juste vérifier l’existence de fichier ayant un certain motif, on peut faire sur un Linux récent:

find -name "toto*" -print -quit | wc -l

Cela permet de chercher en s’arrêtant sur le premier trouvé puis de compter le nombre de fichier trouvé: soit 0 soit 1.

Si findutils n’est pas au moins en version 4.2.3 sur votre système alors l’option -quit n’est pas disponible. Son absence aura pour effet de vous priver d’un arrêt de la commande au premier fichier trouvé…. et donc obligera de faire une recherche complète.

Si l’on veut récupérer l’information d’existence où non de fichier via un code retour, on pourra avantageusement utiliser grep combiné à un ls comme ceci

if (ls | grep "2014*" 1>/dev/null 2>&1) then echo "trouve"; else echo "pas trouvé"; fi