GESCHRIEBEN IN KLAREM DEUTSCH.
Über uns
CLAY TRIBUNE.
ShopCartAccount
Anzeige

Russells Prinzipien für schnelle Tokio-Anwendungen)

Ein Blogbeitrag legt Prinzipien für das schnelle Schreiben von Tokio-Anwendungen dar, mit praktischen Hinweisen zu Fairness, Batching und der Messung von Latenz.

Von mitch·6 Min. Lesezeit
A visual representation of a Tokio runtime with glowing nodes indicating tasks and connections.

Die meisten asynchronen Rust-Programme stützen sich auf Tokio, das den Ruf erlangt hat, schwer zu debuggen und zu benchmarken. Vor RustConf veröffentlichte Russell einen Blog-Post mit allgemeinen Prinzipien für den Aufbau schneller Tokio-Anwendungen, und der Post wird derzeit auf Hacker News diskutiert.

Russell hat eine frühe Version eines Dokuments zusammengestellt, das zu einer Sammlung von Best Practices wachsen soll. Er hat die Leser gebeten, Bedenken zu äußern oder direkte Änderungen über Issues oder Pull Requests vorzunehmen. Er beabsichtigt, in Kürze eine funktionierende App hinzuzufügen, damit die Leute sehen können, wie die dial9-Trace mit den Richtlinien übereinstimmt.

Die unumstößlichen Regeln

Tokio-Runtimes kommen nicht mit vielen festen Regeln für den Aufbau schneller Workloads, so Russell. Die Antwort auf so viele Fragen lautet „es kommt darauf an“, da die Leistung eines Workloads vollständig davon abhängt, welche anderen Aufgaben ebenfalls auf dem Runtime ausgeführt werden, und zwar in diesem genauen Moment.

Anzeige

Deshalb treten so viele Probleme erst dann auf, wenn das System live geht. Das Erstellen von asynchroner Software, die reibungslos läuft, erfordert das Ausgleichen von Fairness und Batching, während gleichzeitig der Kontention gemanagt und Isolation sichergestellt wird.

Aufteilen für Latenz, Batching für Durchsatz

Die Optimierung für geringe Latenz über viele Anfragen hängt von einem einzigen Prinzip ab: häufiger relinquieren. Dieses Prinzip erfordert Fairness zwischen Verbindungen, sodass keine einzelne Anfrage den Rest aufhalten kann.

Nehmen wir Redis (oder eine ähnliche Anwendung mit Unterstützung für Request-Pipelining). Ein einfacher Ansatz liest Daten direkt von der Verbindung, bevor alle Daten angekommen sind. Mit gepipelinten Anfragen landet die gesamte gepipelinte Anfrage (oder fast die gesamte) in einem In-Memory-Puffer. Das Auslesen von Frames aus diesem Puffer erzeugt dann Poll::Ready-Ergebnisse, ohne jemals zum Netzwerk zurückkehren zu müssen.

Das Ergebnis sind lange Polls und ungleiche Behandlung der Benutzer. Der Durchsatz wird im Allgemeinen weniger beeinträchtigt — die Gesamtzahl der Anfragen bleibt gleich. Aber die Latenz verschiebt sich stark, da eine gesamte Kette von Operationen hinter einer einzigen vorhergehenden pausieren muss.

In diesem Beispiel reduziert das explizite Aufgeben der Kontrolle nach jeder Anfrage die Latenz um etwa das 10-fache. Man kann eine noch größere Reduktion erzielen, indem man die Kontrolle erst abgibt, nachdem mehrere Leseaktionen hintereinander sofort bereit sind.

Hier ist der Beispielcode, den Russell liefert:

The handle_conn method runs an endless loop that keeps checking for incoming data on the connection until shutdown is triggered. Inside that loop, it calls tokio::select!, which waits for two possible outcomes at once. One outcome is a call to self.connection.read_frame() returning a result; the other is a signal arriving on self.shutdown. When read_frame completes successfully, its result is stored in frame, and the method moves on to execute the command contained within it using execute_command(&self.db, &mut self.connection, frame).await. If the shutdown signal arrives instead, the loop ends immediately and the method returns success. The comment about fairness suggests adding tokio::task::yield_now().await between iterations, though it isn’t included in the code here.

Batching zur Amortisierung von Overhead

Fairness hat ihren Preis, und das zweite Prinzip ist Batching: Je mehr nützliche Arbeit eine Anwendung pro Runtime-Ereignis leisten kann – Aufgaben ändern, abfragen, zwischen Worker wechseln oder Threads umschalten – desto effizienter wird sie.

Tokio::fs dient Russells Paradebeispiel, und er treibt seinen Punkt gelegentlich noch weiter, indem er sagt, dass „tokio::fs als schädlich gilt.“ – das heißt, ohne io_uring führt Tokio jede Dateisystemoperation auf dem Blocking-Pool aus. Jeder Aufruf von spawn_blocking verursacht seine eigenen Kosten, und der Blocking-Pool wird für die gesamte Runtime gemeinsam genutzt.

Bevor Sie eine Reihe von Dateisystemoperationen – oder eine andere Art von Blockierarbeit – durchführen, sollten Sie diese Operationen zusammen in ein möglichst großes einzelnes Batch fassen. Manchmal ist es anstatt zu versuchen, alles innerhalb Ihrer Anwendung gleichzeitig zu bearbeiten, ein dediziertes OS-Thread tatsächlich der effizientere Ansatz.

Die Regel gilt unabhängig davon, wo Sie mit Tokio interagieren. Wann immer Sie wissen, dass eine Aufgabe die globale Warteschlange erreicht, kann das Kombinieren mehrerer Aufgaben in einem Batch diese Koordiniierungsbelastung insgesamt erleichtern.

Das Erzeugen einer Aufgabe ist ebenfalls kostengünstig, aber selbst das ist nicht kostenlos. Das Erstellen einer Aufgabe bleibt preiswert, aber wenn Sie 100 oder 1000 Aufgaben erzeugen, erfordert jede Instanz die individuelle Aufmerksamkeit der Runtime. Jede einzelne führt zu neuen Möglichkeiten für Planungsverzögerungen, fügt der Runtime eine weitere Abfrage hinzu, die verwaltet werden muss, und trägt im Allgemeinen zum Overhead bei.

Bevor Sie eine Aufgabe erzeugen, sollten Sie sich vor Augen führen, was Sie wirklich planen: Die Ausführung einer 10-Mikrosekunden-Arbeitseinheit auf ihrer eigenen Aufgabe ist wahrscheinlich mehr hinderlich als hilfreich. Sie können Tools wie dial9 oder tokio-metrics verwenden, um die Lebensdauer Ihrer Aufgaben zu überwachen.

Wie Sie erkennen, dass Sie ein Problem haben

In seiner Arbeit präsentiert Russell zwei Sätze von Markern, die auf Probleme hinweisen. Der erste Satz betrifft die Latenz.

  • P99 ist deutlich größer als P50.
  • Abfragen dauern länger, als die darin enthaltene Arbeit erfordern sollte.
  • Viele Spans fallen in einen einzelnen Poll.

Das zweite Thema ist Durchsatz:

  • Tokio-APIs wie spawn_blocking verbrauchen in Flamegraphs merkbaren Zeitaufwand.
  • Eine enge Schleife führt viele einzelne, kleine Dateisystem- oder blockierende Operationen durch.
  • Der Durchsatz verbessert sich, wenn die gleiche Arbeit in größere Einheiten zusammengefasst wird.

Ausnahmen und Metriken

Tokio ist nicht immer die Ursache des Problems, so Russell, der darauf hinweist, dass dial9 Licht auf die Bibliothek durch umfangreiche Sichtbarkeit geworfen hat. So oft dial9 ein tatsächliches Tokio-Problem aufdeckt, zeigt es genauso häufig die Abwesenheit eines solchen, was den Leuten das Vertrauen gibt, anderswo Antworten zu suchen.

Natürlich ist es manchmal ein Tokio-Problem.

Die hilfreichste Tokio-Metrik ist das Schedule-Latenz-Histogramm, das kürzlich hinzugefügt wurde. Schedule-Latenz misst, wie lange Tokio benötigt, um einen Future zu pollen, nachdem Ihre Aufgabe bereit ist, ausgeführt zu werden – beispielsweise, wenn das Socket Daten enthält. Diese Metrik verrät nicht die Ursache, ist aber das deutlichste Zeichen für Probleme zwischen Tokio und Ihrem Code.

Unsere Sichtweise

Diese Richtlinien erweisen sich als hilfreich. Die Beispiele sind spezifisch, und der Unterschied zwischen Latenz und Durchsatz wird klar erklärt. Es ist ein solider Ansatz, rückwärts von einer realen Metrik auszugehen, die Sie verbessern möchten.

Dieser Eintrag ist kein Rezept zum Kochen. Stattdessen präsentiert er eine Sammlung von Leitgedanken, und Russell räumt selbst ein, dass die Antwort auf zahlreiche Anfragen oft auf „kommt darauf an“ hinausläuft. Diese Bescheidenheit macht das gesamte Stück lesenswert.

Russells Versprechen einer Beispiel-App wird sich als nützlich erweisen. Die Verfolgung von dial9-Spuren neben diesen Prinzipien in der Praxis wird es den Lesern erleichtern zu verstehen, wie sie diese in die Praxis umsetzen können.

Jeder, der eine Tokio-Anwendung entwickelt, wird in diesem Beitrag einen soliden Ausgangspunkt finden. Es ersetzt keine sorgfältige Profilerstellung und Tests, sondern weist auf das hin, was untersucht werden sollte.

Das Notizbuch

Das Notizbuch abonnieren.

Die besten Geschichten des Tages und jedes neue Urteil, in klarem Deutsch, um sieben im Postfach. Eine Mail am Tag, nicht mehr.

Wir schicken eine Bestätigungsmail. Jede Ausgabe hat einen Abmeldelink, ein Klick genügt.

Anzeige

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Als Amazon-Partner verdient Clay Tribune an qualifizierten Verkäufen.