Zum Hauptinhalt springen

19 Posts getaggt mit ".NET"

Entwicklung verteilter Systeme und Infrastruktur in .NET.

Alle Tags anzeigen

Ein Connection String, jede Plattform: Verbindung zu Zaris

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

Ein Connection String für .NET-Konfiguration, PowerShell und die Web-Konsole

Die Hälfte der „funktioniert in Dev, bricht in Prod"-Verbindungsfehler hat dieselbe Ursache: Die Verbindungsdetails wurden an drei Stellen auf drei verschiedene Arten geschrieben. In der appsettings.json stand eine Host-Liste, im Betriebshandbuch wurde eine andere in eine PowerShell-Sitzung kopiert, und der Connection String, den jemand aus einem Wiki kopiert hat, enthielt ein veraltetes Token.

Zaris nimmt diese ganze Problemklasse mit einem einzigen, plattformunabhängigen Connection-String-Format vom Tisch. Exakt derselbe String funktioniert, wenn Ihre .NET-Anwendung ihn aus der Konfiguration liest, wenn ein Operator ihn einem PowerShell-Cmdlet übergibt und wenn jemand ihn direkt aus der Web-Konsole kopiert. Eine kanonische Schreibweise, überall.

Dieser Beitrag zerlegt den String Teil für Teil, zeigt alle drei Verwendungsarten und plädiert für die wichtigste Regel in der Produktion: Legen Sie niemals ein rohes Token in den String.

Datenstrukturen, die replizieren: Hashes, Listen, Sets und Sorted Sets in Zaris

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

Native Hashes, Listen, Sets und Sorted Sets, repliziert wie jeder andere Key in Zaris

Zaris 1.x speicherte Werte. Zaris 2.0.0 speichert Collections. Hashes, Listen, Sets und Sorted Sets sind nun erstklassige, serverseitige Typen mit echten, typisierten .NET-APIs — und, ebenso wichtig, sie verhalten sich wie alles andere im Store.

Genau diesen letzten Punkt machen viele falsch. In vielen Systemen sind „Datenstrukturen“ eine clientseitige Illusion: Eine Bibliothek serialisiert ein Dictionary in einen Blob, schreibt es per Put und nennt das einen Hash. Das funktioniert, bis zwei Clients gleichzeitig dieselbe Collection anfassen oder bis Sie fragen, was bei einem Failover passiert. In Zaris ist ein Hash ein Hash auf dem Server. Er liegt auf der Partition, die seinen Key besitzt, repliziert auf das Replikat dieser Partition und überlebt den Verlust eines Nodes genau so wie ein einfacher Key.

Zaris 2.0.0: Native Datenstrukturen, Streams und ein Redis-Protokoll-Frontend

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

Zaris 2.0.0 — Native Datenstrukturen, Streams und ein Redis-Protokoll-Frontend

Heute veröffentlichen wir Zaris 2.0.0 — das größte Release seit der allgemeinen Verfügbarkeit von Zaris und das erste mit einem Major-Versionssprung. Es macht aus Zaris, einem schnellen, replizierten Key/Value-Store, einen vollwertigen Datenstruktur-Server: native Hashes, Listen, Sets, Sorted Sets, Pub/Sub und Redis Streams, alle auf dieselbe Weise partitioniert und repliziert wie Ihre Keys bereits.

Und es öffnet die Tür für alle anderen. Jeder Node kann nun ein Redis-Protokoll-Frontend (RESP) bereitstellen, sodass eine Anwendung in Python, Ruby, Java, Go — jeder Sprache mit einem Redis-Client — auf Zaris zeigen und es als Drop-in nutzen kann, ohne natives SDK und ohne eine Zeile umzuschreiben.

Die Versionsnummer ist eine 2 und keine 1.x, und das hat einen Grund: 2.0.0 ändert das Wire-Protokoll. Lesen Sie den Upgrade-Hinweis unten, bevor Sie es ausrollen.

Jede Sprache, ein Store: Wie Zaris das Redis-Protokoll spricht

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

Jede Sprache, ein Store

Zaris ist ein verteilter Key/Value-Store, der nativ für .NET gebaut wurde. Seine erstklassigen Clients sprechen ein effizientes binäres Protokoll, und wenn Ihr Stack .NET ist, ist das der Pfad, den Sie wollen — echte Async-APIs, .NET-Typen, Koordinationsprimitive, Drop-ins für IDistributedCache und HybridCache.

Die meisten Systeme bestehen aber nicht nur aus .NET. Da gibt es einen Python-Dienst für Datenarbeit, einen Go-Sidecar, einen Ruby-Job-Runner, ein Node-Gateway. Sie alle auf einen neuen Client umzuschreiben, nur um einen neuen Store auszuprobieren, kommt nicht in Frage. Deshalb kann in 2.0.0 jeder Zaris-Node zusätzlich einen Redis-Protokoll-Listener (RESP2) bereitstellen: Richten Sie einen bestehenden Redis-Client darauf und nutzen Sie Zaris als Drop-in-Key/Value-Store, in jeder Sprache, ohne eine Zeile Client-Code zu ändern.

Dieser Beitrag beschreibt, wie dieses Frontend tatsächlich funktioniert — das Routing-Modell, das Datenmodell, die Failover-Modelle und genau, welche Befehle dabei und welche nicht dabei sind.

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.

Von Redis kommend: Ihre .NET-App auf Zaris umstellen

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

Zwei Wege von Redis zu Zaris

Wenn Ihre .NET-Dienste bereits mit Redis kommunizieren, muss der Wechsel zu Zaris kein Rewrite sein. Zaris ist ein verteilter, replizierter In-Memory-Key/Value-Store, der nativ für .NET gebaut wurde — und seit 2.0.0 spricht er zwei Protokolle gleichzeitig: sein eigenes effizientes binäres Protokoll für erstklassige .NET-Clients und ein Redis-kompatibles RESP2-Frontend, auf das jeder Redis-Client in jeder Sprache zeigen kann.

Daraus ergeben sich zwei ehrliche Migrationspfade, die sich nicht gegenseitig ausschließen. Sie können mit dem RESP-Pfad ohne Änderungen beginnen, um Zaris heute unter Last zu bringen, und den nativen Client dort einführen, wo es sich lohnt. Dieser Beitrag geht beide Pfade durch und gibt Ihnen eine klare Regel für die Wahl an die Hand.

Zaris 0.9.0 ist da — und allgemein verfügbar

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

Zaris 0.9.0 — Allgemein verfügbar

Heute veröffentlichen wir Zaris 0.9.0 — und damit erreicht Zaris die allgemeine Verfügbarkeit. Es ist produktionsreif, plattformübergreifend und wird nun durch kommerziellen Support begleitet.

Falls Sie es noch nicht kennen: Zaris ist ein verteilter In-Memory-Key/Value-Store für .NET. Er ist schnell, repliziert und lässt sich über einen Browser, PowerShell oder direkt aus Ihrer Anwendung betreiben. Dieses Release ist der Höhepunkt dieser Arbeit — der Punkt, an dem aus einem „vielversprechenden Projekt“ etwas wird, das Sie in Produktion einsetzen können.

Verteilte Locks erklärt

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

Ein praktischer Leitfaden für .NET-Entwickler

Einleitung​

Wenn Anwendungen über mehrere Rechner skalieren, wird die Koordination zu einem der schwierigsten Aspekte des Systementwurfs.

In einer Anwendung auf einem einzelnen Rechner ist gegenseitiger Ausschluss unkompliziert. Entwickler können sich auf Sprachprimitive wie lock in C#, Mutex oder Semaphore verlassen, um sicherzustellen, dass jeweils nur ein Thread auf eine gemeinsame Ressource zugreift.

Diese Primitive funktionieren jedoch nur innerhalb eines einzelnen Prozesses oder Rechners.

Sobald Anwendungen über mehrere Dienste oder Nodes laufen, greifen klassische Locking-Mechanismen nicht mehr. Zwei Dienste auf verschiedenen Rechnern können sich nicht auf eine lokale lock-Anweisung stützen, um den Zugriff auf gemeinsamen Zustand zu koordinieren.

Hier werden verteilte Locks unverzichtbar.

Verteilte Locks erlauben es Diensten in einem Cluster, den Zugriff auf gemeinsame Ressourcen zu koordinieren und dabei sicherzustellen, dass zu einem gegebenen Zeitpunkt nur ein Teilnehmer den Lock hält.


Lokale Locks vs. verteilte Locks​

Ein typischer lokaler Lock in C# sieht so aus:

private static readonly object _lock = new object();

public void ProcessOrder()
{
lock(_lock)
{
Console.WriteLine("Processing order safely.");
}
}

Das funktioniert einwandfrei innerhalb eines einzelnen Prozesses.

Läuft Ihre Anwendung jedoch auf mehreren Dienstinstanzen, besitzt jede Instanz ihr eigenes _lock-Objekt. Der Lock koordiniert die Arbeit nicht mehr über das gesamte System.

Worker A → lock acquired Worker B → lock acquired Worker C → lock acquired

Alle Worker fahren gleichzeitig fort, womit der Zweck des Lockings verfehlt ist.

Verteilte Systeme benötigen einen gemeinsamen Koordinationsmechanismus.


Verteiltes Locking


Die Idee hinter verteilten Locks​

Ein verteilter Lock nutzt ein gemeinsames System, um den Besitz des Locks abzubilden.

Statt Speicher zu sperren, hält ein Node den Lock-Besitz in einem verteilten Koordinationssystem fest, zum Beispiel in:

  • einem verteilten Key/Value-Store
  • einem Koordinationsdienst
  • einem verteilten Cache

Beispielkonzept:

Lock Key: order:123 Owner: worker‑1

Versucht ein anderer Worker, denselben Lock zu erwerben, schlägt die Operation fehl, bis der Lock freigegeben wird oder abläuft.


Praxisszenario: Verteilte Rechnungsverarbeitung​

Stellen Sie sich einen Cluster von Workern vor, die Rechnungen verarbeiten.

Worker A Worker B Worker C

Alle Worker suchen nach offenen Rechnungen.

Ohne Locking:

Worker A → processes invoice #101 Worker B → processes invoice #101 Worker C → processes invoice #101

Dieselbe Rechnung könnte mehrfach verarbeitet werden.

Ein verteilter Lock stellt sicher, dass nur ein Worker die Rechnung verarbeitet.


Verteilte Locks mit Clustron DKV​

Clustron DKV bietet integrierte Primitive für die verteilte Koordination.

Locks werden über die Locks API verwaltet, und Operationen auf gesperrten Keys müssen das korrekte Lock-Handle enthalten.


Einen Lock erwerben​

Ein Client erwirbt zunächst einen Lock für einen bestimmten Key.

var locks = ((IDkv)client).Locks;

var key = "order:123";

var lockHandle = await locks.AcquireAsync(key, TimeSpan.FromSeconds(10));

if (lockHandle == null)
{
Console.WriteLine("Failed to acquire lock.");
return;
}

Console.WriteLine("Lock acquired.");

Das zurückgegebene Lock-Handle repräsentiert den Besitz des Locks.


Operationen unter dem Lock ausführen​

Sobald ein Lock erworben wurde, müssen Operationen das Lock-Handle enthalten.

var result = await client.PutAsync(
key,
"processed",
Put.WithLock(lockHandle.Handle));

if (result.IsSuccess)
{
Console.WriteLine("Value updated safely.");
}

Nur der Lock-Besitzer kann den Wert ändern.


Lesen mit dem Lock​

Auch Lesezugriffe können den Lock-Besitz berücksichtigen.

var value = await client.GetAsync<string>(
key,
Get.WithLock(lockHandle.Handle));

if (value.IsSuccess)
{
Console.WriteLine($"Value: {value.Value}");
}

Konkurrierende Operationen ohne den Lock​

Versucht ein anderer Worker, den Key ohne den Lock zu ändern, schlägt die Operation fehl.

var attempt = await client.PutAsync(key, "new-value");

if (!attempt.IsSuccess)
{
Console.WriteLine($"Operation rejected: {attempt.Status}");
}

Ergebnis:

KvStatus.Locked

Dies verhindert die gleichzeitige Änderung derselben Ressource.


Den Lock freigeben​

Ist die Arbeit abgeschlossen, kann der Lock freigegeben werden.

await lockHandle.ReleaseAsync();

Console.WriteLine("Lock released.");

Nach der Freigabe kann ein anderer Worker den Lock erwerben.


Beispielhafter Cluster-Ablauf​

Worker A → Acquire Lock → Success Worker B → Acquire Lock → Failed Worker C → Acquire Lock → Failed

Worker A → Update Key Worker A → Release Lock

Nur ein Worker führt die Operation aus, was die Korrektheit sicherstellt.


Umgang mit Ausfällen​

Locks in Clustron sind zeitlich begrenzt.

Beim Erwerb eines Locks muss eine Dauer angegeben werden:

await locks.AcquireAsync(key, TimeSpan.FromSeconds(10));

Stürzt der Client, der den Lock hält, ab oder bricht die Verbindung ab, läuft der Lock schließlich ab, sodass ein anderer Worker ihn erwerben kann.

Dadurch bleiben Locks nicht dauerhaft gehalten.


Best Practices​

Bei der Implementierung verteilter Locks:

  • Halten Sie kritische Abschnitte kurz
  • Vermeiden Sie nach Möglichkeit globale Locks
  • Partitionieren Sie Locks nach Ressource
  • Planen Sie stets für Fehlerszenarien

Schlussgedanken​

Verteilte Locks gehören zu den grundlegendsten Koordinationsprimitiven in verteilten Systemen.

Sie erlauben es Diensten auf mehreren Rechnern, den Zugriff auf gemeinsame Ressourcen sicher zu koordinieren und dabei die Korrektheit des Systems zu wahren.

Plattformen wie Clustron DKV vereinfachen dies durch integrierte Locking-Primitive, atomare Operationen und starke Konsistenzgarantien.

Im nächsten Artikel betrachten wir ein weiteres essenzielles Primitiv verteilter Systeme:

Leader-Election.

Verteilte Systeme entwickeln

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

Ein praktischer Leitfaden für .NET-Entwickler

Einleitung​

Die meisten Backend-Systeme beginnen ihr Leben auf einem einzelnen Rechner.

Eine ASP.NET-Core-Anwendung verarbeitet Requests, eine Datenbank speichert die Anwendungsdaten, und Hintergrundprozesse übernehmen asynchrone Aufgaben wie den Versand von E-Mails oder die Erstellung von Berichten. In dieser Umgebung ist Nebenläufigkeit beherrschbar, und die Koordination zwischen den Komponenten ist vergleichsweise einfach.

Wächst das System jedoch, ändert sich das.

Der Traffic nimmt zu, mehr Anwendungsinstanzen werden hinter einem Load Balancer betrieben, und Dienste laufen auf mehreren Rechnern. Aus der einstigen Einzelanwendung wird ein verteiltes System.

An diesem Punkt ändert sich die Natur des Problems grundlegend.

Die Herausforderungen beschränken sich nicht mehr darauf, korrekte Geschäftslogik zu schreiben. Entwickler müssen sich nun mit der Koordination über Rechnergrenzen hinweg, mit Netzwerkausfällen, Teilausfällen und der Wahrung der Konsistenz über verteilten Zustand befassen.

Hier beginnt die Ingenieurdisziplin der verteilten Systeme.


Architektur verteilter Systeme


Was ist ein verteiltes System?​

Ein verteiltes System ist eine Menge unabhängiger Computer, die dem Benutzer als ein einziges, in sich geschlossenes System erscheinen.

Statt die gesamte Logik auf einem Rechner auszuführen, werden Verantwortlichkeiten auf mehrere Nodes verteilt, die über das Netzwerk kommunizieren.

Beispiele sind:

  • Microservice-Architekturen
  • Verteilte Datenbanken
  • Geclusterte Caches
  • Message Queues
  • Cloud-Infrastrukturplattformen

Diese Systeme bieten Skalierbarkeit und Ausfallsicherheit, bringen aber neue Komplexität mit sich.


Warum verteilte Systeme schwierig sind​

Anders als eine Anwendung auf einem einzelnen Rechner müssen verteilte Systeme mit Gegebenheiten wie den folgenden umgehen:

Netzwerkausfälle​

In verteilten Systemen findet die Kommunikation über ein Netzwerk statt. Netzwerke können langsam, unzuverlässig oder zeitweise nicht verfügbar sein.

Ein Request kann fehlschlagen, weil der entfernte Node abgestürzt ist, weil das Netzwerk Pakete verworfen hat oder weil ein Timeout aufgetreten ist.

Das System muss solche Szenarien kontrolliert abfangen.


Teilausfälle​

In einem verteilten System können Komponenten unabhängig voneinander ausfallen.

Ein Node in einem Cluster kann abstürzen, während die anderen gesund bleiben. Eine Dienstinstanz kann neu starten, während andere Dienste weiterlaufen.

Das bedeutet, dass das System auch dann korrekt arbeiten muss, wenn nur ein Teil davon funktioniert.


Herausforderungen bei der Konsistenz​

Werden Daten über mehrere Nodes hinweg repliziert, wird es schwierig, sie konsistent zu halten.

Zum Beispiel:

  • Zwei Nodes können versuchen, dieselben Daten gleichzeitig zu aktualisieren.
  • Netzwerkverzögerungen können dazu führen, dass sich Nodes vorübergehend nicht über den aktuellen Zustand einig sind.
  • Ein Node kann abstürzen, nachdem er nur einen Teil einer Operation ausgeführt hat.

Systeme, die trotz dieser Szenarien einen konsistenten Zustand wahren, erfordern eine sorgfältige Koordination.


Zentrale Bausteine verteilter Systeme​

Die meisten verteilten Systeme stützen sich auf einige wenige Kernprimitive.

Wer diese Primitive versteht, kann zuverlässige Architekturen besser entwerfen.


Verteilter Speicher​

Daten müssen häufig auf mehrere Rechner verteilt gespeichert werden.

Das ermöglicht:

  • horizontale Skalierung
  • Fehlertoleranz
  • Hochverfügbarkeit

Hierfür werden üblicherweise verteilte Key/Value-Stores eingesetzt.


Koordination​

Nodes müssen Aktionen wie die folgenden koordinieren:

  • Leader-Election
  • verteiltes Locking
  • Cluster-Mitgliedschaft
  • Konfigurationsänderungen

Ohne Koordinationsmechanismen könnten mehrere Nodes widersprüchliche Operationen ausführen.


Konsens​

Manche Systeme benötigen starke Garantien für den gemeinsamen Zustand.

Konsensalgorithmen wie Raft oder Paxos ermöglichen es Nodes, sich auch bei Ausfällen auf Entscheidungen zu einigen.

Diese Algorithmen sind grundlegend für verteilte Datenbanken und Koordinationsdienste.


Partitionierung und Replikation​

Daten werden typischerweise:

  • über Nodes hinweg partitioniert, um die Last zu verteilen
  • repliziert, um sich gegen Ausfälle abzusichern

Performance, Verfügbarkeit und Konsistenz auszubalancieren, ist eine der zentralen Entwurfsherausforderungen.


Gängige Muster verteilter Systeme​

Mehrere Muster tauchen in realen Systemen immer wieder auf.

Beispiele:

  • Leader-Election zur Koordination von Cluster-Aktivitäten
  • Verteilte Locks zur Sicherstellung des gegenseitigen Ausschlusses
  • Job-Queues für die asynchrone Verarbeitung
  • Rate Limiter zur Steuerung des Request-Volumens
  • Transaktionsprotokolle für atomare Aktualisierungen über mehrere Nodes

Diese Muster bilden das Fundament vieler großer Plattformen.


Die Rolle von Koordinationssystemen​

Um die Entwicklung verteilter Systeme zu vereinfachen, setzen viele Organisationen auf Koordinationssysteme wie:

  • etcd
  • ZooKeeper
  • Consul

Diese Systeme stellen Primitive bereit wie:

  • Leader-Election
  • verteilte Locks
  • Cluster-Mitgliedschaft
  • Konfigurationsspeicher

Anwendungen können darauf aufbauend Verhalten auf höherer Ebene umsetzen.


Verteilte Systeme im .NET-Ökosystem​

Während verteilte Infrastrukturwerkzeuge oft mit anderen Ökosystemen in Verbindung gebracht werden, laufen .NET-Anwendungen zunehmend in verteilten Umgebungen.

Moderne .NET-Systeme nutzen häufig:

  • Kubernetes
  • verteilte Caches
  • Microservices
  • ereignisgesteuerte Architekturen

Dadurch gewinnen Koordination und die Verwaltung von verteiltem Zustand auch für .NET-Entwickler zunehmend an Bedeutung.


Schlussgedanken​

Verteilte Systeme zu bauen, unterscheidet sich grundlegend vom Bau klassischer Anwendungen auf einem einzelnen Rechner.

Netzwerkkommunikation, Teilausfälle und Datenkonsistenz bringen Komplexität mit sich, die spezialisierte Muster und Infrastruktur erfordert.

Wer die zentralen Bausteine versteht — Speicher, Koordination, Konsens und Replikation —, kann glücklicherweise Systeme entwerfen, die zuverlässig skalieren und auch bei Ausfällen widerstandsfähig bleiben.

In kommenden Artikeln beleuchten wir einige dieser Primitive genauer, darunter Leader-Election, Koordinationsmuster für verteilte Systeme und praktische Implementierungen in .NET.