Apprendre à optimiser la gestion des ressources système avec ulimit

Quentin Busuttil

Vous le saviez peut-être, Linux, comme d'autres systèmes, attribue des limites par défaut aux processus et aux utilisateurs. Cela permet bien évidemment de s'assurer que l'OS reste disponible pour tous et n'en vienne pas à crasher. Les réglages par défaut sont assez prudents, et il y a de nombreuses situations où il peut être intéressant d'adapter cela à nos propres besoins. Partons à l'exploration du pourquoi et du comment !

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum. Commentez

- Titre : Apprendre à optimiser la gestion des ressources système avec ulimit

- Auteur : Quentin Busuttil

- Parution : 22 février 2017

- Mise à jour : 27 octobre 2017

- Licence : Copyright ® 2017 Quentin Busuttil. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Illustration par l'exemple

- II. Les différents paramètres

- III. Poser un diagnostic

- III-A. Processus

- III-B. Descripteur de fichier

- III-C. Usage mémoire

- IV. Modifier la configuration

- V. Note de la rédaction de Developpez.com

I - Illustration par l'exemple

Il y a quelques semaines, alors que j'étais en train de programmer la gestion de l'encodage des médias pour Buzeo, j'ai fait face à une erreur inhabituelle : EAGAIN. Je creuse quelque peu et trouve la définition suivante :

A temporary resource shortage made an operation impossible. fork can return this error. It indicates that the shortage is expected to pass, so your program can try the call again later and it may succeed. It is probably a good idea to delay for a few seconds before trying it again, to allow time for other processes to release scarce resources. Gnu.org

En gros, cette erreur fait référence à l'épuisement d'une ressource du système, il ne peut donc pas l'allouer au nouveau processus. C'est là que commence l'enquête. L'encodage est une opération cpu-intensive. Cependant mon programme monitore la charge processeur et tant que celui-ci n'est pas à pleine charge, de nouvelles tâches sont lancées. Quant à la RAM, le système en a encore largement à revendre.

C'est là qu'intervient ulimit. Pour des raisons évidentes de stabilité du système, il permet d'empêcher un utilisateur ou process de s'accaparer toutes les ressources. Cependant, lorsque cet usage est légitime, l'inconvénient est qu'il peut brider son fonctionnement ou provoquer des erreurs, alors même que le système n'est pas exploité à son maximum.

II - Les différents paramètres

J'ai parlé ci-dessus de CPU et de RAM, cependant ulimit permet de définir bien d'autres limites que celles-ci. D'ailleurs, on entend souvent parler de la limite des max open files qui nécessite d'être augmentée pour le bon fonctionnement de certains logiciels (mongoDB, par exemple).

Sans surprise, la commande qui vous permet de contrôler tout ça s'appelle ulimit. L'option -a permet d'afficher les réglages pour l'utilisateur courant.

1. Buzut$ ulimit -a

2. core file size          (blocks, -c) 0

3. data seg size           (kbytes, -d) unlimited

4. scheduling priority             (-e) 0

5. file size               (blocks, -f) unlimited

6. pending signals                 (-i) 15584

7. max locked memory       (kbytes, -l) 64

8. max memory size         (kbytes, -m) unlimited

9. open files                      (-n) 1024

10. pipe size            (512 bytes, -p) 8

11. POSIX message queues     (bytes, -q) 819200

12. real-time priority              (-r) 0

13. stack size              (kbytes, -s) 8192

14. cpu time               (seconds, -t) unlimited

15. max user processes              (-u) 15584

16. virtual memory          (kbytes, -v) unlimited

17. file locks                      (-x) unlimited

Il s'agit ci-dessus des réglages par défaut d'une version Ubuntu 14.04. Voyons dans le détail les implications de ces différents paramètres.

core file size

Nombre maximum de core dumps. Il s'agit d'un snapshot du système comprenant l'état de la RAM, le context switch et les registres processeur. C'est à utiliser à des fins de débogage seulement et vous n'en avez habituellement pas besoin. À laisser à 0 donc, pour ne pas courir le risque de rapidement saturer votre espace disque.

data seg size

Taille maximum du segment de données d'un programme. Il s'agit d'une portion d'espace d'adressage virtuel d'un programme contenant les différentes variables de ce dernier.

scheduling priority

Priorité maximum que l'on peut attribuer à un processus. La gestion des priorités, ou ordonnancement, définit l'ordre dans lequel les processus auront accès au processeur (utile lorsque l'on a besoin de faire plusieurs tâches à la fois).

file size

Taille maximum des fichiers écrits par le shell et ses processus enfants.

pending signal

Un poil complexe, on spécifie ici la limite du nombre de signaux qui peuvent être en attente pour un process donné (tels que sigterm, sigkill, sigstop…) lorsque ce dernier est en attente d'I/O. Lorsqu'un process est dans cet état, il n'est pas possible de l'interrompre (le kernet attend que le process « se réveille »), c'est pour cela qu'il y a une file d'attente. Le manuel de sigpending vous en apprendra plus au besoin.

max locked memory

Taille maximum de mémoire qu'un processus peut « verrouiller » en RAM afin de prévenir que ce dernier ne swap (échange) pas.

max memory size

Taille maximale qu'un processus peut occuper en RAM (appelé resident set size). À ne pas confondre avec la taille totale de mémoire virtuelle que le processus possède.

open files

Nombre de descripteurs de fichier qui peuvent être ouverts concurremment.

POSIX message queues

Nombre maximum (en bytes) dans la file d'attente de message POSIX. N'hésitez pas à consulter le man de la message queue pour plus de détails.

real-time priority

C'est la priorité maximale pour un processus, telle qu'il ne peut être interrompu (c'est pour cela qu'on l'appelle real time).

stack size

Taille maximum de la stack size. Il s'agit d'une région réservée en mémoire (à ne pas confondre avec la heap memory) utilisée pour stocker l'emplacement des appels à fonction. Cependant, le développeur peut choisir d'y stocker d'autres informations.

cpu time

Temps processeur maximum en secondes.

max user processes

Il s'agit du nombre maximal de processus qu'un utilisateur peut démarrer ou forker.

virtual memory

Taille maximale de mémoire virtuelle disponible pour un utilisateur donné. Il s'agit de la somme de la mémoire physique et de la mémoire de masse (les disques durs).

file locks

Nombre maximum de de fichiers qu'un processus peut verrouiller. Le file locking est un mécanisme permettant de restreindre l'accès d'un fichier à seulement un utilisateur ou processus à un instant donné.

III - Poser un diagnostic

Il est bien souvent délicat de savoir exactement dans quelle mesure on utilise les ressources. Bien sûr, top peut nous donner une idée. Cela dit, le plupart du temps, nous avons à faire à des questions de file descriptors ou de nombre de processus. Je vous explique donc comment mesurer ce que vous utilisez.

III-A - Processus

Commençons par le plus facile : le nombre de processus. Il s'agit d'un petit one-liner :

1. # ps h -Led -o user | sort | uniq -c | sort -n

2. 1 Debian-exim

3. 1 messagebus

4. 1 ntpd

5. 4 syslog

6. 5 newrelic

7. 14 www-data

8. 34 gogs

9. 34 mysql

10. 94 root

On voit donc l'ensemble de nos utilisateurs et le nombre de processus que chacun d'entre eux a lancé. Easy !

III-B - Descripteur de fichier

Pour les file descriptors, c'est un peu plus long. Mais ne vous en faites pas, ce n'est pas non plus insurmontable. Nous avons vu dans la commande précédente que mysql utilise 34 processus. Nous allons trouver combien cet utilisateur possède de file descriptors.

Premièrement, on trouve son pid à l'aide la commande ps :

# ps aux | grep mysql
root       853  0.0  0.0  18148   756 ?        S    Apr15   0:00 /bin/bash /usr/bin/mysqld_safe
mysql     1085  0.5 20.4 941668 412232 ?       Sl   Apr15 220:17 /usr/sbin/mysqld --basedir=/usr --datadir=/var/lib/mysql --plugin-dir=/usr/lib/mysql/plugin --user=mysql --skip-log-error --pid-file=/var/run/mysqld/mysqld.pid --socket=/var/run/mysqld/mysqld.sock --port=3306

L'utilisateur mysql possède le pid 1085. On va maintenant compter le nombre d'open files grâce à la commande lsof :

# lsof -p 1085 | wc -l
410

Voilà, on sait que notre mysql possède 410 file descriptors ouverts.

III-C - Usage mémoire

Comme nous l'avons vu, il existe plusieurs types de mémoires. Sur notre machine, cela se traduit par l'usage de la RAM ou du SWAP.

Plusieurs commandes permettent d'obtenir d'un coup d'œil la mémoire globalement consommée par la machine et les différents processus qui occupent la RAM. J'utilise pour cela, la plupart du temps htop et glances. Ils ne sont pas installés par défaut sur les distributions, mais répondent au même usage que le vénérable top… en mieux.

Glances présente l'avantage d'afficher plus d'infos de base, c'est un peu le couteau suisse, car on a d'un clin d'œil (at a glance, d'un coup d'œil) toutes les infos du système : CPU, load, RAM, SWAP, I/O disques, remplissage des disques, etc. De son côté, htop présente l'avantage de permettre de filtrer les infos selon divers critères (trier par ordre de consommation mémoire…).

Malgré ces outils géniaux, il nous manque la possibilité de clairement savoir qui occupe tout notre SWAP. C'est sans compter la magie de /proc/*/status et ce magnifique petit one-liner qui vous servira les squatteurs sur un plateau d'argent, rangés par taille s'il vous plaît !

1. for file in /proc/*/status ; do awk '/VmSwap|Name/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 2 -n -r

2. bundle 101244 kB

3. mysqld 70420 kB

4. java 22764 kB

5. fail2ban-server 6816 kB

6. nginx 4624 kB

7. nginx 4588 kB

8. log2gelf 3980 kB

9. bundle 2880 kB

10. redis-server 2148 kB

11. sensu-client 1568 kB

12. (sd-pam) 1132 kB

13. nginx 968 kB

14. postgres 924 kB

15. bundle 880 kB

16. dhclient 856 kB

17. …

IV - Modifier la configuration

Il est grand temps de tuner ces réglages. Vous avez dû remarquer en faisant ulimit -a qu'à chaque ligne correspondait une lettre. En fait, vous pouvez l'utiliser pour modifier dynamiquement la configuration. Par exemple, si je veux augmenter le nombre d'open files (n), à 64 000, il me suffit d'utiliser la commande ulimit -n 64000.

Cette manière présente tout de même quelques limites… La première : elle n'est valable que dans le shell courant ; la seconde : elle ne survit pas à un redémarrage. Pour configurer les limites de manière persistante et pour différents utilisateurs, il va falloir se diriger vers un - ou plutôt plusieurs - fichiers de configuration.

Le premier fichier, /etc/security/limits.conf, contient beaucoup de choses, mais c'est surtout la fin qui nous intéresse :

#*               soft    core            0
#root            hard    core            100000
#*               hard    rss             10000
#@student        hard    nproc           20
#@faculty        soft    nproc           20
#@faculty        hard    nproc           50
#ftp             hard    nproc           0
#ftp             -       chroot          /ftp
#@student        -       maxlogins       4

# je rajoute ici une limite à 64 k pour les file descriptors pour l'utilisateur mongod (soft et hard)
mongod          soft    nofile          64000
mongod          hard    nofile          64000

Comme vous le voyez dans l'exemple, vous pouvez désigner tous les utilisateurs avec une wildcard. Par ailleurs, je parle ici de limites soft et hard. La différence est que la limite soft peut être modifiée à la hausse par l'utilisateur ou le processus, tandis qu'ils ne pourront pas dépasser la limite hard. Le soft est une valeur par défaut en quelque sorte. Ici je fixe la même valeur pour les deux, ce qui signifie que Mongo aura par défaut 64 000 file descriptors et qu'il ne pourra aller au-delà.

Enfin, vous devrez également modifier deux fichiers :

- /etc/pam.d/common-session ;

- /etc/pam.d/common-session-noninteractive.

Il vous faudra ajouter à chacun d'entre eux la ligne suivante :

session required pam_limits.so

Pour la petite histoire, mon erreur venait de la stack size trop petite. J'espère que cet article vous aura éclairé.

V - Note de la rédaction de Developpez.com

Nous tenons à remercier Quentin Busuttil qui nous a aimablement autorisés à publier son tutoriel : Optimiser la gestion des ressources système avec ulimit. Nous remercions également Jacques_jean pour la relecture orthographique.