Das Problem ist immer TCP_NODELAY
(brooker.co.za)- Beim Debugging von Latenzproblemen in verteilten Systemen sollte man als Erstes die Einstellung von TCP_NODELAY prüfen
- Der Nagle-Algorithmus wurde 1984 in RFC896 vorgeschlagen und entwickelt, um beim Senden kleiner Pakete den Overhead von TCP-Headern zu reduzieren
- In Kombination mit dem Mechanismus Delayed ACK führt er jedoch dazu, dass die Datenübertragung bis zum Empfang eines ACK verzögert wird, was die Leistung latenzsensitiver Anwendungen verschlechtert
- In modernen Rechenzentrumsumgebungen ist die RTT sehr kurz, und die meisten Systeme senden ohnehin bereits große Nachrichten, sodass der Nutzen des Nagle-Algorithmus fast vollständig verschwunden ist
- Deshalb sollte in modernen verteilten Systemen TCP_NODELAY standardmäßig aktiviert sein; der Nagle-Algorithmus wird nicht mehr benötigt
Hintergrund des Nagle-Algorithmus
- John Nagles RFC896 von 1984 wurde vorgeschlagen, um das Problem eines 4000-%-Overheads bei 1 Byte Nutzdaten gegenüber 40 Byte Header zu lösen, wie es bei kleinen Datenübertragungen wie Tastatureingaben auftrat
- Das damalige Problem war, dass bei jeder einzelnen Tastatureingabe kleine Pakete gesendet wurden und dadurch die Netzwerkeffizienz sank
- Die Lösung bestand darin, das Senden neuer Segmente zu unterbinden, solange frühere Daten noch nicht per ACK bestätigt wurden
- Dieser Ansatz war in der damaligen Netzwerkumgebung effektiv, ist aber für moderne Systeme, in denen Latenz entscheidend ist, ungeeignet
Zusammenspiel von Nagle-Algorithmus und Delayed ACK
- Delayed ACK (RFC813, RFC1122) bedeutet, dass der Empfänger ACKs nicht sofort sendet, sondern sie verzögert, bis Antwortdaten vorliegen oder ein Timer abläuft
- Der Nagle-Algorithmus wartet auf ein ACK und stoppt die Übertragung, während Delayed ACK das ACK hinauszögert; dadurch entsteht ein Deadlock, bei dem beide Seiten aufeinander warten
- John Nagle selbst bezeichnete diese Kombination als eine „schreckliche Kombination“ und wies darauf hin, dass beide Funktionen zwar unabhängig voneinander eingeführt wurden, gemeinsam aber Latenz verursachen
Probleme in modernen Umgebungen
- Innerhalb eines Rechenzentrums liegt die RTT bei etwa 500 μs, selbst innerhalb derselben Region nur bei wenigen Millisekunden
- In solchen Umgebungen führt bereits eine Verzögerung um eine RTT zu Leistungsverlusten
- Zudem senden moderne verteilte Systeme durch TLS, Serialisierung und Protokoll-Overhead ohnehin bereits ausreichend große Nachrichten, sodass das Problem einzelner Byte-Pakete praktisch nicht mehr existiert
- Die Optimierung kleiner Nachrichten wird heute auf Anwendungsebene vorgenommen
Warum TCP_NODELAY nötig ist
- In latenzsensitiven verteilten Systemen wird empfohlen, TCP_NODELAY zu aktivieren und damit den Nagle-Algorithmus zu deaktivieren
- Das ist weder „ineffizient“ noch eine „falsche Konfiguration“, sondern eine Entscheidung, die zu moderner Hardware und heutigen Traffic-Mustern passt
- Der Autor argumentiert, dass TCP_NODELAY der Standard sein sollte
- Zwar kann Code, der bei jedem
write()-Aufruf sendet, dadurch langsamer werden, aber solcher Code sollte grundsätzlich überarbeitet werden
- Zwar kann Code, der bei jedem
Weitere verwandte Optionen
- Die Option TCP_QUICKACK reduziert ACK-Verzögerungen, ist aber wegen Portabilitätsproblemen und inkonsistentem Verhalten keine grundlegende Lösung
- Das Kernproblem ist, dass der Kernel Daten länger zurückhält, als es die Anwendung beabsichtigt, obwohl sie bei einem
write()-Aufruf sofort gesendet werden sollten
Fazit
- Der Nagle-Algorithmus war eine hervorragende Erfindung, um in früheren Netzwerken die Effizienz zu steigern,
ist jedoch in modernen Hochgeschwindigkeitsnetzwerken und verteilten Systemen eine überholte Funktion, die vielmehr Latenz verursacht - Daher wird die konsequente Aktivierung von TCP_NODELAY als grundlegendes Prinzip moderner Systemarchitektur vorgeschlagen
1 Kommentare
Hacker-News-Kommentare
Damals teilten sich mehrere Hosts einen Ethernet-Kanal, daher wurde CSMA/CD verwendet, um Kollisionen zu vermeiden.
Heute ist Ethernet jedoch meist Point-to-Point aufgebaut, in einer Vollduplex-Umgebung, in der Senden und Empfangen gleichzeitig möglich sind.
Deshalb wird CSMA nicht mehr benötigt, und es erscheint in den meisten Fällen sinnvoll, den Nagle-Algorithmus durch Setzen von TCP_NODELAY zu deaktivieren.
Dass er als Standard gesetzt wurde, halte ich für einen der großen Fehler der Netzwerkgeschichte.
Als wir um 2014 Datacenter-Switches austauschten, mussten wir einige Altgeräte behalten, weil kein 10-Mbit-Halbduplex mehr unterstützt wurde.
Er verhindert die Erzeugung zu vieler kleiner Pakete.
Nagle ist eine Optimierung auf TCP-Ebene und bündelt kleine Pakete für mehr Effizienz.
CSMA betrifft die physische Ebene bzw. die Sicherungsschicht und ist von Nagle unabhängig.
Das in Go geschriebene Backend hatte standardmäßig bereits TCP_NODELAY gesetzt, also war das nicht die Ursache, aber die Diskussion über die Problemwahrnehmung rund um Nagle fand ich interessant.
Es gab auch schon frühere Diskussionen dazu, siehe diesen Thread.
Bei chatartiger Kommunikation wie im DICOM-Protokoll verbessert TCP_NODELAY=1 den Durchsatz deutlich.
Siehe diesen Link.
Ich glaube nicht, dass Delayed ACK bei heutigen Workloads noch große Vorteile bringt.
In der modernen, HTTP-zentrierten Umgebung ist es meiner Ansicht nach besser, sowohl Nagle als auch Delayed ACK zu deaktivieren.
Bei RTTs zwischen Datacentern im Bereich von einigen hundert Mikrosekunden kann schon die Verzögerung um eine einzige RTT eher schaden.
Wikipedia-Link
Wann gesendet und wann gepuffert wird, sollte die Anwendung entscheiden.
Unter Linux ist das ein Hinweis an den Kernel, dass bald weitere Daten gesendet werden, was nützlich ist, wenn Header und Daten getrennt verschickt werden.
Zusammen mit io_uring ist es noch effizienter.
Es wäre gut, wenn man nach einer Nachricht, die eine sofortige Antwort erfordert, den Puffer explizit leeren und senden könnte.
Heutige TCP-Kanäle sind noch komplexer, weil sich synchrone und asynchrone Nachrichten mischen.
Ich wünschte, Protokolle wie SCTP hätten sich stärker verbreitet.
Auch bei Wrapping wie TLS ist es mühsam, Nachrichtengrenzen zu erkennen.
Idealerweise sollte man ein Bit für „Pufferung erlaubt“ setzen können, um große Übertragungen aufzuteilen, und am Ende „sofort senden“ angeben können.
TCP_CORK ist immerhin eine ähnliche Alternative, aber etwas grob.
Bei Datei-I/O gibt es ein ähnliches Problem.
Ziemlich interessant.
Die Anwendung sollte das Verhältnis zwischen Latenz und Durchsatz selbst steuern können.
Auf Anwendungsebene wäre die Umsetzung aber ineffizient, weil man dazu unacked data kennen müsste.
Schon ein einfacher 20-ms-Flush-Timer wäre viel besser gewesen.