Die anhaltende Nutzung der Option TCP_NODELAY
(brooker.co.za)- Probleme mit Latenz in verteilten Systemen lassen sich immer wieder allein durch das Aktivieren von
TCP_NODELAYlösen; das Standardverhalten von TCP kann mit modernen Workloads kollidieren - Der Nagle-Algorithmus wurde 1984 in RFC896 entwickelt, um die Header-Kosten kleiner TCP-Pakete zu reduzieren, und verhindert das Senden neuer Segmente, bevor ein ACK eingetroffen ist
- In Kombination mit delayed ACK wartet eine Seite auf das ACK, während die andere auf Antwortdaten oder einen Timer wartet – ungünstig für latenzempfindliche Pipeline-Anwendungen
- Selbst wenn die RTT innerhalb eines Rechenzentrums nur etwa 500 μs beträgt, können moderne Server in dieser Zeit viel Arbeit erledigen; der Nutzen, eine Übertragung um eine RTT zu verzögern, ist daher unklar
- In modernen verteilten Systemen sind Ein-Byte-Pakete durch TLS, Encoding, Serialisierung und größere Anwendungsnachrichten seltener geworden; in latenzempfindlichen Umgebungen wirkt das Deaktivieren des Nagle-Algorithmus natürlicher
Erste Einstellung beim Debugging von Latenz
- Wenn in verteilten Systemen Probleme mit Latenz auftreten, prüft man oft zuerst, ob
TCP_NODELAYaktiviert ist - Viele Entwickler verteilter Systeme haben Zeit mit Problemen verloren, die sich durch diese eine einfache Socket-Option lösen ließen
- Diese Wiederholung deutet darauf hin, dass das Standardverhalten von TCP nicht zu heutigen verteilten Systemen passt oder dass der Nagle-Algorithmus selbst veraltet sein könnte
Das Problem, das der Nagle-Algorithmus lösen sollte
- RFC896 ist ein Dokument aus dem Jahr 1984, das sich mit dem Problem kleiner Pakete befasst
- Damals entstand beim Senden von Daten über TCP, die wie Tastatureingaben Zeichen für Zeichen eintreffen, die Ineffizienz, dass an 1 Byte Daten jeweils ein 40-Byte-Header angehängt wurde
- Pro 1 Byte Nutzdaten fielen 40 Byte Header an, also 4000 % Overhead
- Bei geringer Last war das verkraftbar, für den Netzwerkdurchsatz aber ungünstig
- Ziel des Nagle-Algorithmus war es, die Kosten von TCP-Headern besser zu amortisieren und den Durchsatz zu erhöhen
- Kleine Pakete entstanden vor allem bei interaktiven Anwendungen wie Shells und bei Implementierungen, die Daten über mehrere
write-Aufrufe stückweise an den Kernel übergeben
- Kleine Pakete entstanden vor allem bei interaktiven Anwendungen wie Shells und bei Implementierungen, die Daten über mehrere
- Das Kernverhalten besteht darin, neue Sendedaten nicht sofort als separates TCP-Segment zu senden, wenn zuvor gesendete Daten noch kein ACK erhalten haben
- Der Nagle-Algorithmus wird häufig zusammen mit einem Timer erklärt, RFC896 selbst verwendet jedoch keinen separaten Timer außer der Round-Trip Time (RTT) des Netzwerks
Verzögerungen in Kombination mit delayed ACK
- delayed ACK bedeutet, dass eine Empfangsbestätigung für ein Paket nicht sofort gesendet wird, sondern gewartet wird, bis zurückzusendende Daten vorliegen oder ein Timer abläuft
- RFC813 ist ein frühes Dokument aus dem Jahr 1982, das verzögerte ACKs vorschlug und beschreibt, dass der Empfänger in bestimmten Situationen das Senden eines ACKs aufschieben und einen Timer für späteres Senden setzen kann
- RFC1122 formalisiert delayed ACK weiter
- Beide Funktionen sind für sich genommen sinnvoll, können zusammen aber Latenz erzeugen
- Der Nagle-Algorithmus wartet auf den Empfang eines ACKs, bevor mehr Daten gesendet werden
- delayed ACK verschiebt das Senden des ACKs, bis Antwortdaten bereitstehen oder ein Timer abläuft
- Das hilft, Pakete besser zu füllen, ist aber schlecht für latenzempfindliche Pipeline-Anwendungen
- Auch John Nagles Kommentar auf Hacker News sieht das Problem nicht in der Vermeidung von Tinygrams, sondern in der Kombination aus ACK-Verzögerung und festem Timer
- Es ist ein Beispiel dafür, wie zwei jeweils vernünftige Protokollfunktionen zusammen unerwünschtes Verhalten erzeugen; solche Wechselwirkungen machen Protokolldesign schwierig
Wo es nicht zu modernen verteilten Systemen passt
- Auch ohne delayed ACK kann das Verhalten des Nagle-Algorithmus von dem abweichen, was moderne verteilte Systeme benötigen
- In heutigen Umgebungen ist die RTT selbst ein schwer zu ignorierender Kostenfaktor
- Eine einzelne RTT innerhalb eines Rechenzentrums liegt typischerweise bei etwa 500 μs
- RTTs zwischen Rechenzentren derselben Region betragen mehrere Millisekunden
- Globale Pfade können bis in den Bereich von Hunderten Millisekunden gehen
- Moderne Server können innerhalb von Hunderten Mikrosekunden viel Arbeit erledigen; es ist daher schwer, eine Verzögerung des Sendens um eine RTT als klaren Vorteil zu betrachten
- Die ursprüngliche Rechtfertigung für den Nagle-Algorithmus lag darin, den 40-fachen Header-Overhead bei Ein-Byte-Paketen zu reduzieren
- Moderne verteilte Datenbanken und verteilte Systeme senden im Allgemeinen keine Ein-Byte-Pakete
- Die von Anwendungen gesendeten Daten sind selbst größer
- Protokoll-Overhead wie TLS kommt hinzu
- Auch Encoding- und Serialisierungs-Overhead fallen an
- Das Problem, kleine Nachrichten zu vermeiden, bleibt wichtig, aber die Verantwortung dafür hat sich effektiv auf die Anwendungsschicht verlagert
- Daten byteweise zu senden, die in JSON verpackt sind, ist unabhängig vom Nagle-Algorithmus ineffizient
Warum TCP_NODELAY als Standardwahl gesehen wird
- Wenn man ein latenzempfindliches verteiltes System auf moderner Rechenzentrums-Hardware baut, kann man
TCP_NODELAYaktivieren und den Nagle-Algorithmus deaktivieren - Angesichts des Traffics, der Anwendungsarchitektur und der Hardwareleistung moderner Systeme ist der Nagle-Algorithmus möglicherweise nicht mehr nötig
- Die Position, dass
TCP_NODELAYder Standard sein sollte, ist ebenfalls vertretbar - Code, der für jedes Byte
writeaufruft, kann mitTCP_NODELAYals Standard langsamer werden - Wenn Effizienz wichtig ist, sollte solcher Code nicht auf den Nagle-Algorithmus vertrauen, sondern in der Anwendungsimplementierung korrigiert werden
TCP_QUICKACK ist eher eine ergänzende Option
TCP_QUICKACKkann als Alternative genannt werden, ist wegen mangelnder Portabilität und spezieller Semantik aber schwer als erste Wahl zu empfehlen- Die Bedeutung in der Linux-tcp-Manpage sollte direkt geprüft werden
- Das größere Problem ist, dass
TCP_QUICKACKdas Grundproblem nicht löst, dass der Kernel Daten länger zurückhält, als es die Absicht des Programms ist - Wenn ein Programm
write()aufruft, erwartet es, dasswrite()tatsächlich ausgeführt wird
1 Kommentare
Meinungen auf Hacker News
Im Laufe meiner Karriere habe ich mehrfach Latenzprobleme behoben, die durch den Nagle-Algorithmus verursacht wurden, und inzwischen ist er das Erste, was ich verdächtige.
Die Logik an sich ist stimmig, passt aber nicht zu manchen Workloads. Meiner Ansicht nach sollte ein Engineer beim Erstellen eines Sockets explizit wählen müssen, statt das dem Betriebssystem-Default zu überlassen.
Das Problem ist nicht, ob es eine gute oder schlechte Option ist, sondern dass es eine Einstellung gibt, die die Art der Datenübertragung ziemlich aggressiv verändert, und viele Menschen wissen nicht einmal, dass sie existiert.
Bisher habe ich jedes Mal einen Bug gefunden.
Beispiele: https://cloud-haskell.atlassian.net/browse/DP-108 oder https://github.com/agentm/curryer/issues/3
Allerdings stimme ich „keine gute/schlechte Option“ nicht zu.
Das ist eine Kernel-seitige Heuristik, um schlecht geschriebene Anwendungen „magisch zu reparieren“, und wie der Artikel sagt, machen normale Anwendungen keine 1-Byte-Netzwerk-
write()-Systemaufrufe.Solche Software sollte repariert werden.
Der Fall, in dem diese Funktion Sinn ergibt, ist meiner Ansicht nach höchstens der seltene Fall eines Kernel-Systemadministrators, der aus Gründen wie Team-Politik die Software, die auf der Maschine läuft, nicht reparieren kann.
Ansonsten macht sie normale Software komplizierter.
Das heißt, man muss seltsame Magie explizit abschalten, die eingebaut wurde, um den Durchsatz schlecht geschriebener Software ein wenig zu erhöhen, und die bei korrekt geschriebener Software große und überraschende Latenzen erzeugt.
John Nagle sagt in dem hier verlinkten Thread, dass Delayed ACKs schlimmer seien; dem stimme ich zu.
Aber das Send/Send/Receive-Muster, das der Nagle-Algorithmus verschlimmert, ist ein völlig legitimer und häufiger Use Case und betrifft alles, was Pipeline-RPC über TCP macht.
Meiner Meinung nach sollten sowohl Delayed ACKs als auch der Nagle-Algorithmus standardmäßig ausgeschaltet sein.
Der Name sollte eher TCP_DELAY lauten, und man sollte es nur einschalten, wenn man keine grundlegende User-Space-Pufferung implementieren will.
Menschen sollten so etwas nicht wissen müssen, und das Default-Verhalten sollte das wenig überraschende sein.
write-Verhalten zu reparieren, wird eine Option zum Einschalten von TCP_DELAY ziemlich merkwürdig.Das bedeutet, man braucht Software-Engineers, die klug genug sind, diese Option zu kennen, aber nicht klug genug, ihre
write-Aufrufe sinnvoll aufzuteilen oder eine bessere, zur eigenen Anwendung passende Nagle-artige Pufferung selbst zu bauen.Wenn es nichts wie
io_uringgibt, das die Kosten von Systemaufrufen ausgleicht, funktioniert die User-Space-Seite besser.Soweit ich mich erinnere, war das die Hauptmotivation.
Die Schlussfolgerung ist etwas seltsam. Der Nagle-Algorithmus war eindeutig ein Versuch, Writes zu bündeln, und unabhängig von Hardware, Netzwerk, Anwendungen oder Use Cases ist Bündelung in manchen Fällen besser.
Auch heute nutzt viel Computing Bündelung, und Netzwerkanwendungen profitieren ebenfalls davon.
Neuere High-Level-Protokolle wie QUIC bündeln Writes und verlagern TCPs unabhängige Verbindungs- und Fehlerbehandlung praktisch in den User Space, sodass das Protokoll Daten möglichst schnell in die Anwendung schiebt und die Anwendung, nicht der Host-TCP/IP-Stack oder Router, die Verbindungs- und Fehlerbehandlung einzelner Streams übernimmt.
Wenn Netzwerke wieder so ausgelastet werden wie früher, wird der Nagle-Algorithmus in modifizierter QUIC-Form zurückkehren, vermutlich tiefer im Anwendungscode, indem mit dem Senden von QUIC-Paketen gewartet wird, bis bestimmte Kriterien erreicht sind.
Alles in der Technik wird neu erfunden, sobald Hardware oder Software an einen Engpass stößt. Weil die Leistung beider nicht im gleichen Tempo wächst, passiert das letztlich immer.
Abgesehen von Bandbreite ist der Nagle-Algorithmus auch dann nützlich, wenn durch kleine Pakete die Paketanzahl pro Sekunde gesättigt wird.
Dadurch konnte man zwar mit einem physischen Teletypewriter auf einen Dienst zugreifen, aber TCP wusste nichts von Nachrichtengrenzen, und während man dieses Wissen heute teilweise hineinstecken kann, konnte frühe Software das nicht.
Dagegen stellen QUIC sowie viele Nicht-TCP-Protokolle wie SCTP und TP4 Nachrichtengrenzen explizit bereit.
Die Schnittstelle zum System ist kein emulierter serieller Port, sondern höchstens nachrichtenbasiert mit Reassembly.
Das Protokoll hat nicht genug Kontext, um korrekt zu bündeln.
Umgekehrt: Wie wäre es, Delayed ACK abzuschalten?
Das Problem ist das pathologische Verhalten, das entsteht, wenn die Vermeidung kleiner Pakete und Delayed ACK miteinander interagieren.
Die exponierte Option zum Abschalten der Vermeidung kleiner Pakete ist
TCP_NODELAY, aber wie schaltet man Delayed ACK ab?Etwa wenn man alle vier Kombinationen benchmarken will, um zu sehen, was am besten passt.
Nach kurzem Suchen gibt es unter Linux die Socket-Option
TCP_QUICKACK, die aber bei jedem Empfang erneut gesetzt werden muss.Außerdem gibt es
/proc/sys/net/ipv4/tcp_delack_minund/proc/sys/net/ipv4/tcp_ato_min.FreeBSD hat
net.inet.tcp.delayed_ackundnet.inet.tcp.delacktime.TCP_QUICKACKbehebt zwar die schlimmste Ausprägung, löst aber nicht das gesamte Problem.Der Nagle-Algorithmus kann weiterhin bis zu eine volle Round-Trip-Time warten, bevor er Daten sendet, und wenn man sich an den RFC hält, kommt dabei fast nur zusätzliche Latenz ohne nennenswerten Nutzen heraus.
TCP_QUICKACKbei jedem Empfang neu setzen zu müssen – was haben sie sich dabei gedacht?Warum sollte man es nur für einen Teil der Zeit abschalten wollen?
quickack 1hinzufügen, um Delayed ACK für diesen Pfad abzuschalten.In einer Welt mit begrenzter Bandbreite, in der Pakete eine Mindestgröße von 64 Byte hatten und zusätzlich noch ein Interframe Gap nötig war, war es eine enorme Bandbreitenverschwendung, für jedes einzelne Byte ein TCP-Paket zu senden.
In den meisten Ethernet-Netzwerken gilt diese Mindestgröße bis heute, und leere ACKs zu senden ist genauso.
Meine Grundhaltung ist allerdings: Das ist nicht
TCP_NODELAY, sondern einfach TCP.Das Argument, dass Nagle nicht mehr nötig sei, überzeugt mich nicht so recht.
Telnet ist heute nicht mehr wichtig, aber es dürfte weiterhin viele Anwendungen geben, die ungefähr so arbeiten:
write(fd, "Host: "),write(fd, hostname),write(fd, "\r\n"),write(fd, "Content-type: ")usw.Auch wenn das kein 40-facher Overhead ist, kann es durchaus etwa das Fünffache sein.
Beim Schreiben in eine Datei würde man so etwas nicht machen und dann magische Performance erwarten – obwohl das Betriebssystem dort ebenfalls eigene Puffer hat.
Es gibt keinen Grund, beim Schreiben auf Sockets etwas anderes zu erwarten, und Nagle rettet einen ohnehin nicht vor dem Overhead von Systemaufrufen.
Das habe ich sowohl durch Lesen des Codes als auch durch Beobachten des
strace-Verhaltens bestätigt.write(2)die einzig sinnvolle Vorgehensweise; deshalb glaube ich, dass dieses Muster heute nicht mehr so verbreitet ist.Auf Servern braucht man in der Regel asynchrone Ein-/Ausgabe, wenn man gut skalieren will, und auch auf Clients sorgt Blockieren bei Netzwerkaufrufen für eine schlechte Nutzererfahrung.
Das gilt besonders in heutigen Umgebungen, in denen sich Netzwerke häufig ändern und Verbindungen oft außer Reichweite geraten.
Selbst abgesehen vom Netzwerk sind Systemaufrufe ziemlich teuer und daher schlecht für die Performance.
Ich frage mich, ob jemand eine gute Möglichkeit kennt, TCP_NODELAY auf einem Socket zu aktivieren, wenn man keinen Zugriff auf den Quellcode der Anwendung hat.
Ich habe weder eine Kernel-Einstellung gefunden, die das dauerhaft anwendet, noch einen Befehl, der es nachträglich ändert.
Über
quickack 1in der Routing-Tabelle konnte ich Delayed ACK abschalten, aberTCP_NODELAYvon außerhalb der Anwendung zu aktivieren scheint besonders schwierig zu sein.Ich erlebe gerade exakt das hier beschriebene Problem zwischen einer Anwendung, die mir gehört, und einer Closed-Source-Anwendung, mit der sie interagiert.
socket(2)funktionieren?Man ruft die eigentliche Funktion auf, macht danach etwas wie
setsockoptund gibt dann den modifizierten Socket zurück.Wenn es ursprünglich
your_app —> serverist, macht man darausyour_app -> localhost_socat -> server.socat hat eine Kommandozeilenoption zum Setzen von
tcp_nodelay.Allerdings muss man die Closed-Source-App dazu bringen, sich mit localhost zu verbinden.
Wenn sie DNS-Lookups macht, kann man sie eventuell über einen Eintrag in
/etc/hostsdazu bringen, localhost zu verwenden.Die App kommuniziert dann über einen lokalen Socket mit socat, daher spielt
tcp_nodelayauf App-Seite keine Rolle.ptracesetsockoptaufrufen?/proc//fd/zu öffnen und dann Socket-Optionen zu setzen könnte funktionieren. Getestet habe ich es nicht.LD_PRELOADVor etwa 15 Jahren habe ich ein sehr echtzeitkritisches MMO gespielt, und die gesamte Kommunikation lief über TCP.
Wenn man einen Button anklickte, erschien die Aktion nicht einmal auf dem Bildschirm, bis ein Antwortpaket zurückkam.
Schließlich fanden alle Kinder, die dieses Spiel spielten – ich eingeschlossen –, heraus, dass das Aktivieren von TCP_NODELAY das Spiel deutlich flüssiger machte.
Besonders groß war der Effekt bei Spielern in California, die nahe an den Gameservern waren.
Ein interessanter Nebeneffekt war: Vor der Änderung blieb das Spiel kurz stehen, wenn der TCP-Stream stockte, und spielte danach die verpassten eingehenden Events sehr schnell ab.
Meistens war dieses Event mein Tod.
Nach der Änderung wurde stattdessen einfach die Verbindung getrennt.
Zugehörige Podcast-Folge von Oxide and Friends: https://www.youtube.com/watch?v=mqvVmYhclAg
Gilt nicht, wenn man eine moderne Sprache wie Go verwendet, die TCP_NODELAY standardmäßig aktiviert :-)
https://github.com/golang/go/issues/57530
Wusste ich nicht
Kann man nicht einfach eine „moderne“ Networking-Library verwenden?
Es ist nicht jedes Mal so. Manchmal ist es DNS
Bei einem Router in der Nähe einer Baustelle hatte sich Staub in der Lücke zwischen Laser und Glasfaser abgesetzt, das Signal ausreichend gedämpft, und es waren 40–50 % Paketverlust zu sehen
Nachdem die Verluststelle gefunden war, schickte das NOC dem entsprechenden Carrier eine E-Mail, und ein am nächsten Tag entsandter Techniker erzählte die Geschichte in seiner Antwort
Trotzdem ist das normalerweise kein großes Problem, weil man es per Patch umgehen kann
TCP_NODELAYoder Stream-BufferingDas wirklich komplexe System Web scheitert auch an Caches