Répartir les interruptions sur les CPU

Avoir plusieurs cœurs c'est bien, mais par défaut les interruption des cartes réseau ne sont pas réparties. On parle ici du trafic entrant exclusivement, le trafic sortant étant réparti à la guise du noyau ne pose pas de problème.

Une technique comme NAPI (device polling) qui est maintenant intégrée d'office dans le noyau et active, permet déjà de réduire significativement le volume des interruptions ce qui soulage énormément le(s) processeur(s). Une autre technique plutôt côté NIC c'est la modération des interruptions. Avec des considérations de latence, et de type de trafic à prendre en compte… Mais ça ne veut pas dire plus d'interruptions.

Par ailleurs le scheduleur n'a pas conscience de la charge subie par les cœurs du fait des interruptions, et peut affecter une tâche à un cœur alors que celui-ci est déjà occupé à prendre en charge toutes ces interruptions. Il est donc utile de répartir celles-ci dès lors que ça risque de chauffer un peu.

Les acteurs

Dans la partie il y a donc les processeurs et plus précisément les cœurs, parce que mutlithreading ou pas, ce sont bien les cœurs qui vont gérer les interruptions et qui supportent la charge.

Il y a l'APIC (Advanced Programmable Interrupt Controller) qui affecte par défaut toutes les interruptions au cœur 0, qu'on soit en SMP ou pas. Mais heureusement ça se configure.

Il y a le noyau, et surtout les pilotes de cartes. Et le matériel qui est à l'origine des interruptions, et plus ou moins évolué.

Premier essai

Une méthode qui peut sembler intéressante de régler le problème est d'indiquer que les interruptions doivent être réparties sur tous les processeurs.

On peut faire ça en écrivant dans /proc/irq/xx/smp_affinity ('xx' désignant le numéro de la ligne d'interruption concernée). Ce fichier contient une ou plusieurs chaines de caractères (autant qu'il en faut pour avoir un bit par core présent dans la machine, en groupes de huit caractères heaxdécimaux séparés par des virgules). A chaque fois qu'un bit est à 1, le processeur correspondant est mis à contribution. Donc par exemple écrire 3 dans ce fichier (donc 0011 en binaire) va activer pour cette ligne d'interruption les processeurs 0 et 1. Ecrire 4 (donc 0100) activera le core 3. Etc.

Pour répartir sur les quatre core de mon Xeon L3426 je pourrais donc écrire '0f' (soit '1111') dans la ligne qui va bien :

# grep eth0 /proc/interrupts
  39:          1          0          0          0          0          0          0          0   PCI-MSI-edge      eth0
# echo 'f' > /proc/irq/39/smp_affinity
# cat /proc/irq/39/smp_affinity
0000000f

Mauvaise pioche. Pour certains usages c'est peut-être une bonne idée mais pour les interfaces réseau non. Parceque une telle répartition va poser des problèmes de cache lorsque pour une même session TCP deux cores vont se retrouver chacun avec des informations dans leur cache, mutlipliant les «cache miss» qui sont très coûteux. Avec pour conséquence une dégradation important des performances, ce qui n'était pas le but recherché.

Sur ma Debian Squeeze et son noyau 2.6.32-5 ça n'a même tout simplement pas fonctionné, les interruptions continuant à n'être gérées que par un cœur unique. Mais c'est sans doute lié au fait que le driver de ma NIC utilise MSI (on va en parler tout de suite). On peut désactiver MSI dans certains drivers, par exemple sur un pilote Broadcom bnx2 en allant mettre un options bnx2 disable_msi=1 dans la config de modprobe.

Une queue par cœur

La solution qui a été inventée c'est de mettre à disposition une queue par core, et que la carte (idéalement), répartisse avec cohérence les interruptions dans des queues qui pourront être chacune affectées à un cœur.

MSI à la rescousse

Les cartes qui font ça ne sont pas tout à fait bas de gamme. MSI (et même MSI-X à présent) qui est «un protocole de cohérence de cache utilisé dans les systèmes multiprocesseur.» (merci Wikipedia). On comprend tout de suite que ça va aider à résoudre le problème. Grâce à MSI on voit apparaitre dans /proc/interrupts une ligne d'interruption par carte et par cœur.

Quand on a la chance d'avoir par exemple une carte genre Intel 82576 (comme la Quad port que Dell propose dans les R210) avec son driver igb qui implémente MSI-X alors on dispose d'autant de files d'entrée et de sortie que de cœur de processeur, pour chaque carte. Ce qui donne sur un Xeon 4C/8T le résultat suivant (noyau 2.6.32-5):

# grep PCI-MSI-edge /proc/interrupts
31:   31:   12379890          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-tx-0
32:   32:    2226006          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-tx-1
33:   33:     850569          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-tx-2
34:   34:          0          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-tx-3
35:   35:   21891622          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-rx-0
36:   36:    9474235          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-rx-1
37:   37:    6956870          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-rx-2
38:   38:    5817087          0          0          0          0          0          0          0   PCI-MSI-edge      eth0-rx-3
41:   41:          6          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-tx-0
42:   42:         29          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-tx-1
43:   43:       8334          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-tx-2
44:   44:          0          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-tx-3
45:   45:      92909          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-rx-0
46:   46:      41755          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-rx-1
47:   47:      43901          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-rx-2
48:   48:      55828          0          0          0          0          0          0          0   PCI-MSI-edge      eth1-rx-3

Reste que toutes les interruptions arrivent toujours sur le premier coeur et lui seul, il va donc falloir les répartir à la main.

Et non, comme expliqué plus haut il n'est pas question de mettre '0f' pour balancer sur tous les processeurs, puisque c'est précisément ce que MSI cherche à éviter.

Exemple de script

J'ai écrit un petit script a deux francs six sous pour répartir les interruptions. C'est fort pratique on a 4 files Rx pour eth0 par exemple, il suffit de récupérer le numéro tout en fin de ligne pour désigner le core qui porte le même numéro.

A rajouter par exemple dans /etc/rc.local, à vos entiers risques et périls.

# balance IRQs for différent MSI-X queues on processor cores
 
for QUEUE in /proc/irq/*/eth*-*x-* ; do
  PROC=${QUEUE# #*-}       # attention retirer le blanc entre les deux '#'
  INTR=${QUEUE#*irq/}
  INTR=${INTR%/eth*}
  echo $((1 << $PROC)) > /proc/irq/${INTR}/smp_affinity
done

Observations

Vous aurez besoin d'adapter ce script. D'une part parceque les noms des files Rx Tx dans /proc/interrupts ne sont pas toujours exactement formatés comme ça, ça change par exemple en fonction de la version du noyau (du driver ?). Et d'autre part parce que il convient d'adapter la répartition des interruptions en fonction de la répartition du cache sur les cœurs. En effet tous les processeurs ne sont pas organisés de la même manière, certains ont par exemple 4 cœurs qui partagent 2 à 2 le même cache.

Enfin la répartition au hasard et derechef des interruptions, si elle améliore drastiquement la bande passante que peut gérer la machine, ne signifie pas qu'on ne va pas avoir un processus qui se rattache au cœur qui est justement occupé à gérer l'essentiels des interruptions.

Les patches de Google

Pour ceux qui n'ont pas la chance de pouvoir s'offrir des cartes qui gèrent plusieurs queues et MSI, les développeurs de Google ont écrit un patch qui permet de simuler la même chose. On obtient tout de même parait-il une amélioration réelle des performance grâce à la possibilité de répartir les interruptions sur les cœurs.

Ces patches sont connus sous le(s) nom(s) de RPS et RFS. Ils sont intégrés dans le noyau Linux à partir de la version 2.6.35 (eh oui, si vous êtes en Squezze fait recompiler) pour votre e1000.

RPS pour «Receive Packet Steering» calcule (ou récupère sur la carte qui l'a déjà calculé) un hash de l'en-tête d'un paquet, et s'en sert pour savoir à quel processeur le faire traiter. RFS pour «Receive Flow Steering» pousse le rafinement en localisant sur le même cœur les données qui font l'objet d'appels système depuis l'espace utilisateur.

Pour en savoir plus sur RPS et RFS : http://linuxfr.org/news/nouvelle-version-2635-du-noyau-linux#long4

Quelques références externes ...