Linux 3.2 – CFS CPU Bandwidth

Publié par cpb
Jan 07 2012

(English translation here)

Linus Torvalds a publié le noyau Linux 3.2 il y a deux jours. Ce dernier contient comme d’habitude de nombreux ajouts. L’un d’eux a attiré mon attention et j’ai souhaité observer son fonctionnement, il s’agit du contrôleur de consommation CPU pour l’ordonnanceur CFS.

Il existait déjà de nombreux moyens de régler le pourcentage de CPU dont pouvait disposer une tâche par rapport aux autres pour l’ordonnancement temps-partagé (entre autres avec l’appel-système setpriority(), la commande en ligne nice, ou les paramètres du systèmes de fichiers /sys/fs/cgroup/cpu). On pouvait ainsi attribuer facilement 25% du temps processeur à une tâche et 75% à une autre par exemple. Mais jusqu’alors, si la seconde se terminait, la première disposait alors de 100% du temps CPU.

Autrement dit, la “bande passante CPU” d’une tâche seule active était toujours de 100%. Il est à présent possible de modifier ceci afin de lui attribuer une portion plus réduite du temps processeur, même lorsqu’elle est seule.

Quel peut-être l’intérêt de réduire la consommation CPU instantanée d’un processus ? Pour les systèmes courants, il faut bien l’avouer, cet intérêt est réduit. Mais il en va autrement pour certains gros serveurs où l’on facture le temps processeur consommé mais également la puissance CPU mise à disposition instantanément. Dans ces environnements il est très intéressant de limiter la puissance CPU dont disposera un processus quelle que soit la charge globale du système.

Un autre intérêt de cette limitation est de réguler la disponibilité du CPU en évitant les pointes de puissance et les brusques ralentissements. On peut imaginer encore un environnement hôte sur lequel on est amené à faire fonctionner simultanément plusieurs – disons 4 – émulateurs (de type Qemu par exemple). En limitant à 25% la puissance CPU accordée à chacun d’entre-eux, le comportement d’un système virtuel ne dépendra pas de la présence ou non d’un autre émulateur sur l’hôte.

Mise en oeuvre

Il existe une nouvelle option de compilation que l’on trouve dans le menu de configuration du kernel dans l’arborescence suivante : “General Setup” – “Control Group support” – “Group CPU scheduler” – “CPU bandwith provisioning for FAIR_GROUP_SCHED“. Il est nécessaire d’activer toutes les options de ce chemin.

Voici un fichier de configuration générique Linux 3.2 pour PC (provenant d’une distribution Ubuntu 11.10) avec l’option “CPU bandwidth provisioning” activée.

Après compilation et redémarrage sur le nouveau kernel, nous observons la présence d’un nouveau fichier  /proc/sys/kernel/sched_cfs_bandwidth_slice_us qui indique la durée des tranches de temps employées lorsqu’une tâche bénéficie d’une puissance CPU reposant sur plusieurs processeurs. Par défaut cette valeur est de 5 millisecondes.

# cat /proc/sys/kernel/sched_cfs_bandwidth_slice_us
5000
#

Pour accéder au paramétrage de la puissance CPU accordée à un groupe ou à une tâche nous devons passer par le système de fichiers cgroup :

# mount none /sys/fs/cgroup -t tmpfs
# mkdir /sys/fs/cgroup/cpu
# mount none /sys/fs/cgroup/cpu/ -t cgroup -o cpu
# ls /sys/fs/cgroup/cpu/
cgroup.clone_children  cgroup.procs       cpu.cfs_quota_us  cpu.rt_runtime_us  cpu.stat           release_agent
cgroup.event_control   cpu.cfs_period_us  cpu.rt_period_us  cpu.shares         notify_on_release  tasks
#

Essais

Tâche unique

Pour verifier la puissance CPU accordée à une tâche nous allons créer un petit programme qui boucle indéfiniment, et affiche toutes les secondes sur sa sortie standard le nombre de boucles qu’il a pu réaliser durant la seconde écoulée.

consomme-cpu.c:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>

int main(void)
{
    long long int compteur;
    time_t debut = time(NULL);
    // Attendre un debut de seconde
    while (time(NULL) == debut)
        ;
    while (1) {
        compteur = 0;
        time (& debut);
        while (time(NULL) == debut)
            compteur ++;
        fprintf(stdout, "[%u]%lldn", getpid(), compteur);
    }
    return 0;
}

Lançons ce processus dans un terminal et laissons-le tourner.

$ ./consomme-cpu 
          [3040]8046202
          [3040]8051003
          [3040]8038645
          [3040]8049329
          [3040]8378210
          [3040]8419106
          [3040]8416285
          [3040]8418075
          [3040]8415878
          [3040]8419727
          [3040]8416073
          [3040]8417343
          [3040]8414809
          ...

Il réalise environ huit millions de boucles par seconde. Dans une autre console, créons un groupe de contrôle spécifique et inscrivons notre processus.

# cd /sys/fs/cgroup/cpu/
# mkdir groupe-1
# echo 3040 > groupe-1/tasks
#

Les paramètres de contrôle de la bande passante sont par défaut les suivants.

# cat groupe-1/cpu.cfs_period_us
100000
# cat groupe-1/cpu.cfs_quota_us
-1
#

La période de régulation de la bande passante est donc de 100 millisecondes, et le quota CPU accordé à la tâche vaut -1, une valeur négative indiquant qu’aucune contrainte n’est appliquée au processus.

Nous allons modifier cette valeur pour lui accorder un quota de 25 millisecondes par périodes de 100 millisecondes.

# echo 25000 > groupe-1/cpu.cfs_quota_us
#

Le comportement du processus varie alors :

          [3040]8416132
          [3040]8417509
          [3040]4302933
          [3040]1766917
          [3040]1763016
          [3040]1798414
          [3040]1740740
          ...

Le nombre de boucles descend alors à 18 millions environ. Ré-augmentons le quota à 50%.

# echo 50000 > groupe-1/cpu.cfs_quota_us
#

Nous voyons alors le nombre de boucles par secondes augmenter à nouveau.

          [3040]1672509
          [3040]2776452
          [3040]3662061
          [3040]3777860
          [3040]3694039
          [3040]3768352
          [3040]3745732
          [3040]3822385
          ...

Restaurons-la valeur 100000 microsecondes et notre processus reprend son comportement initial.

# echo 100000 > groupe-1/cpu.cfs_quota_us
#
          [3040]3708664
          [3040]3779417
          [3040]5944852
          [3040]8051275
          [3040]8049984
          [3040]8050405
          ...

Groupe de tâches

Observons à présent le comportement lorsque nous lançons plusieurs tâches en parallèle que nous inscrivons dans le même groupe de contrôle.

          $ ./consomme-cpu & ./consomme-cpu
          [6105]8051374
          [6106]8050150
          [6106]8047698
          [6105]8046993
          [6106]8046275
          [6105]8046629
          [6106]8050943
          [6105]8044749
          [6106]8049421
          [6105]8047196
          ...

Nos tâches ont été placées sur deux processeurs (ou cœurs) distincts et arrivent ainsi à réaliser chacune 8 millions de boucles par seconde. Toutefois nous pouvons les limiter et leur attribuer un quota de temps-processeur plus faible. Par exemple 100% CPU en tout (pour les deux).

# echo 6105 > groupe-1/tasks
# echo 6106 > groupe-1/tasks
# echo 100000 > groupe-1/cpu.cfs_quota_us
#

Chacune arrive à réaliser environ 4 millions de boucles par seconde.

          [6105]8055582
          [6106]8023743
          [6105]7876233
          [6106]7598323
          [6106]3678658
          [6105]3910164
          [6105]3800836
          [6106]3753870
          [6105]3776468
          [6106]3659704
          ...

Ou encore une valeur de 150% CPU (en s’appuyant sur deux processeurs bien sûr).

# echo 150000 > groupe-1/cpu.cfs_quota_us
#
          [6105]3667414
          [6106]3831150
          [6106]3839798
          [6105]3714979
          [6105]5004119
          [6106]5851544
          [6106]5801272
          [6105]6028022
          [6105]5810675
          [6106]5749970
          [6106]5784018
          [6105]5769202
          ...

Conclusion

Les valeurs observées ci-dessus ne sont pas parfaitement stables et exactes, eu égard à l’ordonnancement temps-partagé qui prend en compte l’ensemble des tâches prêtes à s’exécuter et le comportement de chacune d’entre-elles vis-à-vis de la consommation de temps processeur.

Le contrôle de la puissance CPU disponible pour chaque groupe de tâches est à mon avis principalement intéressant pour les serveurs d’applications et les containers de machines virtuelles. Dans mon cas particulier, je pense l’employer lorsque je fais tourner des programmes de tests sur plusieurs instances de Qemu afin d’isoler chacune de la puissance CPU globalement disponible sur le serveur hôte.

<p style=”text-align: justify;”>

URL de trackback pour cette page