- Les problèmes de latence dans les systèmes distribués se résolvent de façon répétée simplement en activant
TCP_NODELAY, ce qui suggère que le comportement par défaut de TCP peut être en décalage avec les charges de travail modernes - L’algorithme de Nagle a été conçu en 1984 dans la RFC896 pour réduire le coût des en-têtes des petits paquets TCP, en empêchant l’envoi d’un nouveau segment avant la réception d’un ACK
- Utilisé avec le delayed ACK, il peut introduire de la latence : un côté attend l’ACK tandis que l’autre attend soit des données de réponse soit l’expiration d’un minuteur, ce qui pénalise les applications en pipeline sensibles à la latence
- Même avec un RTT intra-datacenter d’environ 500μs, les serveurs modernes peuvent accomplir beaucoup de travail pendant ce laps de temps, ce qui rend discutable l’intérêt de retarder une transmission d’un RTT complet
- Dans les systèmes distribués modernes, avec TLS, l’encodage, la sérialisation et la taille des messages applicatifs, le problème des paquets d’un seul octet est moins fréquent, et dans les environnements sensibles à la latence il est plus naturel de désactiver l’algorithme de Nagle
Le réglage regardé en premier lors du débogage de la latence
- Lorsqu’un problème de latence apparaît dans un système distribué, on commence souvent par vérifier si
TCP_NODELAYest activé - De nombreux développeurs de systèmes distribués ont perdu du temps sur des problèmes qui se résolvaient avec cette simple option de socket
- Cette répétition laisse penser que le comportement par défaut de TCP ne convient pas aux systèmes distribués actuels, ou que l’algorithme de Nagle lui-même est devenu obsolète
Le problème que l’algorithme de Nagle cherchait à résoudre
- RFC896 est un document de 1984 qui traite du problème des petits paquets
- À l’époque, lorsqu’on envoyait via TCP des données arrivant caractère par caractère, comme des frappes clavier, on se retrouvait avec 40 octets d’en-tête pour 1 octet de données
- Cela représentait 4000 % d’overhead avec 40 octets d’en-tête pour 1 octet utile
- Supportable à faible charge, mais défavorable au débit réseau
- L’objectif de l’algorithme de Nagle était de mieux amortir le coût des en-têtes TCP afin d’augmenter le débit
- Les petits paquets provenaient surtout d’applications interactives humaines comme les shells, ou d’implémentations transmettant les données au noyau petit à petit via plusieurs appels à
write
- Les petits paquets provenaient surtout d’applications interactives humaines comme les shells, ou d’implémentations transmettant les données au noyau petit à petit via plusieurs appels à
- Son fonctionnement essentiel consiste à ne pas envoyer immédiatement de nouvelles données dans un segment TCP distinct tant que les données précédemment envoyées n’ont pas encore été acquittées
- On présente souvent l’algorithme de Nagle avec un minuteur, mais la RFC896 elle-même n’utilise pas de minuteur distinct en dehors du temps d’aller-retour (RTT) du réseau
La latence introduite lorsqu’il est combiné au delayed ACK
- Le delayed ACK consiste à ne pas envoyer immédiatement l’accusé de réception d’un paquet, mais à attendre soit des données à renvoyer, soit l’expiration d’un minuteur
- RFC813 est un document précoce de 1982 proposant le retard d’ACK, en expliquant que dans certaines situations le récepteur peut différer l’envoi d’un ACK et programmer un minuteur pour l’envoyer plus tard
- RFC1122 formalise davantage le delayed ACK
- Les deux mécanismes sont raisonnables séparément, mais peuvent créer de la latence lorsqu’ils sont utilisés ensemble
- L’algorithme de Nagle attend la réception d’un ACK avant d’envoyer davantage de données
- Le delayed ACK retarde l’envoi de l’ACK jusqu’à ce que des données de réponse soient prêtes ou qu’un minuteur expire
- Cela aide à remplir davantage les paquets, mais convient mal aux applications en pipeline sensibles à la latence
- Un commentaire de John Nagle sur Hacker News attribue lui aussi le problème non pas à la prévention des tinygrams, mais à la combinaison du retard d’ACK et d’un minuteur fixe
- C’est un exemple où deux fonctions de protocole raisonnables se combinent pour produire un comportement indésirable, ce qui illustre la difficulté de concevoir des protocoles
Les points de décalage avec les systèmes distribués modernes
- Même sans delayed ACK, le comportement de l’algorithme de Nagle peut ne pas correspondre à ce que recherchent les systèmes distribués modernes
- Dans l’environnement actuel, le RTT lui-même représente un coût qu’on ne peut pas ignorer
- Un RTT unique à l’intérieur d’un datacenter est généralement d’environ 500μs
- Entre datacenters d’une même région, le RTT se chiffre en quelques ms
- Sur des trajets mondiaux, il peut atteindre plusieurs centaines de ms
- Les serveurs modernes peuvent effectuer beaucoup de travail même en quelques centaines de μs, ce qui rend peu évident le bénéfice d’un choix consistant à retarder l’envoi des données d’un RTT complet
- La justification initiale de l’algorithme de Nagle était de réduire l’overhead d’en-tête multiplié par 40 associé aux paquets d’un seul octet
- Les bases de données distribuées et systèmes distribués modernes n’envoient généralement pas de paquets d’un seul octet
- Les données envoyées par l’application sont elles-mêmes plus volumineuses
- Des overheads de protocole supplémentaires s’ajoutent, comme TLS
- Il faut aussi compter l’overhead de l’encodage et de la sérialisation
- Le problème consistant à éviter les petits messages reste important, mais cette responsabilité s’est en pratique déplacée vers la couche applicative
- Envoyer des données encapsulées en JSON octet par octet n’est pas efficace, indépendamment de l’algorithme de Nagle
Pourquoi considérer TCP_NODELAY comme le choix par défaut
- Si vous construisez un système distribué sensible à la latence sur du matériel moderne de type datacenter, vous pouvez activer
TCP_NODELAYafin de désactiver l’algorithme de Nagle - Compte tenu du trafic des systèmes modernes, de la structure des applications et des performances matérielles, l’algorithme de Nagle n’est peut-être plus nécessaire
- On peut même défendre l’idée que
TCP_NODELAYdevrait être la valeur par défaut - Un code qui appelle
writepour chaque octet peut devenir plus lent avecTCP_NODELAYcomme comportement par défaut - Si l’efficacité est importante, il vaut mieux corriger ce code au niveau de l’application plutôt que de compter sur l’algorithme de Nagle
TCP_QUICKACK reste plutôt un choix secondaire
TCP_QUICKACKpeut être évoqué comme alternative, mais son manque de portabilité et sa sémantique particulière en font un mauvais premier choix- Il faut vérifier directement sa signification dans la page de manuel Linux sur tcp
- Plus fondamentalement,
TCP_QUICKACKne résout pas le problème de fond : le noyau peut garder les données plus longtemps que ne le souhaite le programme - Si le programme appelle
write(), on s’attend à ce quewrite()soit effectivement exécuté
1 commentaires
Avis sur Hacker News
Au cours de ma carrière, j’ai corrigé plusieurs fois des problèmes de latence causés par l’algorithme de Nagle, et c’est désormais la première chose que je soupçonne
La logique elle-même se tient, mais elle ne convient pas à certaines charges de travail ; à mon avis, il faudrait que l’ingénieur fasse un choix explicite lors de la création d’un socket, plutôt que de s’en remettre au réglage par défaut du système d’exploitation
Le problème n’est pas de savoir si c’est une bonne ou une mauvaise option, mais qu’il existe un réglage qui modifie assez agressivement la manière dont les données sont envoyées, et que beaucoup de gens ignorent son existence
Jusqu’ici, j’ai trouvé un bug à chaque fois
Exemples : https://cloud-haskell.atlassian.net/browse/DP-108 ou https://github.com/agentm/curryer/issues/3
En revanche, je ne suis pas d’accord avec l’idée que ce ne serait « ni une bonne ni une mauvaise option »
C’est une heuristique côté noyau destinée à « corriger magiquement » des applications mal écrites et, comme le dit l’article, une application normale ne fait pas d’appel système réseau
write()d’un octetCe genre de logiciel doit être corrigé
Le seul cas où cette fonctionnalité a du sens, selon moi, c’est celui d’un administrateur système noyau dans une situation rare où il ne peut pas corriger le logiciel qui tourne sur la machine, par exemple pour des raisons de politique d’équipe
Dans tous les autres cas, elle rend les logiciels normaux plus complexes
Cela veut dire qu’il faut désactiver explicitement une magie étrange ajoutée pour augmenter légèrement le débit de logiciels mal écrits, et qui crée des latences importantes et surprenantes dans les logiciels bien écrits
Dans le fil lié ici, John Nagle dit que les ACK différés sont pires, et je suis d’accord avec lui
Mais le schéma Send/Send/Receive que l’algorithme de Nagle aggrave est un cas d’usage parfaitement valable et courant, et correspond à tout ce qui fait du RPC en pipeline au-dessus de TCP
À mon avis, les ACK différés comme l’algorithme de Nagle devraient être désactivés par défaut
Le nom devrait plutôt être quelque chose comme TCP_DELAY, et on ne devrait l’activer que lorsqu’on ne veut pas implémenter un buffering élémentaire en espace utilisateur
Les gens ne devraient pas avoir besoin de connaître ce genre de choses, et le comportement par défaut devrait être celui qui ne surprend pas
write, alors une option pour activer TCP_DELAY devient assez bizarreCela suppose un ingénieur logiciel assez malin pour connaître cette option, mais pas assez pour découper correctement ses appels à
writeou implémenter lui-même un meilleur buffering à la Nagle adapté à son applicationÀ moins d’avoir quelque chose comme
io_uringpour compenser le coût des appels système, l’approche côté espace utilisateur fonctionne mieuxSi je me souviens bien, c’était la motivation principale
La conclusion est un peu étrange. L’algorithme de Nagle était clairement une tentative de regrouper les écritures, et indépendamment du matériel, du réseau, de l’application ou du cas d’usage, il y a des situations où regrouper les écritures est préférable
Aujourd’hui encore, beaucoup de calculs utilisent le regroupement des écritures, et les applications réseau en bénéficient aussi
Des protocoles de plus haut niveau plus récents comme QUIC regroupent les écritures et déplacent en pratique les connexions indépendantes et la gestion des erreurs de TCP vers l’espace utilisateur, afin que le protocole pousse les données vers l’application aussi vite que possible, tandis que la gestion des connexions et des erreurs des flux individuels revient à l’application, et non à la pile TCP/IP de l’hôte ou aux routeurs
Si les réseaux redeviennent saturés comme autrefois, l’algorithme de Nagle reviendra sous une forme modifiée pour QUIC, probablement plus profondément dans le code applicatif, en attendant que certains seuils soient atteints avant d’envoyer des paquets QUIC
Tout, en technologie, est réinventé lorsque le matériel ou le logiciel atteint un goulot d’étranglement. Comme les performances des deux ne progressent pas au même rythme, cela finit toujours par arriver
Au-delà de la bande passante, l’algorithme de Nagle est utile quand de petits paquets saturent le nombre de paquets par seconde
Cela permettait certes de se connecter à un service avec un téléscripteur physique, mais TCP s’est retrouvé incapable de connaître les frontières de messages ; aujourd’hui, on peut injecter une partie de cette connaissance, mais les premiers logiciels ne le pouvaient pas
À l’inverse, de nombreux protocoles non TCP comme QUIC, SCTP ou TP4 fournissent explicitement des frontières de messages
L’interface avec le système n’est pas un port série émulé, mais quelque chose fondé sur des messages, tout au plus réassemblés
Le protocole n’a pas assez de contexte pour regrouper correctement
À l’inverse, que penser de la désactivation des ACK retardés ?
Le problème vient du comportement pathologique qui apparaît quand la prévention des petits paquets interagit avec les ACK retardés.
L’option exposée pour désactiver la prévention des petits paquets est
TCP_NODELAY, mais comment désactiver les ACK retardés ?C’est le cas quand on veut benchmarker les quatre combinaisons pour voir laquelle convient le mieux.
En cherchant un peu, Linux dispose de l’option de socket
TCP_QUICKACK, mais il faut la définir à chaque réception.Il y a aussi
/proc/sys/net/ipv4/tcp_delack_minet/proc/sys/net/ipv4/tcp_ato_min.FreeBSD a
net.inet.tcp.delayed_acketnet.inet.tcp.delacktime.TCP_QUICKACKcorrige la pire forme du problème, mais ne le résout pas entièrement.L’algorithme de Nagle peut toujours attendre jusqu’à un aller-retour complet avant d’envoyer les données, et, si l’on suit la RFC, cela ajoute surtout de la latence pour presque aucun gain.
TCP_QUICKACK, devoir le définir à chaque réception… à quoi pensaient-ils ?Pourquoi voudrait-on ne le désactiver que par moments ?
quickack 1à la fin de la route pour désactiver les ACK retardés sur ce chemin.Dans un monde où la bande passante était limitée, où la taille minimale d’un paquet était de 64 octets et où il fallait en plus un intervalle inter-trame, envoyer un paquet TCP pour chaque octet était un énorme gaspillage de bande passante.
Sur la plupart des réseaux Ethernet, la taille minimale reste la même aujourd’hui, et c’est aussi vrai pour l’envoi d’ACK vides.
Mais ma position de base est la suivante : ce n’est pas
TCP_NODELAY, c’est simplement TCP.Je ne suis pas vraiment convaincu par l’argument selon lequel Nagle ne serait plus nécessaire.
Telnet n’est plus important aujourd’hui, mais il doit encore exister beaucoup d’applications qui font des choses du genre :
write(fd, "Host: "),write(fd, hostname),write(fd, "\r\n"),write(fd, "Content-type: "), etc.Même si ce n’est pas un surcoût d’un facteur 40, cela peut facilement être autour de 5 fois.
On n’écrit pas dans un fichier de cette façon en s’attendant à des performances magiques. Même si le système d’exploitation a aussi ses propres buffers.
Il n’y a aucune raison d’attendre autre chose seulement parce qu’on écrit dans un socket, et Nagle ne vous épargne de toute façon pas le surcoût des appels système.
Je l’ai vérifié à la fois en lisant le code et en observant son comportement avec
strace.write(2)est la seule approche vraiment sensée ; ce type de pattern ne me paraît donc plus si courant.Côté serveur, les entrées-sorties asynchrones sont généralement nécessaires pour bien passer à l’échelle, et côté client, être bloqué par un appel réseau donne une mauvaise expérience.
C’est encore plus vrai dans l’environnement actuel, où les changements de réseau sont fréquents et où l’on sort souvent de la zone de couverture.
Même en dehors de l’aspect réseau, les appels système sont assez coûteux, donc c’est mauvais pour les performances.
Je me demande s’il existe une bonne façon d’activer TCP_NODELAY sur un socket quand on n’a pas accès au code source de l’application.
Je n’ai pas trouvé de paramètre noyau pour l’appliquer de façon permanente, ni de commande pour le changer après coup.
J’ai pu désactiver les ACK retardés en mettant
quickack 1dans la table de routage, mais activer TCP_NODELAY depuis l’extérieur de l’application semble particulièrement difficile.J’ai récemment rencontré exactement le problème décrit ici entre une application dont je suis propriétaire et une application source fermée avec laquelle elle interagit.
socket(2)ne fonctionnerait-elle pas ?L’idée serait d’appeler la vraie fonction, puis de faire un
setsockoptou équivalent, et de retourner le socket modifié.Si c’était initialement
your_app —> server, cela deviendraityour_app -> localhost_socat -> server.socat a une option en ligne de commande pour définir
tcp_nodelay.Il faut toutefois convaincre l’application source fermée de se connecter à localhost.
Si elle fait une résolution DNS, une entrée dans
/etc/hostspourrait éventuellement la faire se connecter à localhost.Comme l’application communique avec socat via un socket local, le
tcp_nodelaycôté application n’a pas d’effet.setsockoptviaptrace./proc//fd/et définir les options du socket pourrait peut-être fonctionner. Je n’ai pas testé.LD_PRELOADIl y a environ 15 ans, je jouais à un MMO très temps réel, et toutes les communications passaient par TCP.
Quand on cliquait sur un bouton, mon action n’apparaissait même pas à l’écran tant que le paquet de réponse n’était pas revenu.
Au final, les gamins qui jouaient à ce jeu, moi compris, ont tous découvert qu’activer TCP_NODELAY rendait le jeu beaucoup plus fluide.
L’effet était particulièrement marqué pour les joueurs de Californie, proches des serveurs du jeu.
Un effet secondaire intéressant était qu’avant le changement, quand le flux TCP se bloquait, le jeu se figeait brièvement puis rejouait très vite les événements reçus en retard.
En général, cet événement était ma mort.
Après le changement, à la place, la connexion était simplement coupée.
Épisode connexe du podcast Oxide and Friends : https://www.youtube.com/watch?v=mqvVmYhclAg
Cela ne s’applique pas si vous utilisez un langage moderne qui active TCP_NODELAY par défaut, comme Go :-)
https://github.com/golang/go/issues/57530
Je ne savais pas
Ne peut-on pas simplement utiliser une bibliothèque réseau « moderne » ?
Ce n’est pas toujours ça. Parfois, c’est DNS
De la poussière, près d’un chantier, s’était déposée dans l’interstice entre le laser et la fibre optique d’un routeur, atténuant suffisamment le signal ; on observait 40 à 50 % de perte de paquets
Après avoir localisé le point de perte, le NOC a envoyé un e-mail à l’opérateur de transport concerné, et le lendemain le technicien dépêché sur place a répondu en racontant cette histoire
Mais comme on peut généralement la contourner par un patch, ce n’est pas si grave
TCP_NODELAYou le buffering de fluxLe Web, système vraiment complexe, échoue aussi à cause des caches