Debian : utilisation des cgroups sur un serveur web mutualisé

Les cgroups - groupes de contrôle - ont pour but de contrôler les ressources systèmes utilisées par un ou plusieurs processus. Les processus sous contrôle sont affectés dans des groupes sur lequels agissent des contrôleurs de ressources. Chaque contrôleur gère un type de ressources :
- cpuset : allocation CPU (numéro CPU/core CPU).
- cpuacct : consommation CPU (nombre de cycles).
- memory : contrôle de la mémoire vive et de la mémoire swap.
- devices : contrôle l’accès aux périphériques.
- blkio : contrôle l’accès aux périphériques de type bloc (ex disque dur).
- _netcls : contrôle l’accès aux périphériques réseau.
Par défaut, la configuration s’effectue manuellement, ce qui est fastidieux et à réserver en cas d’état de saturation des ressources. Deux démons peuvent ainsi prendre la main pour automatiser la gestion :
- cgconfig : qui créée la hiérarchie des cgroups.
- cgrulesengd : qui classe les processus dans les cgroups en fonction de régles spécifiques.
Dans le cadre d’un serveur web, les cgroups sont utiles pour prioritiser les processus du serveur web, de MySQL, PHP (ou tout autre interpréteur), afin de garantir une réactivité du serveur. Ils peuvent également restreindre les ressources CPU de chaque utilisateur afin de garantir un partage équitable des ressources CPU entre chaque processus utilisateur (php, cron, ssh) et éviter une consommation anormale des ressources mémoire et bande passante.
Mon article détaille ainsi la mise en place d’un serveur web mutualisé sous cgroups avec les objectifs suivants :
- garantir la priorité de toutes les tâches système.
- limiter les ressources CPU de toutes les tâches des utilisateurs.
- limiter l’utilisation mémoire vive des tâches utilisateurs et réserver par la même de la RAM pour le système.
- interdire l’utilisation de la mémoire swap pour les tâches utilisateurs.
Au niveau logiciel, la configuration est la suivante :
- distribution Debian 6 Squeeze.
- serveur web Nginx.
- PHP 5.3.9 en configuration FPM (packages fournis par dotdeb).
- accès SSH chrooté pour chaque utilisateur.
Chaque utilisateur (client1, client2, etc …) possède son pool de processus PHP attribué, processus qui s’exécutent donc sous son identité. L’ensemble des utilisateurs appartient au groupe secondaire clients. Ainsi, les cgroups vont s’appliquer sur ce groupe secondaire clients. Quant à Nginx, il fait partie automatiquement du cgroup par défaut ; il sera traité et prioritisé comme toute autre tâche système.
Compilation du noyau avec support cgroups
Le noyau fourni avec squeeze n’étant pas configuré pour la gestion des cgroups, il convient d’en compiler un spécifiquement, en veillant à ce que les options suivantes soient identiques. Je précise bien ne pas compiler en module mais en statique :
root@skinner:~# grep -e CONNECTOR -e PROC_EVENTS -e CGROUP /boot/config-2.6.32-bhuisgen
CONFIG_CGROUPS=y
CONFIG_CGROUP_NS=y
CONFIG_CGROUP_FREEZER=y
CONFIG_CGROUP_DEVICE=y
CONFIG_CGROUP_CPUACCT=y
CONFIG_CGROUP_MEM_RES_CTLR=y
CONFIG_CGROUP_MEM_RES_CTLR_SWAP=y
CONFIG_CGROUP_SCHED=y
CONFIG_NET_CLS_CGROUP=y
CONFIG_CONNECTOR=y
CONFIG_PROC_EVENTS=y
Pour la procédure de compilation noyau sous Debian, je vous renvoie à mon article précédent. J’ai également mis en ligne le noyau utilisé pour ce serveur, vous pouvez le télécharger ici.
Une fois le serveur redémarré avec le nouveau kernel, vous pouvez vérifier les contrôleurs de ressources disponibles :
root@skinner:/etc# cat /proc/cgroups
#subsys_name hierarchy num_cgroups enabled
cpuset 0 1 1
ns 0 1 1
cpu 1 3 1
cpuacct 2 2 1
memory 4 3 1
devices 3 2 1
freezer 0 1 1
net_cls 0 1 1
Il reste à installer les démons userland pour gérer les cgroups :
root@skinner:~# apt-get install libcgroup cgroup-bin
Configuration des cgroups
root@skinner:~# more /etc/cgconfig.conf
mount {
cpu = /mnt/cgroups/cpu;
cpuacct = /mnt/cgroups/cpuacct;
devices = /mnt/cgroups/devices;
memory = /mnt/cgroups/memory;
}
group clients {
cpu {
cpu.shares = 512;
}
memory {
memory.memsw.limit_in_bytes = 8G;
memory.limit_in_bytes = 8G;
}
}
root@skinner:~# more /etc/cgrules.conf
# /etc/cgrules.conf
@clients cpu,memory clients
Création d’un compte utilisateur
root@skinner:~# groupadd -g 2000 clients
root@skinner:~# groupadd -g 2001 client1
root@skinner:~# useradd -m client1 -p pa$$word -u 2001 -g 2001 -G clients -s /bin/false
root@skinner:~# mkdir -p /home/client1/www/{html,lib,logs,tmp}
root@skinner:~# chmod 755 /home/client1
root@skinner:~# chown -R root:root /home/client1
root@skinner:~# chown client1:client1/home/client1/www/html
root@skinner:~# chown client1:client1 /home/client1/www/lib
root@skinner:~# chown client1:client1 /home/client1/www/tmp
root@skinner:~# id client1
uid=2001(client1) gid=2001(client1) groups=2001(client1),2000(clients)
Configuration Nginx / PHP
root@skinner:~# more /etc/nginx/sites-available/client1
server {
listen 192.168.1.173:80;
server_name client1.my.domain *.client1.my.domain;
root /home/client1/www/html/;
access_log /home/client1/www/logs/access.log main;
error_log /home/client1/www/logs/error.log;
index index.html index.php;
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-client1.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /home/client1/www/html$fastcgi_script_name;
include fastcgi_params;
}
}
root@skinner:~# more /etc/php5/fpm/pool.d/client1.conf
[client1]
listen = /var/run/php-$pool.sock
listen.backlog = -1
listen.owner = www-data
listen.group = www-data
listen.mode = 0600
user = $pool
group = $pool
pm = static
pm.max_children = 4
pm.max_requests = 500
access.log = /home/$pool/www/logs/php-$pool.access.log
access.format = %R - %u %t "%m %r%Q%q" %s %f %{mili}d %{kilo}M %C%%
request_terminate_timeout = 35
request_slowlog_timeout = 15
slowlog = /home/$pool/www/logs/php-$pool.log.slow
php_admin_value[disable_functions] = dl, exec, highlight_file, fsockopen, passthru, pcntl_exec, phpinfo, pclose, popen, system, shell_exec, set_time_limit, show_source
php_admin_value[memory_limit] = 64M
php_admin_value[open_basedir] = /home/$pool/www/html:/home/$pool/www/lib:/home/$pool/www/tmp:/home/$pool/www/awstats
php_admin_value[session.save_path] = /var/lib/php5
php_admin_value[upload_tmp_dir] = /home/$pool/www/tmp
Lancement des démons
root@skinner:~# /etc/init.d/cgconfig start
root@skinner:~# /etc/init.d/cgred start
root@skinner:~# /etc/init.d/php-fpm
On peut à présent vérifier que les processus PHP de client1 sont bien attribués à notre cgroup clients au niveau des contrôleurs de ressources CPU et mémoire (RAM + swap) :
root@skinner:~# ps -u client1
PID TTY TIME CMD
19507 ? 00:00:00 php5-fpm
19508 ? 00:00:00 php5-fpm
19509 ? 00:00:00 php5-fpm
19510 ? 00:00:00 php5-fpm
root@skinner:~# cat /proc/19507/cgroup
8:memory:/clients
7:devices:/sysdefault
6:cpuacct:/sysdefault
5:cpu:/clients
Il est également possible de manipuler à chaud les cgroups en parcourant sa hiérarchie, montée sous Debian dans /mnt/cgroups :
root@skinner~# cd /mnt/cgroups
root@skinner:/mnt/cgroups# ls -ltotal 0
drwxr-xr-x 4 root root 0 Jan 16 11:45 cpu
drwxr-xr-x 3 root root 0 Jan 16 11:45 cpuacct
drwxr-xr-x 3 root root 0 Jan 16 11:45 devices
drwxr-xr-x 4 root root 0 Jan 16 11:45 memory
root@skinner:/mnt/cgroups# ls -l cpu
total 0
-r--r--r-- 1 root root 0 Jan 16 11:45 cgroup.procs
drwxrwxr-x 2 root root 0 Jan 16 11:45 clients
-rw-r--r-- 1 root root 0 Jan 16 11:45 cpu.shares
-rw-r--r-- 1 root root 0 Jan 16 11:45 notify_on_release
-rw-r--r-- 1 root root 0 Jan 16 11:45 release_agent
drwxrwxr-x 2 root root 0 Jan 16 11:45 sysdefault
-rw-r--r-- 1 root root 0 Jan 16 11:45 tasks
Il suffit ensuite de faire les commandes echo appropriées selon les besoins.
Les cgroups représentent donc la brique logicielle indispensable à tout serveur afin de partager et limiter les ressources.