2 Punkte von GN⁺ 2024-05-10 | 1 Kommentare | Auf WhatsApp teilen
  • Probleme mit Latenz in verteilten Systemen lassen sich immer wieder allein durch das Aktivieren von TCP_NODELAY lö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_NODELAY aktiviert 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
  • 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_NODELAY aktivieren 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_NODELAY der Standard sein sollte, ist ebenfalls vertretbar
  • Code, der für jedes Byte write aufruft, kann mit TCP_NODELAY als 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_QUICKACK kann 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_QUICKACK das 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, dass write() tatsächlich ausgeführt wird

1 Kommentare

 
GN⁺ 2024-05-10
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.

    • Mir geht es ähnlich: Jedes Mal, wenn ich ein neues RPC-Framework sehe, habe ich das Hobby, ein GitHub-Issue zu eröffnen mit der Frage: „Habt ihr TCP_NODELAY berücksichtigt, oder schafft dieses Framework nur 20 Aufrufe pro Sekunde?“
      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.
    • Wenn der Zweck hauptsächlich darin besteht, Anwendungen mit schlechtem 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.
    • Zustimmung. Im Bereich High-Frequency/Low-Latency Trading ist es schon seit ziemlich langer Zeit, vermutlich seit mehr als 15 Jahren, gut bekannt, dass man den Nagle-Algorithmus abschaltet, und es ist auch eines der ersten Dinge, die ich prüfe.
    • Was man eigentlich will, ist, die Verzögerung auf n Mikrosekunden zu setzen; dafür gibt es aber keinen guten Weg außer, vor den Systemaufruf selbst User-Space-Pufferung zu setzen.
      Wenn es nichts wie io_uring gibt, das die Kosten von Systemaufrufen ausgleicht, funktioniert die User-Space-Seite besser.
    • Diese Logik war ursprünglich auf Dinge wie Telnet-Sessions zugeschnitten.
      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.

    • Der Unterschied zwischen QUIC und TCP liegt in TCPs Ursünde und der seiner Vorgänger: der Nachahmung einer asynchronen seriellen Port-Verbindung, bei der die Nachrichtenschicht unsichtbar ist.
      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.
    • Stimmt, aber diese konkrete Implementierung stützte sich auf eine Heuristik, die entscheidet, wie gebündelt wird, und diese Annahme scheint nicht gepasst zu haben.
    • Bündelung sollte nicht vom Protokoll, sondern von der Anwendung gesteuert werden.
      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_min und /proc/sys/net/ipv4/tcp_ato_min.
    FreeBSD hat net.inet.tcp.delayed_ack und net.inet.tcp.delacktime.

    • TCP_QUICKACK behebt 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.
    • Genau. TCP_QUICKACK bei 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?
    • Unter CentOS/RedHat kann man am Ende einer Route quickack 1 hinzufü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.

    • Es wäre schön, ein Protokoll mit einem eingebauten Mechanismus zu haben, der erkennt, dass die Gegenstelle aus irgendeinem Grund abgerissen ist.
    • Sollte QUIC(https://en.wikipedia.org/wiki/QUIC) nicht TCP-Probleme wie Latenz lösen?
  • 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.

    • Dann sollte man die Anwendung reparieren.
      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.
    • Wegen der Telnet-Erwähnung war ich neugierig, was OpenSSH macht: Es setzt TCP_NODELAY für alle Verbindungen, einschließlich interaktiver Sitzungen.
      Das habe ich sowohl durch Lesen des Codes als auch durch Beobachten des strace-Verhaltens bestätigt.
    • Wenn man von asynchroner Ein-/Ausgabe ausgeht, ist das Puffern statt Blockieren bei jedem kleinen 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.
    • Nur weil einige Entwickler miserablen Code schreiben, sollte nicht das ganze Internet bestraft werden.
    • So sollte man es von vornherein nicht machen.
      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 1 in der Routing-Tabelle konnte ich Delayed ACK abschalten, aber TCP_NODELAY von 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.

    • Könnte nicht so etwas wie ein LD_PRELOAD-Intercept für socket(2) funktionieren?
      Man ruft die eigentliche Funktion auf, macht danach etwas wie setsockopt und gibt dann den modifizierten Socket zurück.
    • Je nach konkreter Situation könnte man vielleicht socat dazwischenschalten.
      Wenn es ursprünglich your_app —> server ist, macht man daraus your_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/hosts dazu bringen, localhost zu verwenden.
      Die App kommuniziert dann über einen lokalen Socket mit socat, daher spielt tcp_nodelay auf App-Seite keine Rolle.
    • Könnte man nicht einen Debugger anhängen und per ptrace setsockopt aufrufen?
    • /proc//fd/ zu öffnen und dann Socket-Optionen zu setzen könnte funktionieren. Getestet habe ich es nicht.
    • LD_PRELOAD
  • Vor 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.

    • Ich weiß nicht, ob WoW gemeint ist, aber ungefähr zu der Zeit hat ein Spielupdate genau diese Änderung vorgenommen und vermutlich auch noch anderes geändert.
      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

    • Das war eine hervorragende Folge und hat die Bedeutung von Visualisierung wirklich eindrucksvoll gezeigt.
  • Gilt nicht, wenn man eine moderne Sprache wie Go verwendet, die TCP_NODELAY standardmäßig aktiviert :-)

  • Es ist nicht jedes Mal so. Manchmal ist es DNS

    • Einmal führte eine defekte Line Card in einem Router dazu, dass das letzte Bit von IPv4-Adressen auf 0 gesetzt wurde, woraus ein Ticket „nur gerade IPv4-Adressen erreichbar“ entstand
    • In meinem Fall war es einmal schmutziges Glas
      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
    • Einmal in 50 Jahren, in 2 Milliarden km Entfernung, kann es auch ein defekter Speicherchip sein
      Trotzdem ist das normalerweise kein großes Problem, weil man es per Patch umgehen kann
    • BGP sollte man auch nicht vergessen, ebenso wenig wie eine Festplatte, die ohne Benachrichtigung vollgelaufen ist
    • Wenn etwas fehlschlägt, ist es DNS; wenn es einfach stehen bleibt, ist es TCP_NODELAY oder Stream-Buffering
      Das wirklich komplexe System Web scheitert auch an Caches