Wie im , gab es ein Problem mit dem verteilten Dienst, nennen wir ihn Elvin. Diesmal habe ich das Problem nicht selbst entdeckt, die Jungs aus der Kundenabteilung haben mich informiert.
Eines Morgens wachte ich von einer unzufriedenen E-Mail wegen großer Verzögerungen bei Elvin auf, den wir in naher Zukunft starten wollten. Insbesondere hatte der Kunde eine Verzögerung im 99. Percentil von etwa 50 ms, was weit über unserem Budget für Verzögerungen liegt. Es war erstaunlich, da ich den Dienst sorgfältig getestet hatte, insbesondere die Latenz, die ein häufiges Beschwerdethema ist.
Bevor ich Elvin zum Testen freigab, habe ich viele Experimente mit 40.000 Anfragen pro Sekunde (QPS) durchgeführt, und alle zeigten eine Verzögerung von weniger als 10 ms. Ich war bereit zu behaupten, dass ich mit ihren Ergebnissen nicht einverstanden war. Aber als ich noch einmal auf die E-Mail schaute, fiel mir etwas Neues auf: Ich hatte die Bedingungen, die sie erwähnten, nicht genau getestet; ihre QPS war viel niedriger als meine. Ich hatte bei 40k QPS getestet, sie nur bei 1k. Ich führte ein weiteres Experiment durch, diesmal mit einem niedrigeren QPS, nur um sie zufriedenzustellen.
Da ich darüber in meinem Blog schreibe, haben Sie wahrscheinlich schon verstanden: Ihre Zahlen waren korrekt. Ich habe meinen virtuellen Clienten immer wieder überprüft, und das Ergebnis blieb dasselbe: eine niedrige Anzahl von Anfragen erhöht nicht nur die Latenz, sondern auch die Anzahl der Anfragen mit einer Latenz von über 10 ms. Mit anderen Worten, während bei 40k QPS etwa 50 Anfragen pro Sekunde die 50 ms überschritten, waren es bei 1k QPS pro Sekunde 100 Anfragen über 50 ms. Ein Paradox!

Den Suchkreis eingrenzen
Wenn man mit dem Problem der Latenz in einem verteilten System mit vielen Komponenten konfrontiert ist, sollte man zunächst eine kurze Liste der Verdächtigen erstellen. Lassen Sie uns etwas tiefer in die Architektur von Elvin eintauchen:

Ein guter Ausgangspunkt ist die Liste der durchgeführten Eingabe-/Ausgabeübergänge (Netzwerkanfragen/Datensuche usw.). Lassen Sie uns herausfinden, wo die Latenz liegt. Neben der offensichtlichen Eingabe/Ausgabe mit dem Client geht Elvin einen Schritt weiter: Er greift auf den Datenspeicher zu. Dieses Speicher arbeitet jedoch in einem Cluster mit Elvin, daher sollte die Latenz dort geringer sein als beim Client. Also, die Liste der Verdächtigen:
- Netzwerkanfrage vom Client zu Elvin.
- Netzwerkanruf von Elvin zum Speichersystem.
- Disk-Suche im Speichersystem.
- Netzwerkanruf aus dem Speichersystem zu Elvin.
- Netzwerkanruf von Elvin zum Client.
Versuchen wir, einige Punkte auszuschließen.
Das Speichersystem ist nicht das Problem.
Zuerst habe ich Elvin in einen Ping-Pong-Server umgewandelt, der keine Anfragen bearbeitet. Er sendet leere Antworten zurück, wenn er eine Anfrage erhält. Wenn die Latenz sinkt, liegt das Problem bei Elvin oder beim Speichersystem – nichts Ungewöhnliches. Im ersten Experiment erhalten wir folgendes Diagramm:

Wie wir sehen, zeigt der Ping-Pong-Server keine Verbesserungen. Das bedeutet, dass das Speichersystem die Latenz nicht erhöht und die Liste der Verdächtigen halbiert wird:
- Netzwerkanfrage vom Client zu Elvin.
- Netzwerkanruf von Elvin zum Client.
Toll! Die Liste wird schnell kürzer. Ich dachte, ich hätte die Ursache beinahe herausgefunden.
gRPC
Jetzt ist es Zeit, Ihnen einen neuen Spieler vorzustellen: . Es handelt sich um eine Open-Source-Bibliothek von Google für die Interprozesskommunikation. . Obwohl gRPC gut optimiert und weit verbreitet ist, benutze ich es zum ersten Mal in einem System dieser Größenordnung, und ich erwartete, dass meine Implementierung – gelinde gesagt – suboptimal sein würde.
Vorhandensein von gRPC im Stack hat eine neue Frage aufgeworfen: Könnte es meine Implementierung oder die Ursache der Verzögerung sein? gRPC Wir fügen einen neuen Verdächtigen zur Liste hinzu:
- Der Client ruft die Bibliothek auf
gRPC - Die Bibliothek
gRPCführt einen Netzwerkaufruf der Bibliothek auf dem Client durchgRPCauf dem Server - Die Bibliothek
gRPCfragt Elvin an (keine Operation im Fall des Ping-Pong-Servers)
Um Ihnen zu zeigen, wie der Code aussieht, unterscheidet sich meine Implementierung des Clients/Elvin nicht wesentlich von den client-server .
Hinweis: Die obige Liste ist etwas vereinfacht, da
gRPCes die Verwendung eines eigenen (vorlagebasierten?) Thread-Modells ermöglicht, in dem sich die AusführungsstapelngRPCund die Benutzerimplementierung verflechten. Der Einfachheit halber halten wir an diesem Modell fest.
Profiling wird alles lösen.
Nachdem ich die Datenspeicher ausgeschlossen habe, dachte ich, ich wäre fast fertig: „Jetzt ist es einfach! Wir wenden das Profil an und erkennen, wo die Verzögerung auftritt.“ Ich da die CPU sehr schnell ist und meist nicht der Engpass ist. Die meisten Verzögerungen treten auf, wenn der Prozessor die Verarbeitung stoppen muss, um etwas anderes zu tun. Präzises CPU-Profiling ist genau dafür ausgelegt: es zeichnet alles genau auf. und zeigt, wo Verzögerungen auftreten.
Ich habe vier Profile analysiert: eines mit hohen QPS (geringe Latenz) und ein weiteres mit einem Ping-Pong-Server bei niedrigem QPS (hohe Latenz), sowohl auf der Client- als auch auf der Serverseite. Außerdem habe ich zur Sicherheit ein Beispiel für die CPU-Last aufgenommen. Beim Vergleich der Profile suche ich normalerweise nach anomalen Aufrufstapeln. Zum Beispiel treten auf der schlechten Seite mit hoher Latenz viel mehr Kontextwechsel auf (10-mal oder mehr). In meinem Fall war die Anzahl der Kontextwechsel jedoch nahezu identisch. Zu meinem Entsetzen gab es dort nichts Wesentliches.
Weitere Fehlersuche
Ich war verzweifelt. Ich wusste nicht, welche anderen Werkzeuge ich verwenden könnte, und mein nächster Plan bestand im Wesentlichen darin, Experimente mit verschiedenen Variationen durchzuführen, ohne das Problem klar zu diagnostizieren.
Was wäre, wenn
Von Anfang an machte mir die spezifische Verzögerungszeit von 50 ms Sorgen. Das ist eine sehr lange Zeit. Ich beschloss, Teile des Codes auszuschneiden, bis ich genau herausfinden konnte, welcher Teil den Fehler verursacht. Dann folgte ein Experiment, das funktionierte.
Wie so oft erscheint im Nachhinein alles klar. Ich habe den Kunden auf eine Maschine mit Elvin gesetzt und eine Anfrage gesendet. localhostUnd die erhöhte Latenz verschwunden!

Irgendwas stimmte mit dem Netzwerk nicht.
Das Können eines Netzwerkingenieurs erlernen.
Ich muss gestehen: Mein Wissen über Netzwerktechnologien ist schrecklich, besonders wenn man bedenkt, dass ich täglich damit arbeite. Aber das Netzwerk war der Hauptverdächtige, und ich musste lernen, wie ich es debuggen kann.
Glücklicherweise liebt das Internet diejenigen, die lernen wollen. Die Kombination aus Ping und Tracert schien ein guter Anfang zur Fehlersuche bei Netzwerktransportproblemen zu sein.
Zuerst habe ich auf dem TCP-Port von Elvin gestartet. Ich habe die Standardeinstellungen verwendet – nichts Besonderes. Von über tausend Pings überschritt keiner 10 ms, außer dem ersten zum Aufwärmen. Das steht im Widerspruch zur beobachteten Latenzsteigerung von 50 ms im 99. Perzentil: Da hätten wir bei 100 Anfragen etwa eine mit 50 ms Verzögerung sehen müssen.
Dann habe ich : vielleicht lag das Problem an einem der Knoten auf dem Weg zwischen Elvin und dem Kunden. Aber auch der Tracer kam mit leeren Händen zurück.
Somit lag der Grund für die Verzögerung nicht an meinem Code, nicht an der Implementierung von gRPC und nicht am Netzwerk. Ich begann bereits zu befürchten, dass ich das nie verstehen würde.
Welche Betriebssysteme verwenden wir jetzt?
gRPC ist weit verbreitet in Linux, jedoch ist es für Windows exotisch. Ich entschied mich, ein Experiment durchzuführen, das funktionierte: Ich erstellte eine Linux-VM, kompilierte Alvin für Linux und setzte ihn ein.

Und das ergab sich: Im Ping-Pong-Server gab es unter Linux keine derartigen Verzögerungen wie bei einem entsprechenden Knoten unter Windows, obwohl die Datenquelle identisch war. Das Problem liegt offenbar an der Implementierung von gRPC für Windows.
Nagle-Algorithmus
Ich dachte die ganze Zeit, dass mir ein Flag fehlt. gRPCJetzt habe ich verstanden, dass es in Wirklichkeit gRPC das Windows-Flag fehlt. Ich fand eine interne RPC-Bibliothek, von der ich überzeugt war, dass sie gut für alle gesetzten Flags funktioniert. . Dann fügte ich all diese Flags in gRPC hinzu und setzte Alvin auf Windows im korrigierten Ping-Pong-Server unter Windows ein!

Fast bereit: Ich begann, die hinzugefügten Flags einzeln zu entfernen, bis die Regression zurückkehrte, sodass ich die Ursache genau identifizieren konnte. Es war der berüchtigte , Nagle-Algorithmus-Schalter.
versucht, die Anzahl der über das Netzwerk gesendeten Pakete zu reduzieren, indem die Übertragung von Nachrichten verzögert wird, bis die Paketgröße ein bestimmtes Byte-Limit überschreitet. Obwohl dies für den durchschnittlichen Benutzer angenehm sein kann, ist es für Echtzeit-Server schädlich, da das Betriebssystem einige Nachrichten zurückhält, was zu Verzögerungen bei niedrigem QPS führt. Bei gRPC wurde dieses Flag in der Linux-Implementierung für TCP-Sockets gesetzt, jedoch nicht für Windows. Ich habe das .
Fazit
Eine große Verzögerung bei niedrigem QPS wurde durch eine Betriebssystemoptimierung verursacht. Rückblickend hat das Profiling keine Verzögerung festgestellt, da es im Kernel-Modus und nicht im Ich weiß nicht, ob man den Nagle-Algorithmus durch ETW-Aufzeichnungen beobachten kann, aber das wäre interessant.
Was das Experiment localhost betrifft, so hat es wahrscheinlich keinen Einfluss auf den tatsächlichen Netzwerk-Code gehabt, und der Nagle-Algorithmus wurde nicht aktiviert, weshalb die Verzögerungsprobleme verschwunden waren, als der Client über localhost auf Elvin zugegriffen hat.
Das nächste Mal, wenn Sie eine Zunahme der Verzögerung bei der Reduzierung der Anzahl der Anfragen pro Sekunde beobachten, sollte der Nagle-Algorithmus auf Ihrer Verdächtigenliste stehen!
Quelle: habr.com
