tl;dr: Augmentation du budget alloué au traitement des paquets réseau et de la taille du ring buffer des cartes réseau des agents DC/OS à DC1

Pourquoi ? Description du besoin

Au boulot, on nous reportait quelques echecs de CI, dus à des connexion qui échouaient vers des ressources internes, et en particulier vers les proyxs sortants. Notre CI étant hébergé sur un cluster DC/OS, il a fallu analyser ce qu'il se passait directement au niveau des machines en elles mêmes.

Critères de choix & solutions envisagées

On a constaté des drops de paquets et des "overruns" sur les interfaces réseau des agents, d'abord p1, puis finalement les autres aussi.

$ ifconfig eth3
eth3: flags=6211<UP,BROADCAST,RUNNING,SLAVE,MULTICAST>  mtu 1500
        ether 00:d0:e6:00:22:5a  txqueuelen 1000  (Ethernet)
        RX packets 552848402  bytes 758129279695 (706.0 GiB)
        RX errors 1411  dropped 12796  overruns 1411  frame 0
        TX packets 251477135  bytes 236791294927 (220.5 GiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
        device interrupt 18  memory 0x96800000-96ffffff

(ip -s link show dev eth3 pour ne pas utiliser ifconfig, qui est déprécié, mais j'ai trouvé la sortie plus difficile à lire sur le moment)

Un autre constat des drops en regardant les statistiques de la carte réseau et de ses files de traitement :

$ ethtool -S eth3 | grep rx_discard
     [0]: rx_discards: 104
     [1]: rx_discards: 84
     [2]: rx_discards: 485
     [3]: rx_discards: 73
     [4]: rx_discards: 475
     [5]: rx_discards: 0
     [6]: rx_discards: 149
     [7]: rx_discards: 41
     rx_discards: 1411

Au passage, on constate que les cartes réseau de ces machines ont 8 files de traitement des paquets. ethtool -l nous indique que l'ont pourrait passer à 30 files, combinées, c'est à dire qu'il faut toujours un nombre pair et que deux files sont liées, l'une pour la réception (RX) et l'autre pour la trasmission (TX).

rx buffer

Après quelques recherches, en particulier à l'aide de cet article, Monitoring and Tuning the Linux Networking Stack: Receiving Data, il a été constaté que la taille du ring buffer du driver réseau était configurée à une valeur assez basse par défaut :

$ ethtool -g eth3
Ring parameters for eth3:
Pre-set maximums:
RX:             4078
RX Mini:        0
RX Jumbo:       0
TX:             4078
Current hardware settings:
RX:             453
RX Mini:        0
RX Jumbo:       0
TX:             4078

Mon analyse est que les CPU de ces machines, servant au CI, sont soumis à des pics de charge, assez longs lors des builds et que le noyau n'a pas le temps de dépiler tous les paquets réseau arrivant dans le ring buffer entre la carte et le noyau. Ainsi, les anciens paquets non traités sont droppés.

/proc/net/softnet_stat

Un autre extrait de l'article traitant cette fois de la gestion des interruptions réseau. Le traitement se fait à l'aide de la couche NAPI, qui est une optimisation du traitement des interruptions réseau, pour ne pas générer une interruption par paquet réseau, comme c'était le cas à l'origine, et ainsi pouvoir traiter de fortes charges réseau, sans monopoliser le CPU. Ce mécanisme désactive les interruptions matérielles et ce sont les interruptions logicielles qui sont en charge de venir traiter régulièrement le contenu du buffer de la carte réseau. Toujours dans une optique de ne pas monopoliser le CPU à ce traitement, ce processus a une limite de temps allouée à chaque passage, nommée budget.

Quelques métriques :

$ cat /proc/net/softnet_stat
031231d7 00000000 00000011 00000000 00000000 00000000 00000000 00000000 00000000 00000000
03296a2d 00000000 00000016 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0324971c 00000000 00000010 00000000 00000000 00000000 00000000 00000000 00000000 00000000
02f8e92c 00000000 00000012 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00238d8a 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
002283b5 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0021ba0e 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0023d351 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0412b711 00000000 00000026 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0474550a 00000000 00000027 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0476f62a 00000000 00000036 00000000 00000000 00000000 00000000 00000000 00000000 00000000
04a45a35 00000000 0000003a 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00160440 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00175b21 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0015ed4f 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
0014f088 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

Un extrait de la doc pour lire ça : (TL;DR, la deuxième indique les paquets droppés par manque de place et la troisième colonne indique le nombre de dépassements de budget)

Each line of /proc/net/softnet_stat corresponds to a struct softnet_data structure, of which there is 1 per CPU. The values are separated by a single space and are displayed in hexadecimal The first value, sd->processed, is the number of network frames processed. This can be more than the total number of network frames received if you are using ethernet bonding. There are cases where the ethernet bonding driver will trigger network data to be re-processed, which would increment the sd->processed count more than once for the same packet. The second value, sd->dropped, is the number of network frames dropped because there was no room on the processing queue. More on this later. The third value, sd->time_squeeze, is (as we saw) the number of times the net_rx_action loop terminated because the budget was consumed or the time limit was reached, but more work could have been. Increasing the budget as explained earlier can help reduce this.

Finalement, et contrairement à ce que je pensais au début, dans l'exemple collecté, ça ne serait peut-être pas tant la place dans le buffer qui manquerait, mais le temps pour le traiter.

Actions envisagées

  • allouer plus de budget pour traiter le contenu de ce buffer - action à chaud : sysctl -w net.core.netdev_budget=600 (l'unité étant le paquet traité, le temps étant plafonné à 2 jiffies, il y a un effet de seuil dans l'augmentation de la valeur. Par défaut le budget est de 300)
  • augmenter la taille du ring buffer, à 1024 voir au max à 4096 - ⚠️ fait un down/up sur la carte réseau donc drop toutes les connexions en cours ⚠️ - ethtool -G ethX rx 1024

Next steps

Si cela ne suffit pas, d'autres pistes peuvent être explorées :

  • irqbalance et le fait de répartir les gestionnaires d'interruptions sur tous plusieurs cœurs et de ne surtout pas en avoir plusieurs sur le même coeur. Le budget étant assigné à un coeur, si plusieurs gestionnaires d'interruptions sont assignés au même coeur, ils partageront le même budget. A noter qu'un équilibrage automatique est réalisé sur les systèmes "modernes", nos centos 7 ont un process irqbalance qui tourne. Je ne m'attends donc pas de progrès de ce côté là, mais je n'ai pas creusé.

Quelques ressources documentaires