Zum Hauptinhalt springen

7 Posts getaggt mit "Performance"

Durchsatz, Latenz und Skalierung in verteilten Systemen.

Alle Tags anzeigen

Viele Keys, wenige Round-Trips: Wie Zaris eine Bulk-Operation per Fan-out verteilt

· 12 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Eine Bulk-Operation per Fan-out an die besitzenden Nodes verteilen

Fragen Sie einen verteilten Store nach einem Key, wird der Aufwand von einer einzigen Zahl dominiert: dem Round-Trip zum Node, der ihn besitzt. Der Lookup selbst dauert Nanosekunden; das Netzwerk Mikrosekunden. Fragen Sie nun nach tausend Keys. Wenn Sie in einer Schleife tausendmal Get aufrufen, zahlen Sie diesen Round-Trip tausendmal, seriell, und der Store verbringt fast die gesamte Wanduhrzeit untätig mit dem Warten auf Pakete. Die Keys könnten auf vier Nodes verteilt sein, die parallel hätten antworten können — aber eine naive Schleife gibt ihnen nie die Chance.

Zaris hat drei Bulk-APIs — GetManyAsync, PutManyAsync, DeleteManyAsync — und darunter ein gemischtes ExecuteBatchAsync, und ihr ganzer Daseinszweck ist, „tausend Keys" in „eine Anfrage pro besitzendem Node, parallel abgesetzt" zu verwandeln. Dieser Beitrag ist der Fan-out-Pfad von Anfang bis Ende: wie der Client Keys nach Owner in Buckets sortiert, warum alle vier APIs auf eine einzige Engine zusammenfallen, wie ein Index-Feld die Antworten durch zwei Schichten von Nebenläufigkeit in Reihenfolge hält und warum — anders als bei einer Multi-Key-Transaktion — eine Bulk-Operation pro Item arbeitet und ein Teilerfolg normal ist.

Ein Hop zum Owner: Wie der Zaris-Client einen Key routet

· 12 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Wie der Zaris-Client einen Key zu seinem Owner routet

Es gibt zwei ehrliche Wege, einen Client für einen verteilten Cache zu bauen. Der erste: irgendeinen Node wählen, ihm die Anfrage senden und den Cluster sie intern an den Besitzer des Keys weiterleiten lassen — einfach für den Client, aber jede Anfrage, die auf dem falschen Node landet, zahlt für einen zusätzlichen Netzwerk-Hop innerhalb des Clusters. Der zweite: den Client klug genug machen, um zu wissen, welcher Node den Key besitzt, bevor er irgendetwas sendet, sodass der erste Socket, den er berührt, derjenige ist, der tatsächlich antworten kann. Zaris geht den zweiten Weg. Ein GET verlässt Ihren Prozess bereits an den Node adressiert, der den Key hält — kein Proxy, kein Weiterleitungs-Hop, kein Koordinator in der Mitte.

Dieser Beitrag ist der Blick ins Innere, wie das funktioniert: die Funktion, die einen Key in einen Owner verwandelt, die Lookup-Tabelle, die der Client im Speicher hält, und — der Teil, den man leicht falsch macht — wie der Client korrekt bleibt, wenn sich das Ownership bewegt, während Ihr Traffic unterwegs ist.

Der Latenzboden: Eine Mikrosekunde Arbeit in zehn Mikrosekunden Installation

· 11 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Eine Mikrosekunde Arbeit in zehn Mikrosekunden Installation

Hier ist eine Zahl, die verändert, wie man über einen Key/Value-Store denkt. Wenn wir den In-Memory-Store von Zaris isolieren und ihm Operationen zuführen, ohne dass etwas anderes im Weg ist — kein Socket, keine Serialisierung, kein Routing —, bedient ein einzelner Thread mehr als eine Million Lesezugriffe pro Sekunde und skaliert sauber auf mehr als acht Millionen über acht Threads. Der Store erledigt die eigentliche Arbeit eines GET also in unter zwei Mikrosekunden und ist ausdrücklich nicht der Engpass.

Und doch erreicht ein vollständiges GET über eine echte TCP-Verbindung auf derselben Maschine ein Plateau bei einigen Hunderttausend pro Sekunde. Der Store kann die Arbeit fünfmal schneller erledigen, als der Server sie liefern kann. Die interessante Performance-Frage zu Zaris lautet also nicht „Wie schnell ist der Store?" Sondern genau umgekehrt: Wohin geht die ganze Zeit zwischen Socket und Store? Dieser Beitrag ist die Antwort, Operation für Operation nachverfolgt, denn die Lücke zwischen 1,7 Mikrosekunden Logik und 10 bis 13 Mikrosekunden Server-CPU pro Operation ist kein Rauschen — sie ist der Latenzboden, und fast alles davon ist Installation.

Die Worker-Pool-Obergrenze: Wenn ein Node bei 25 % CPU ein Plateau erreicht

· 8 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Ein Node bei nahezu 25 % CPU mit flachem Durchsatz, weil der Dispatch-Worker-Pool die Obergrenze war

Hier ist ein Benchmark-Ergebnis, das wie ein Widerspruch aussieht. Ein Zaris-Node, der eine leselastige Workload bedient, erreicht seine Obergrenze bei knapp unter einer halben Million Ops/s und weigert sich zu steigen — fügen Sie mehr Client-Verbindungen und mehr Lastgeneratoren hinzu, die Zahl bewegt sich nicht. Die naheliegende Diagnose lautet „er ist gesättigt." Nur sagt das CPU-Diagramm etwas anderes: Der Node steht bei etwa einem Viertel seiner verfügbaren CPU. Drei von vier Kernen sind untätig, und der Durchsatz ist flach.

Das ist kein CPU-limitierter Node und — wenn Sie das sorgfältig ausgeschlossen haben — auch kein Client-limitierter Benchmark. Es ist eine dritte Sache, und zwar ein Klassiker: Der Worker-Pool für das Request-Dispatching war schmaler, als die Arbeit es wollte, sodass Requests in der Warteschlange standen, während Kerne Däumchen drehten.

Allokationsfreie Hot Paths: Wie der Zaris-.NET-Client ValueTask nutzt

· 6 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Der Zaris-.NET-Client nutzt ValueTask in seinen Hot-Path-Operationen

Meistens ist Task<T> genau der richtige Rückgabetyp für eine asynchrone Methode, und man sollte nicht zweimal darüber nachdenken. Es gibt aber einen bestimmten Ort, an dem er unbemerkt Kosten verursacht: ein Hot Path, der synchron abschließt und millionenfach aufgerufen wird.

Der Mechanismus: Task<T> ist ein Referenztyp. Jeder Aufruf, der einen zurückgibt, allokiert ein Task-Objekt auf dem Heap — selbst wenn die Operation synchron abschließt. Ein Cache-Treffer direkt aus einem lokalen Puffer, ein Awaitable, das bereits fertig ist: Nichts davon erfordert echte Asynchronität, dennoch zahlen Sie für ein Heap-Objekt, das das Ergebnis trägt. Eine Allokation ist nichts. Bei der Aufrufrate eines Clients mit hohem Durchsatz — einer engen Schleife mit Millionen kleiner Gets und Puts — ist das ein stetiger Strom kleiner Heap-Allokationen, und dieser Strom füttert den Garbage Collector. Mehr Gen-0-Collections, mehr CPU für GC statt für Ihre Arbeit, mehr Jitter bei den Tail-Latenzen.

Der Zaris-.NET-Client setzt genau dort an, wo es zählt, und nirgendwo sonst. Dieser Beitrag handelt von dieser Entscheidung: welche Methoden sich geändert haben, warum, welche Regeln Sie für den korrekten Einsatz befolgen müssen und — bewusst — welche Methoden wir unangetastet gelassen haben.

Große Werte, kleine Pausen: Large-Object-Chunking in Zaris

· 6 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Large-Object-Chunking hält große Werte vom LOH fern

Es gibt eine Zahl, die sich jeder .NET-Performance-Ingenieur irgendwann merkt: 85.000 Bytes. Jede einzelne Allokation ab etwa dieser Größe landet nicht auf dem normalen Heap für kleine Objekte, sondern auf dem Large Object Heap (LOH). Der LOH ist ein anderes Tier. Er wird nur bei einer Gen-2-(vollständigen) Garbage Collection eingesammelt und standardmäßig nicht kompaktiert. Diese Kombination ist in Ordnung für einen großen Puffer, den man einmal allokiert und behält. Sie ist nicht in Ordnung für einen latenzarmen In-Memory-Store, der wiederholt große Werte schreibt und überschreibt.

Stellen Sie sich die Last vor: große serialisierte Blobs, umfangreiche JSON-Dokumente, Bilder, Protokoll-Payloads — gespeichert, aktualisiert, ersetzt, immer wieder. Jeder dieser großen Schreibvorgänge legt ein frisches Objekt auf dem LOH ab. Da der LOH nicht kompaktiert wird, verschmelzen freigegebene Bereiche nicht zu zusammenhängendem Speicher, sondern fragmentieren. Und weil jede Rückgewinnung eine vollständige Gen-2-Collection bedeutet, werden die Pausen länger und treten häufiger auf, genau dann, wenn der Working Set wächst. Für einen Store, dessen ganzes Versprechen niedrige Latenz ist, ist das die falsche Art von Überraschung.

Zaris liefert ein optionales Feature, das den LOH für solche Werte komplett umgeht. Es ist standardmäßig aus, und dieser Beitrag erklärt, was es tut, wie es funktioniert und — ebenso wichtig — wann Sie es einschalten sollten und wann nicht.

Lesezugriffe, die mit Ihren Cores skalieren

· 5 Minuten Lesezeit
Clustron Team
Engineering für verteilte Systeme

Der GET-Durchsatz von Zaris skaliert mit den Cores

Redis ist schnell. Es ist aber auch, konstruktionsbedingt, auf dem Datenpfad single-threaded: eine Event-Loop, ein Core, ein Befehl nach dem anderen. Das ist ein wirklich gutes Design — es macht Redis leicht nachvollziehbar und beseitigt eine ganze Kategorie von Nebenläufigkeitsfehlern. Aber es hat eine Obergrenze, die sich nicht mit Hardware kaufen lässt. Geben Sie einer Engine mit einer einzigen Event-Loop eine 4-Core-, eine 8-Core- oder eine 32-Core-Maschine, und der Lese-Durchsatz landet an derselben Stelle. Die zusätzlichen Cores bleiben ungenutzt.

Zaris geht den anderen Weg. Keys sind partitioniert, und jede Partition hat einen Besitzer, sodass Lesezugriffe auf verschiedene Keys auf verschiedenen Cores landen und gleichzeitig laufen. Das Ergebnis ist ein Durchsatz, der sich beim Hinzufügen von Cores bewegt — statt einer flachen Linie ein Anstieg.