Lesezugriffe, die mit Ihren Cores skalieren
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.
Die Zahlen
Wir haben den GET-Durchsatz über das RESP-Frontend von Zaris gemessen — denselben Redis-Protokollpfad, den ein gewöhnlicher Redis-Client nutzen würde — und mit Redis und Memurai auf derselben Hardware verglichen.
| Engine | 1 Core | 4 Cores |
|---|---|---|
| Zaris (RESP GET) | ~483.000 Ops/Sek. | ~946.000 Ops/Sek. |
| Redis / Memurai | ~740–780.000 Ops/Sek. | ~740–780.000 Ops/Sek. (flach) |
Zwei Dinge stimmen hier, und wir möchten bei beiden ehrlich sein.
Erstens, der ehrliche Vorbehalt: Der Wechsel von 1 auf 4 Cores verdoppelt den Zaris-Durchsatz ungefähr, er vervierfacht ihn nicht. Das entspricht einer Skalierungseffizienz von etwa 0,5 — sublinear. Core-übergreifende Koordination, Speicherbandbreite und der Netzwerkpfad fordern alle ihren Tribut. Wer Ihnen erzählt, dass ein verteilter Store perfekt linear skaliert, will Ihnen etwas verkaufen.
Zweitens, der Teil, der zählt: Zaris steigt, und eine Engine mit einer einzelnen Event-Loop kann das nicht. Auf einem Core liegen Redis und Memurai tatsächlich vorn — eine straffe Single-Thread-Loop ohne Core-übergreifende Koordination ist auf einem Core schwer zu schlagen. Doch diese Zahl ist zugleich ihre Obergrenze. Zaris startet niedriger und zieht vorbei, sobald Cores hinzukommen, weil die Arbeit tatsächlich parallel ist. Auf einer großen modernen Maschine schlägt „skaliert mit einer Effizienz von 0,5“ ein „auf einen Core festgenagelt“ jedes Mal.
Warum es skaliert
Eine einzelne Event-Loop serialisiert alles: jedes GET, jedes SET, jeden Client, alles durch einen Thread geleitet. Das ist Cache-freundlich und lock-frei, aber im Grunde die Arbeit eines einzigen Cores.
Zaris dagegen gibt jeder Partition einen Besitzer und verteilt die Arbeit pro Core. Zwei Clients, die zwei Keys in verschiedenen Partitionen lesen, berühren niemals denselben Thread. Mit mehr Cores und mehr Partitionen laufen mehr dieser Lesezugriffe gleichzeitig.
Und dafür wird keine Proxy-Steuer fällig. Zaris hasht Keys mit derselben CRC16(key) % 16384-Slot-Berechnung wie Redis Cluster, sodass ein Cluster-fähiger Redis-Client jeden Key direkt zum Node leitet, der ihn besitzt:
redis client ──CRC16(key) % 16384 → slot → owner──▶ node that holds the key ──▶ reply
Das ist ein Hop, genau wie bei der Kommunikation mit Redis Cluster — der Client hasht den Key und verbindet sich direkt mit dessen Besitzer, und wenn der Befehl dort ankommt, wird er aus dem In-Process-Store dieses Nodes bedient. Gegenüber einer einzelnen Instanz gewinnen Sie Lesekapazität, die mit dem Cluster wächst, statt an einem Core zu enden, ohne dafür das direkte Routing zum Besitzer aufzugeben.
Auch der Schreibpfad wurde schneller
2.0.0 drehte sich nicht nur um Lesezugriffe. Auf dem SET-Pfad haben wir ein Read-before-Write entfernt, das jeder Schreibvorgang bezahlen musste, und einen verirrten Count()-Aufruf auf dem Hot Path behoben, der den SET-Durchsatz unbemerkt gedeckelt hatte. Beim nativen .NET-Client geben die Hot-Path-Operationen — Get, Put, Delete, Expire, Persist, Ttl und ihre verketteten Formen — nun ValueTask zurück, sodass ein synchron abgeschlossener Aufruf nichts alloziert:
// Alloc-free when the value is already local — no Task on the fast path.
ValueTask<ReadResult<Order>> pending = client.GetAsync<Order>("order:42");
Und für Workloads mit großen, häufig aktualisierten Werten speichert ein optionaler Large-Object-Chunking-Modus große, heiße Werte als byte[][]-Stücke unterhalb der LOH-Schwelle. Das hält sie vom Large Object Heap fern und aus dem Weg des Garbage Collectors.
Ehrlich messen
Wenn Sie dies selbst benchmarken, werden Sie über zwei Dinge stolpern:
- Lese-Benchmarks sind meist Client-gebunden. Ein einzelner Lastgenerator ist lange vor dem Server ausgelastet, und dann messen Sie Ihren Client, nicht Zaris. Um den Server tatsächlich skalieren zu sehen, brauchen Sie genug gleichzeitige Last — mehrere Generatoren, genug Verbindungen —, um jeden Core zu beschäftigen. Bleibt der Durchsatz flach, während Sie Server-Cores hinzufügen, prüfen Sie zuerst, ob Ihr Client der Engpass ist.
- Messen Sie die Server-CPU korrekt. Lesen Sie sie aus der cgroup, nicht aus einer Schätzung eines Cluster-Metrics-Servers. Die Schätzung hinkt hinterher und glättet, und sie wird Ihnen darüber falsche Angaben machen, ob der Server tatsächlich ausgelastet ist.
Probieren Sie es aus
dotnet add package Clustron.Zaris.SDK --version 2.0.0
Wenn Sie Zaris für einen .NET-Stack gegen Redis abwägen, legt der Vergleich Clustron vs. Redis die Kompromisse dar, und Zaris 2.0.0 behandelt alles andere, was in diesem Release ausgeliefert wurde — native Datenstrukturen, Streams und das Redis-Protokoll-Frontend, das diese Zahlen überhaupt erst messbar gemacht hat. Wenn Sie von einem bestehenden Redis-Deployment kommen, führt Von Redis kommend durch die Migration, und der Deep Dive zum Redis-Protokoll erklärt das Proxy-Design im Detail.
Downloads und vollständige Release Notes finden Sie auf GitHub, oder installieren Sie Zaris auf der Plattform Ihrer Wahl.