Skip to main content

Il est lent ton réseau, GRO

31 oct. 2018

Il y a quelques jours, un utilisateur d'un de nos clusters OpenStack nous a fait part d'un problème réseau bien mystérieux : des taux de transferts faiblards entre ses machines et GitHub. On va vous spoiler la fin : c'était la faute au GRO (Generic Receive Offload), un mécanisme censé... améliorer les performances réseau. Pour les amateurs de Cluedo dans le datacenter, restez avec nous, on va vous raconter de quoi il en retourne plus en détail.

Les symptômes

La victime se plaignait du temps nécessaire pour effectuer un git clone d'une certaine repository GitHub. Jugez plutôt ; sur la plateforme ENIX située à Paris :

Receiving objects: 100% (6681/6681), 19.65 MiB | 343.00 KiB/s, done.

Tandis qu'une VM de taille similaire sur AWS EC2 eu-west-3 (autrement dit, Paris aussi) obtenait le résultat suivant :

Receiving objects: 100% (6681/6681), 19.65 MiB | 5.69 MiB/s, done.

(Les résultats obtenus pouvaient varier un peu, mais il n'y avait pas besoin de faire une grande campagne de benchmarks pour se rendre compte qu'il y avait un problème quelque part.)

Premières analyses

Dans ce genre de situation, on commence souvent par regarder l'itinéraire emprunté par les connexions, à l'aide d'outils comme traceroute ou tracepath, par exemple. Si on est familier avec la topologie et les caractéristiques de ses liens de transit IP, cela permet parfois de se rendre compte que l'on passe par un chemin suboptimal (par exemple, un Internet Exchange réputé pour être saturé aux heures de pointes). Mais même sans cette connaissance du terrain, comparer deux traceroute permet de voir s'ils passent par deux chemins différents et si cela pourrait expliquer le problème.

En ce qui nous concerne, rien ne saute aux yeux à priori ; et même après avoir re-routé le trafic à destination de GitHub pour emprunter un autre transitaire, on a gagné 10ms de ping, mais la vitesse de transfert n'a pas bougé d'un poil.

Un indice confondant

Par coup de chance, l'un d'entre nous a eu la présence d'esprit d'exécuter le même git clone depuis une autre machine non virtualisée de notre infrastructure. Surprise : pas de ralentissement, tout va bien. Ça met donc hors de cause le chemin réseau entre ENIX et GitHub. Du coup, on se concentre sur le cluster OpenStack. On le passe au peigne fin, on lance des iperf à tour de bras entre le fameux cluster OpenStack et d'autres machines ... Et on ne voit rien. La même machine qui n'enregistre que 300 ko/s avec GitHub est capable de communiquer à plusieurs gigabits/s avec ses voisines, et encore, sans transpirer.

L'expérience à la rescousse

Cela fait longtemps que les interfaces Ethernet (surtout celles équipant les serveurs) disposent de tout un arsenal de fonctionalités (comme le TSO ou LSO) supposées améliorer les performances, tout particulièrement dans les liens à très haut débit (plusieurs gigabits voire plusieurs dizaines de gigabits par seconde). Malheureusement, ça ne marche pas toujours comme on voudrait (on va y revenir plus tard), et dans bien des cas (sur des routeurs, des firewalls, des hyperviseurs...) il est conseillé de désactiver ces fonctionalités. C'est le genre de chose qu'on apprend (généralement dans la douleur) quand on a passé quelques années à opérer un NOC avec, précisément, des routeurs, des firewalls, des hyperviseurs, de part et d'autre de l'Atlantique, cherchant à transférer des octets d'un bout à l'autre le plus vite possible. (C'était précisément une des choses que faisait SmartJog, dont sont issus certains d'entre nous!)

Le réflexe, c'est de lancer sur l'hyperviseur ethtool -k enp3s0f0 (en remplaçant par le nom de sa carte réseau) et d'apercevoir en particulier les lignes suivantes :

generic-segmentation-offload: on
generic-receive-offload: on

Voilà le suspect numéro 1 dans notre affaire.

La solution

À partir de là, tout va très vite ... Il suffit de désactiver ces fonctionalités et de tester de nouveau la vitesse de transfert. Dans notre cas, les deux commandes suivantes ont immédiatement résolu le problème :

ethtool -K enp3s0f0 gro off
ethtool -K enp3s0f1 gro off

Le résultat se fait sentir instantanément (pas besoin de relancer la VM ou l'hyperviseur ou quoi que ce soit) :

Receiving objects: 100% (6708/6708), 19.67 MiB | 15.63 MiB/s, done.

On va donc maintenant 3 fois plus vite que la VM sur EC2. Bien! L'incident est clos, c'était la faute au GRO.

Mais ... Pourquoi ?

Comment se fait-il qu'une fonctionalité censée accélérer les transferts se retrouve à les ralentir?

Pour comprendre, penchons-nous sur la raison d'être de ce GRO.

Le protocole Ethernet a été inventé dans les années 70. Les premières versions offraient un débit fracassant de presque 3 Mb/s. La taille maximale d'une trame était de 1518 octets. Ça veut dire que pour émettre (ou recevoir) à la vitesse maximale (en utilisant des trames de taille maximale), il suffisait d'être capable de traiter un peu plus de 200 paquets par seconde. Traiter un paquet, ça demande du travail à la carte réseau, à son driver, et à tout un tas de couches du noyau (Ethernet, IP, TCP), et ce indépendamment de la taille du paquet. Si on veut transférer de grandes quantités de données, on a intérêt à utiliser les paquets les plus gros possibles. Mais depuis les années 70, la taille maximale des trames Ethernet n'a presque pas changé (on a ajouté 4 octets pour gérer les VLANs, et dans certains cas, on peut utiliser des Jumbo Frames faisant un peu plus de 9000 octets). En revanche, la vitesse des liens Ethernet a décollé : presque toutes les machines grand public ont des interfaces 1 gigabit/s, et les serveurs ayant au moins 2 interfaces 10 gigabits/s sont monnaie courante. Pour émettre ou recevoir à 20 gigabits/s, même avec des trames de 9000 octets, il faut traiter plus de 250 000 paquets par seconde. Les ordinateurs d'aujourd'hui sont plus rapides que ceux des années 70, mais envoyer ou recevoir 250 000 paquets par seconde ça représente tout de même énormément de temps CPU, et ça justifie qu'on tente d'optimiser ça par tous les moyens possibles.

Un des moyens possibles, c'est le GSO (Generic Segmentation Offload) pour l'émission, et le GRO (Generic Receive Offload) pour la réception.

Le principe du GSO est relativement simple. Au lieu d'envoyer au driver de la carte réseau des paquets de 1500 octets (ou 9000), on lui envoie des paquets bien plus gros (64 ko), et le driver les découpe en paquets plus petits avant de les passer à la carte réseau. Ça n'a l'air de rien comme ça, mais le fait que le découpage se fasse le plus bas possible peut déjà améliorer les performances d'environ 17%. Certaines cartes sont même capables de faire le découpage elles-mêmes, ce qui permet d'obtenir des gains bien supérieurs. On peut s'en convaincre en regardant ces benchmarks, par exemple.

Le GRO est plus délicat, car son but est de fusionner les paquets entrants. Par exemple, on reçoit 10 paquets de 1500 octets appartenant à la même connexion, et on les fait apparaître au système comme un gros paquet de 15000 octets. Mais en pratique, on ne reçoit pas 10 paquets d'un coup : les paquets arrivent les uns après les autres. À chaque fois, la carte réseau doit donc décider « est-ce que j'attends un peu des fois que d'autres paquets arrivent, ou bien j'envoie ce que j'ai pour l'instant ? »

En fait, sur un routeur (ou un firewall ou un hyperviseur, autrement dit : toute machine recevant des paquets et les transmettant à une autre, physique ou virtuelle), le GRO ou le LRO sont vivement déconseillés, car ils violent le principe de bout-en-bout, qui dit grosso modo que les choses compliquées (chiffrement, fragmentation...) doivent être effectués par les machines à chaque bout d'une connexion, et pas par les routeurs acheminant le trafic entre elles. En pratique, le LRO a déjà été identifé comme provoquant des problèmes de performance (par exemple en limitant la taille de la fenêtre de transmission TCP, voire en dégradant complètement les communications) et les notes accompagnant les drivers Intel de l'époque annonçaient carrément « Due to a known general compatibility issue with LRO and routing, do not use LRO when routing or bridging packets. »

On peut se demander pourquoi le noyau ne prend pas l'initiative de désactiver ces fonctionalités dès lors que le routage ou le bridging sont activés. En pratique, ça n'est pas si simple : une machine qui fait tourner Docker (ou n'importe quel système de containers) effectue du routage et du bridging, mais ne sera probablement pas affectée négativement par le GRO, car les applications ayant besoin de hautes performances réseau vont utiliser le host networking ou bien une interface macvlan, et leur trafic ne sera pas routé ou bridgé. Du coup, réponse de Normand : « Ça dépend ... »

Conclusions

Verdict : quand on déploie des machines physiques, il y a une foule de paramètres auxquels il faut faire attention. Il fut une époque (lointaine) où il fallait se rappeler d'utiliser hdparm pour activer le DMA et avoir des performances correctes sur ses disques IDE ; plus récemment, certains disques SATA/SAS nécessitaient eux aussi sdparm pour régler correctement les paramètres de write back / write through. Devoir faire attention à la configuration des interfaces réseau n'est pas si choquant que ça. Si vous utilisez des machines physiques (par exemple, si vous opérez votre propre cloud privé) c'est important à savoir ; sinon, votre fournisseur d'infrastructure le fera pour vous!