Derselbe Code, mit oder ohne Netzwerk: Der Embedded-Modus von Zaris
Die meisten verteilten Datenspeicher zwingen Sie, sich früh und endgültig zwischen zwei Welten zu entscheiden. Da ist der „echte" Store — ein Cluster, den Sie deployen, auf den Sie einen Client über das Netzwerk richten und mit dem Sie über ein Wire-Protokoll sprechen. Und da ist der „falsche" Store, zu dem Sie in Unit-Tests greifen: ein In-Memory-Dictionary, ein Mock, ein handgeschriebener Stub, der gerade genug der Schnittstelle implementiert, damit der Test kompiliert. Beide haben nichts gemeinsam. Ihr Test prüft das Verhalten des Stubs, nicht das des Stores, und die Lücke dazwischen ist genau der Ort, an dem sich Bugs verstecken.
Zaris hebt diese Entscheidung auf. Derselbe typisierte .NET-Client, der über TLS mit einem verteilten Cluster spricht, betreibt den Store auch eingebettet in Ihrem Prozess, ohne Socket, ohne Partition-Map und ohne Deployment — und zwar mit derselben Store-Engine, nicht mit einem Ersatz. Welchen von beiden Sie bekommen, wählen Sie mit einer einzigen Connection-String. Dieser Beitrag erklärt, wie das funktioniert, wofür es sich wirklich eignet und wo die harte Grenze verläuft, an der der Embedded-Modus endet und ein Cluster beginnt.
Eine Zeichenfolge ist der ganze Schalter
Ein Zaris-Client wird immer aus einer Connection-String konfiguriert. Die Zeichenfolge enthält den Transport, die Seed-Endpunkte, den Store-Namen und Optionen. Es gibt zwei Formen, und der Unterschied zwischen ihnen ist der Unterschied zwischen einem Cluster und einem eingebetteten Store:
zaris://inproc/orders # embedded — store runs inside this process
zariss://zaris.mycorp.com:7863/orders # remote — connect to a cluster over TLS
Das Host-Token inproc ist reserviert. Sieht der Client es, gibt es keine Endpunkte zu wählen, keinen Transport auszuhandeln, keinen TLS-Handshake und kein Token — der Store wird aus dem Speicher im aktuellen Prozess bedient. Alles andere an Ihrem Code bleibt identisch:
// Embedded: no server to start, no port, no network.
IZarisClient store = await ZarisClient.ConnectAsync("zaris://inproc/orders");
// Later, in production — the ONLY line that changed:
IZarisClient store = await ZarisClient.ConnectAsync("zariss://zaris.mycorp.com:7863/orders?ca=/etc/zaris/ca.crt");
In ASP.NET Core ist es dieselbe Geschichte über Dependency Injection — die Connection-String stammt üblicherweise aus der Konfiguration, der Wechsel ist also ein Konfigurationswert, keine Codeänderung:
// Program.cs — one registration, driven entirely by the connection string.
builder.Services.AddClustronZaris("orders", builder.Configuration.GetConnectionString("orders")!);
Tragen Sie zaris://inproc/orders in appsettings.Development.json und zariss://.../orders in die Produktion ein, und dieselbe Binärdatei läuft eingebettet auf dem Laptop eines Entwicklers und geclustert in der Cloud, ohne dass etwas neu kompiliert wird.
Warum „derselbe Client" der Punkt ist
Der Grund, warum das wichtig ist, steckt im zurückgegebenen Typ. ConnectAsync liefert unabhängig vom Modus ein IZarisClient — oder, für die volle Feature-Oberfläche, IZaris. Der Embedded-Modus wird von einer Klasse namens ZarisInProcClient bedient, die exakt dieselben Schnittstellen wie der Cluster-Client implementiert:
public interface IZaris : IZarisClient
{
ILeasesClient Leases { get; }
ILocksClient Locks { get; }
ICountersClient Counters { get; }
IScanClient Scan { get; }
IWatchClient Watch { get; }
IPubSubClient PubSub { get; }
IHashClient Hashes { get; }
IListClient Lists { get; }
ISetClient Sets { get; }
ISortedSetClient SortedSets { get; }
IStreamClient Streams { get; }
}
Jede dieser Oberflächen ist im Embedded-Modus vorhanden, getragen von derselben Store-Engine, die der Cluster nutzt. Key/Value-Get/Put/Delete, Bulk und Batch, Multi-Item-Transaktionen, TTL und Ablauf, optimistisches Compare-and-Swap, Watch für reaktive Lesevorgänge, native Streams mit Consumer Groups, die replizierten Datenstrukturen — all das wird gegen den eingebetteten Store aufgelöst, nicht gegen einen Mock.
Diese gemeinsame Engine macht den eingebetteten Store zu einem getreuen Testziel. Eine Compare-and-Swap-Schleife, die darauf angewiesen ist, dass ein Konflikt präzise gemeldet wird, ein Watch-Abonnement, das feuern muss, wenn sich ein Schlüssel ändert, eine Transaktion, die atomar committen muss oder gar nicht — jedes verhält sich eingebettet so wie geclustert, weil es buchstäblich derselbe Codepfad auswertet. Ein KvStatus.Conflict bedeutet eingebettet dasselbe wie in der Produktion. Ein handgeschriebener Dictionary-Stub kann das nicht versprechen.
Was der Embedded-Host tatsächlich ist
Es lohnt sich, genau zu sein, was zaris://inproc startet, denn die ehrlichen Grenzen ergeben sich direkt daraus. Der Embedded-Host ist eine feste Ein-Node-Topologie, die jedes Segment besitzt. Es gibt keine Partition-Map, weil es nur einen Teilnehmer gibt; es gibt keine Replikation, weil es keine zweite Kopie gibt; es gibt keinen Transport, weil es keinen zweiten Prozess gibt, den man erreichen müsste.
Was tatsächlich läuft, ist die echte Mechanik, die Sie in einem Test haben wollen. Der Store stellt sein Segment vorab bereit, bindet den Lease-Service, den Watch-Service und lokale Metriken ein und startet den TTL-Sweeper in seinem normalen Takt — ein Schlüssel, den Sie mit ExpireAsync auf 50 ms TTL setzen, verschwindet also tatsächlich, per Timer, genau wie in einem Cluster. Ablauf, Eviction-Buchhaltung, Versionierung und Watch-Benachrichtigungen sind alle aktiv. Es ist ein vollständiger Store, der zufällig die Cluster-Größe eins hat.
Innerhalb eines Prozesses werden Clients nach Store-Namen geteilt. Zwei Aufrufe ConnectAsync("zaris://inproc/orders") im selben Prozess lösen zum selben eingebetteten Store auf, sodass ein Wert, den der eine schreibt, der andere liest — praktisch, wenn Ihr Test einen Producer und einen Consumer startet und erwartet, dass sie sich gegenseitig sehen. Verschiedene Store-Namen ergeben verschiedene Stores; verschiedene Prozesse erhalten völlig getrennte eingebettete Stores (mehr zu dieser Grenze unten).
Wo sich der Embedded-Modus bewährt
Unit- und Integrationstests, die den echten Store ausüben. Das ist der Hauptanwendungsfall. Ihr Repository, Ihre Cache-Schicht, Ihr Saga-Koordinator — testen Sie sie gegen zaris://inproc/test-{Guid}, erhalten Sie pro Test einen frischen, isolierten Store und prüfen die echte Semantik: echte Versionierung für Ihre CAS-Schleifen, echten TTL-Ablauf auf einem echten Timer, echte Transaktions-Atomarität, echte Watch-Events. Kein Docker-Container, der in der CI gestartet werden muss, kein Port, der zu vergeben wäre, keine instabile Aufräumphase. Der Store wird beim ersten Connect erstellt und stirbt mit dem Prozess.
Single-Process-Anwendungen, die später erwachsen werden könnten. Eine Desktop-App, ein Hintergrund-Worker, ein CLI-Tool oder ein kleiner Service, der heute einen schnellen strukturierten Store braucht, morgen aber ein geclustertes Deployment werden könnte. Beginnen Sie eingebettet. An dem Tag, an dem Sie diese Daten über Instanzen hinweg teilen müssen, ändern Sie die Connection-String auf einen Cluster — und weil Sie die ganze Zeit gegen IZaris programmiert haben, bewegt sich sonst nichts.
Integrationen, die standardmäßig eingebettet ausgeliefert werden. Der Zaris-ASP.NET-Core-Session-Provider ist ein konkretes Beispiel: Zaris als Session-Store einzubinden ist ein Aufruf, und in seinem Sample läuft er vollständig eingebettet —
builder.Services.AddZarisSession(options =>
{
options.ConnectionString = "zaris://inproc/sessions";
});
— sodass ein Entwickler die App mit F5 starten und funktionierende Sessions ohne jede Infrastruktur haben kann, um dann für ein Multi-Instanz-Deployment auf zariss://.../sessions zu wechseln, ohne den Session-Code anzufassen.
Um den Embedded-Modus zu nutzen, referenzieren Sie das Paket Clustron.Zaris.InProc neben dem Client (es zielt auf .NET 8). Das Paket registriert sich selbst: Der Client entdeckt und lädt die In-Process-Factory bei der ersten Verwendung, die bloße Referenz genügt also. In einer Test-Assembly, in der Sie lieber explizit sein möchten, verdrahtet new InProcBootstrap().Register(); es deterministisch.
Wo er ehrlicherweise endet
Der Embedded-Modus ist ein echter Store, aber ein Single-Node-In-Memory-Store, und etwas anderes vorzugeben wäre genau der Marketing-Nebel, den dieser Blog vermeiden will. Drei harte Grenzen, direkt aus dem Design:
Keine Dauerhaftigkeit. Der eingebettete Store hält Daten im Prozessspeicher und nirgendwo sonst. In diesem Modus gibt es weder Replikation noch ein On-Disk-Log — beendet sich der Prozess, sind die Daten weg. Das ist genau das, was Sie für einen Test wollen (jeder Lauf mit sauberem Zustand), und genau das, worauf Sie sich bei allem, was einen Neustart überleben soll, nicht verlassen dürfen. Der Embedded-Modus ist keine Datenbank, auf die Sie Ihre Produktionsdaten stützen.
Keine Hochverfügbarkeit, kein Scale-out. Ein Cluster aus einem Node toleriert den Verlust von null Nodes und skaliert bis zu Speicher und CPU eines Prozesses. All die Gründe, aus denen Sie zum verteilten Zaris greifen würden — einen Node-Ausfall ohne Datenverlust überstehen, Replikate für Dauerhaftigkeit dimensionieren, den Durchsatz über Kerne und Maschinen skalieren — treffen auf einen Embedded-Host schlicht nicht zu, weil es nur einen davon gibt. Brauchen Sie diese Eigenschaften, brauchen Sie den Cluster.
Kein prozessübergreifendes Teilen. Die In-Process-Registry teilt einen Store nach Namen innerhalb eines Prozesses. Zwei verschiedene Prozesse, die beide zaris://inproc/orders öffnen, erhalten zwei unabhängige Stores, die nichts voneinander wissen — zwischen ihnen gibt es per Definition keinen Socket. In dem Moment, in dem Ihre Daten für einen zweiten Prozess oder eine zweite Maschine sichtbar sein müssen, ist der Embedded-Modus das falsche Werkzeug und ein Cluster das richtige.
Nichts davon sind Bugs, die zu beheben wären; es ist die Definition von „embedded". Der Wert liegt nicht darin, dass der In-Process-Store heimlich verteilt wäre — sondern darin, dass an dem Tag, an dem Sie Verteilung brauchen, das Einzige, was Sie ändern, die Zeichenfolge ist, die sagt, wo der Store lebt.
Dasselbe Versprechen, zweimal gehalten
Der Pitch ist klein und konkret. Sie schreiben Ihre Anwendung gegen eine Client-Oberfläche. In einem Test, auf einem Laptop oder in einem Single-Process-Tool wird diese Oberfläche von einem echten Zaris-Store bedient, der in Ihrem eigenen Speicher läuft — schnell erstellt, getreu in seiner Semantik und weg, wenn der Prozess endet. In der Produktion spricht derselbe Code über TLS mit einem verteilten Cluster. Das Verhalten, das Ihre Tests festgenagelt haben, ist das Verhalten, das Sie ausliefern, weil es nie eine andere Implementierung war — nur ein anderer Ort, an dem der Store lebt.
Das ist die ganze Idee hinter zaris://inproc: Die Lücke zwischen „dem Store in meinem Test" und „dem Store in der Produktion" ist eine Connection-String breit, und Sie entscheiden pro Umgebung, auf welcher Seite Sie stehen.