Muster zur Aufrechterhaltung der Konsistenz in einem verteilten System mit Ereignisquellen?

12

Ich habe in letzter Zeit über Event-Sourcing gelesen und mag die Ideen, die dahinter stecken, aber ich habe folgendes Problem.

Angenommen, Sie haben N gleichzeitige Prozesse, die Befehle empfangen (z. B. Webserver), als Ergebnis Ereignisse generieren und diese in einem zentralen Speicher speichern. Nehmen wir außerdem an, dass alle vorübergehenden Anwendungszustände im Speicher der einzelnen Prozesse beibehalten werden, indem Ereignisse aus dem Speicher nacheinander angewendet werden.

Angenommen, wir haben die folgende Geschäftsregel: Jeder einzelne Benutzer muss einen eindeutigen Benutzernamen haben.

Wenn zwei Prozesse einen Benutzerregistrierungsbefehl für denselben Benutzernamen X erhalten, überprüfen sie beide, dass X nicht in ihrer Liste der Benutzernamen enthalten ist. Die Regel wird für beide Prozesse validiert und sie speichern beide ein Ereignis "Neuer Benutzer mit Benutzername X" im Speicher .

Wir sind jetzt in einen inkonsistenten globalen Status eingetreten, da die Geschäftsregel verletzt wurde (es gibt zwei unterschiedliche Benutzer mit demselben Benutzernamen).

In einem herkömmlichen RDBMS-System mit N Servern <-> 1 wird die Datenbank als zentraler Synchronisationspunkt verwendet, um solche Inkonsistenzen zu vermeiden.

Meine Frage lautet: Wie gehen Event-Sourcing-Systeme normalerweise mit diesem Problem um? Verarbeiten sie einfach alle Befehle der Reihe nach (z. B. begrenzen sie die Anzahl der Prozesse, die in den Speicher geschrieben werden können, auf 1)?

Olivier Lalonde
quelle
1
Wird eine solche Einschränkung durch Code gesteuert oder handelt es sich um eine DB-Einschränkung? N Ereignisse können nacheinander gesendet oder nicht gesendet und verarbeitet werden. N Ereignisse können gleichzeitig überprüft werden, ohne sich gegenseitig zu verwerfen. Wenn die Bestellung wichtig ist, müssen Sie die Validierung synchronisieren. Oder Gebrauch Warteschlange einzureihen Ereignisse tun dispatch'em in sequentialy
Laiv
@Laiv richtig. Der Einfachheit halber nahm ich an, dass es keine Datenbank gibt, alle Zustände bleiben im Speicher. Das sequentielle Verarbeiten bestimmter Befehlstypen durch eine Warteschlange ist eine Option, aber es scheint kompliziert zu sein, zu entscheiden, welche Befehle andere beeinträchtigen können, und ich würde wahrscheinlich alle Befehle in dieselbe Warteschlange stellen, was einem einzelnen Verarbeitungsvorgang gleichkommt : / Wenn ich zum Beispiel einen Benutzer habe, der einen Kommentar zu einem Blogeintrag hinzufügt, sollten "Benutzer löschen", "Benutzer sperren", "Blogeintrag löschen", "Blogeintragskommentare deaktivieren" usw. in derselben Warteschlange stehen.
Olivier Lalonde
1
Ich bin mit Ihnen einverstanden, mit Warteschlangen oder Semaphoren zu arbeiten, ist nicht einfach. Weder mit Parallelitäts- noch mit Ereignisquellenmustern arbeiten. Grundsätzlich führen alle Lösungen jedoch dazu, dass der Datenverkehr eines Ereignisses vom System koordiniert wird. Es ist jedoch ein interessantes Paradigma. Es gibt auch externe Caches, die sich an Tupeln wie Redis orientieren und dabei helfen können, den Datenverkehr zwischen Knoten zu verwalten, z. B. das Zwischenspeichern des letzten Status einer Entität oder wenn eine solche Entität gerade verarbeitet wird. Shared Caches sind bei solchen Entwicklungen weit verbreitet. Es mag komplex erscheinen, aber nicht aufgeben ;-) Es ist ziemlich interessant
Laiv

Antworten:

6

In einem herkömmlichen RDBMS-System mit N Servern <-> 1 wird die Datenbank als zentraler Synchronisationspunkt verwendet, um solche Inkonsistenzen zu vermeiden.

In Ereignissystemen hat der "Ereignisspeicher" dieselbe Funktion. Bei einem Ereignisquellenobjekt besteht Ihr Schreibvorgang aus dem Anhängen Ihrer neuen Ereignisse an eine bestimmte Version des Ereignisdatenstroms. Genau wie bei der gleichzeitigen Programmierung können Sie bei der Verarbeitung des Befehls eine Sperre für diesen Verlauf festlegen. Bei Ereignissystemen ist es üblicher, optimistischer vorzugehen: Laden Sie den vorherigen Verlauf, berechnen Sie den neuen Verlauf und tauschen Sie ihn aus. Wenn ein anderer Befehl ebenfalls in diesen Stream geschrieben hat, schlägt das Vergleichen und Austauschen fehl. Von dort aus führen Sie entweder Ihren Befehl erneut aus oder geben Ihren Befehl auf oder führen Ihre Ergebnisse sogar in den Verlauf ein.

Konflikte werden zu einem Hauptproblem, wenn alle N Server mit ihren M Befehlen versuchen, in einen einzigen Stream zu schreiben. Die übliche Antwort besteht darin, jedem Ereignis in Ihrem Modell einen Verlauf zuzuweisen. Benutzer (Bob) hat also einen anderen Verlauf als Benutzer (Alice) und schreibt in den einen, blockiert aber nicht die Schreibvorgänge in den anderen.

Meine Frage lautet: Wie gehen Event-Sourcing-Systeme normalerweise mit diesem Problem um? Verarbeiten sie einfach alle Befehle nacheinander?

Greg Young bei der Set-Validierung

Gibt es eine elegante Möglichkeit, eindeutige Einschränkungen für Domänenobjektattribute zu überprüfen, ohne die Geschäftslogik in die Serviceschicht zu verschieben?

Eine kurze Antwort, die in vielen Fällen eingehender untersucht wird, zeigt, dass entweder (a) es sich um einen schlecht verstandenen Proxy für eine andere Anforderung handelt oder (b) Verstöße gegen die "Regel" zulässig sind, wenn sie festgestellt werden können (Ausnahmebericht). , innerhalb eines bestimmten Zeitfensters abgeschwächt oder mit geringer Häufigkeit (z. B .: Clients können prüfen, ob ein Name verfügbar ist, bevor sie einen Befehl zur Verwendung senden).

In einigen Fällen, in denen Ihr Ereignisspeicher eine gute Mengenvalidierung aufweist (z. B. eine relationale Datenbank), implementieren Sie die Anforderung, indem Sie in dieselbe Transaktion, in der die Ereignisse bestehen, in eine Tabelle mit eindeutigen Namen schreiben.

In einigen Fällen können Sie die Anforderung nur erzwingen, indem Sie alle Benutzernamen im selben Stream veröffentlichen (wodurch Sie den Satz von Namen im Speicher als Teil Ihres Domänenmodells auswerten können). - In diesem Fall aktualisieren zwei Prozesse den Aktualisierungsversuch "des" Stream-Verlaufs, aber eine der Compare-and-Swap-Operationen schlägt fehl, und die Wiederholung dieses Befehls kann den Konflikt erkennen.

VoiceOfUnreason
quelle
1) Danke für die Anregungen und Hinweise. Wenn Sie "Compare-and-Swap" sagen, bedeutet dies, dass der Prozess zum Zeitpunkt des Speicherns eines Ereignisses feststellt, dass neue Ereignisse eingetroffen sind, seit er mit der Verarbeitung des Befehls begonnen hat? Ich denke, dies würde einen Ereignisspeicher erfordern, der "Compare-and-Swap" -Semantik unterstützt, richtig? (zB "schreibe nur dieses Ereignis und nur wenn das letzte Ereignis die ID X hat")?
Olivier Lalonde
2) Ich mag auch die Idee, temporäre Inkonsistenzen zu akzeptieren und sie schließlich zu reparieren, aber ich bin mir nicht sicher, wie ich das zuverlässig codieren würde ... Vielleicht habe ich einen dedizierten Prozess, der Ereignisse nacheinander validiert und Rollback-Ereignisse erstellt, wenn er sie erkennt etwas ist schief gelaufen? Vielen Dank!
Olivier Lalonde
(1) Ich würde eher "neue Version der Geschichte" als "neue Ereignisse" sagen, aber Sie haben die Idee; Ersetzen Sie die Historie nur, wenn es die ist, die wir erwarten.
VoiceOfUnreason
(2) Ja. Es ist ein bisschen Logik, die Ereignisse aus dem Speicher stapelweise liest und am Ende des Stapels einen Ausnahmebedingungsbericht sendet ("Wir haben zu viele Benutzer mit dem Namen Bob") oder Befehle ausgibt, um das Problem zu kompensieren (vorausgesetzt, die richtige Antwort lautet) ohne menschliches Zutun berechenbar).
VoiceOfUnreason
2

Klingt so, als ob Sie einen Geschäftsprozess ( sagaim Kontext von Domain Driven Design) für die Benutzerregistrierung implementieren könnten, in dem der Benutzer wie ein Benutzer behandelt wird CRDT.

Ressourcen

  1. https://doc.akka.io/docs/akka/current/distributed-data.html http://archive.is/t0QIx

  2. "CRDTs mit verteilten Akka-Daten" https://www.slideshare.net/markusjura/crdts-with-akka-distributed-data , um mehr zu erfahren

    • CmRDTs - Operationsbasierte CRDTs
    • CvRDTs - state basierte CRTDs
  3. Codebeispiele in Scala https://github.com/akka/akka-samples/tree/master/akka-sample-distributed-data-scala . Vielleicht ist "Einkaufswagen" am besten geeignet.

  4. Tour des Akka Clusters - Akka Distributed Data https://manuel.bernhardt.io/2018/01/03/tour-akka-cluster-akka-distributed-data/
SemanticBeeng
quelle