CRDT: Konfliktfreie replizierte Datentypen


Wie zĂ€hle ich google.com-Seitentreffer? Und wie speichert man den ZĂ€hler von Likes sehr beliebter Benutzer? In diesem Artikel wird vorgeschlagen, die Lösung dieser Probleme mithilfe von CRDT (Conflict-Free Replicated Data Types, was auf Russisch ungefĂ€hr als Conflict-Free Replicated Data Types ĂŒbersetzt wird) und im allgemeineren Fall von Replikatsynchronisierungsaufgaben in einem verteilten System mit mehreren fĂŒhrenden Knoten in Betracht zu ziehen.

1. Einleitung


Wir sind seit langem daran gewöhnt, Anwendungen wie einen Kalender oder einen Notendienst wie Evernote zu verwenden. Sie zeichnen sich dadurch aus, dass Sie offline (von mehreren GerĂ€ten bis zu mehreren Personen gleichzeitig (mit denselben Daten) arbeiten können. Die Herausforderung fĂŒr die Entwickler jeder solchen Anwendung besteht darin, die „reibungsloseste“ Synchronisierung von Daten sicherzustellen, die gleichzeitig auf mehreren GerĂ€ten geĂ€ndert werden. Im Idealfall sollte keine Benutzerbeteiligung erforderlich sein, um ZusammenfĂŒhrungskonflikte zu lösen.

In einem frĂŒheren Artikel haben wir bereits einen Ansatz zur Lösung solcher Probleme in Betracht gezogen - Operational Transformation. Außerdem wird eine sehr Ă€hnliche Methode beschrieben, die sowohl Vor- als auch Nachteile hat (zum Beispiel wurde CRDT fĂŒr JSON noch nicht erfunden. Aktualisierung: Dank an msvn fĂŒr den Link hier Hier ist ein Projekt der Autoren eines Forschungsartikels zur Implementierung von JSON in CRDT.

2. Starke eventuelle Konsistenz


In letzter Zeit wurde viel Arbeit geschrieben und viel Forschung auf dem Gebiet der eventuellen Konsistenz betrieben. Meiner Meinung nach gibt es jetzt einen starken Trend hin zu einer Verlagerung von starker Konsistenz zu verschiedenen Optionen fĂŒr Konsistenz, um zu untersuchen, welche Konsistenz in welchen Situationen / Systemen rentabler anzuwenden ist, und um bestehende Definitionen zu ĂŒberdenken. Dies fĂŒhrt zu Verwirrung, wenn beispielsweise die Autoren einiger Werke, die ĂŒber Konsistenz sprechen, eine eventuelle Konsistenz mit einigen zusĂ€tzlichen Eigenschaften bedeuten und andere Autoren hierfĂŒr eine bestimmte Terminologie verwenden.

Die von den Autoren eines der Artikel aufgeworfene Frage kritisiert die derzeitige Definition der eventuellen Konsistenz: Wenn Ihr System auf alle Anfragen immer mit „42“ antwortet, ist alles in Ordnung, und es ist schließlich konsistent.

Ohne die Richtigkeit dieses Artikels zu verletzen, verwende ich gemĂ€ĂŸ den Autoren der Originalartikel die folgende Terminologie (bitte beachten Sie, dass dies keine strengen Definitionen sind, sondern Unterschiede):

  • Starke Konsistenz (SC): Alle SchreibvorgĂ€nge sind streng geordnet. Eine Leseanforderung auf einem Replikat gibt das gleiche, zuletzt aufgezeichnete Ergebnis zurĂŒck. Echtzeitkonsens ist erforderlich, um Konflikte zu lösen (mit den daraus resultierenden Konsequenzen), kann einem RĂŒckgang auf n / 2 - 1 Knoten standhalten.
  • Eventuelle Konsistenz (EC): Aktualisieren Sie die Daten lokal und senden Sie das Update weiter. Das Lesen auf verschiedenen Replikaten kann veraltete Daten zurĂŒckgeben. Im Falle von Konflikten rollen wir entweder zurĂŒck oder entscheiden irgendwie, was zu tun ist. T.O. Konsens ist weiterhin erforderlich, jedoch nicht mehr in Echtzeit .
  • Starke eventuelle Konsistenz (SEC): EC + zur Lösung von Konflikten haben Replikate einen vordefinierten Algorithmus. T.O. Konsens ist nicht erforderlich , er kann einem Abfall auf n - 1 Knoten standhalten.

Beachten Sie, dass SEC (sozusagen) das Problem des CAP-Theorems löst: Alle drei Eigenschaften sind erfĂŒllt.

Wir sind also bereit, SC zu spenden, und möchten einen bestimmten Satz grundlegender Datentypen fĂŒr unser möglicherweise instabiles verteiltes System haben, die Schreibkonflikte fĂŒr uns automatisch lösen (keine Benutzerinteraktion oder Anforderung an einen Schiedsrichter erforderlich).

3. Aufgaben zu Likes und Hits


Zweifellos gibt es mehrere Algorithmen zur Lösung solcher Probleme. CRDT bietet einen ziemlich eleganten und einfachen Weg.

Google.com Trefferanzahl:


google.com verarbeitet ungefĂ€hr 150.000 Anfragen pro Sekunde aus der ganzen Welt. Offensichtlich muss der ZĂ€hler asynchron aktualisiert werden. Warteschlangen lösen das Problem teilweise. Wenn wir beispielsweise eine externe API bereitstellen, um diesen Wert abzurufen, mĂŒssen wir eine Replikation durchfĂŒhren, um das Repository nicht mit Leseanforderungen zu versehen. Und wenn es bereits eine Replikation gibt, vielleicht ohne globale Warteschlangen?

Bild


Benutzerlikes zÀhlen:


Die Aufgabe ist der vorherigen sehr Ă€hnlich, nur dass Sie jetzt eindeutige Treffer zĂ€hlen mĂŒssen.

4. Terminologie


FĂŒr ein umfassenderes VerstĂ€ndnis des Artikels mĂŒssen Sie die folgenden Begriffe kennen:

  1. Idempotenz
    Sagt, dass das mehrmalige Anwenden der Operation das Ergebnis nicht Àndert.
    Beispiele - GET-Operation oder Addition mit Null: f(x)=x+0
  2. KommutativitÀt
    f(x,y)=f(y,x)
  3. Teilbestellung
    ReflexivitÀt + TransitivitÀt + Antisymmetrie
  4. Halbgitter
    Teilweise bestelltes Set mit exakter Oberseite (Unterseite)
  5. Versionsvektor
    Ein Dimensionsvektor ist gleich der Anzahl der Knoten, und jeder Knoten erhöht bei Auftreten eines bestimmten Ereignisses seinen Wert im Vektor. WĂ€hrend der Synchronisation werden Daten mit diesem Vektor ĂŒbertragen, und dies fĂŒhrt eine Ordnungsbeziehung ein, mit der Sie bestimmen können, welches Replikat alte / neue Daten enthĂ€lt.

5. Modelle synchronisieren


Staatsbasiert:


Es wird auch als passive Synchronisation bezeichnet und bildet den konvergenten replizierten Datentyp - CvRDT.
Es wird in Dateisystemen wie NFS, AFS, Coda und in KV-Speichern Riak, Dynamo verwendet
In diesem Fall tauschen die Replikate den Status direkt aus, das empfangende Replikat fĂŒhrt den empfangenen Status mit seinem aktuellen Status zusammen.

Bild

Um die Konvergenz von Replikaten mithilfe dieser Synchronisierung durchzufĂŒhren, ist Folgendes erforderlich:

  • Die Daten bildeten ein Halbgitter
  • Die ZusammenfĂŒhrungsfunktion ergab eine genaue Obergrenze
  • Die Repliken bildeten einen zusammenhĂ€ngenden Graphen.

Ein Beispiel:

  • Datensatz: natĂŒrliche Zahlen  mathbbN
  • Mindestartikel: − infty
  • merge(x,y)=max(x,y)

Solche Anforderungen geben uns eine kommutative und idempotente ZusammenfĂŒhrungsfunktion, die auf einem bestimmten Datensatz monoton wĂ€chst :

Bild

Dies stellt sicher, dass die Replikate frĂŒher oder spĂ€ter konvergieren und Sie sich keine Gedanken ĂŒber das DatenĂŒbertragungsprotokoll machen mĂŒssen. Wir können Nachrichten mit einem neuen Status verlieren, sie mehrmals senden und sie sogar in beliebiger Reihenfolge senden .

Betriebsbasiert:


Es wird auch als aktive Synchronisation bezeichnet und bildet den kommutativen replizierten Datentyp - CmRDT.
Wird in kooperativen Systemen wie Bayou, Rover, IceCube, Telex verwendet.

In diesem Fall tauschen die Replikate StatusaktualisierungsvorgĂ€nge aus. Beim Aktualisieren von Daten wird das ursprĂŒngliche Replikat:

  • Ruft die generate () -Methode auf, die die effector () -Methode zurĂŒckgibt, die auf anderen Replikaten ausgefĂŒhrt werden soll. Mit anderen Worten, effector () ist der Abschluss zum Ändern des Status der verbleibenden Replikate.
  • Anwenden eines Effektors auf einen lokalen Staat
  • Sendet den Effektor an alle anderen Repliken

Bild

Um eine Konvergenz von Replikaten durchzufĂŒhren, mĂŒssen die folgenden Bedingungen erfĂŒllt sein:

  • ZuverlĂ€ssiges Lieferprotokoll
  • Wenn der Effektor gemĂ€ĂŸ der eingegebenen Reihenfolge (fĂŒr einen bestimmten Typ) an alle Replikate geliefert wird, sind simultane Effektoren kommutativ oder
  • Wenn der Effektor an alle Replikate geliefert wird, ohne die Bestellung zu berĂŒcksichtigen, sind alle Effektoren kommutativ.
  • Wenn der Effektor mehrmals geliefert werden kann, muss er idempotent sein
  • Einige Implementierungen verwenden Warteschlangen (Kafka) als Teil des Übermittlungsprotokolls.

Delta-basiert:


In Anbetracht des Status / op-basiert ist leicht zu erkennen, dass es keinen Sinn macht, den gesamten Status zu senden, wenn ein Update nur einen Teil des Status Ă€ndert. Wenn eine große Anzahl von Änderungen einen Status (z. B. einen ZĂ€hler) betrifft, können Sie eine aggregierte Änderung und nicht alle VorgĂ€nge senden Änderungen.

Die Delta-Synchronisation kombiniert beide AnsĂ€tze und sendet Delta-Mutatoren aus, die den Status gemĂ€ĂŸ dem letzten Synchronisationsdatum aktualisieren. Bei der anfĂ€nglichen Synchronisation muss der Status vollstĂ€ndig gesendet werden, und einige Implementierungen berĂŒcksichtigen in solchen FĂ€llen bereits den Status der verbleibenden Replikate, wenn Delta-Mutatoren erstellt werden.

Die nÀchste Optimierungsmethode besteht darin, das op-basierte Protokoll zu komprimieren, wenn Verzögerungen zulÀssig sind.

Bild

Rein betriebsbasiert:


Es gibt eine Verzögerung beim Erstellen eines Opektors bei der op-basierten Synchronisation. In einigen Systemen ist dies möglicherweise nicht akzeptabel. Dann mĂŒssen Sie die ursprĂŒngliche Änderung auf Kosten der Komplikation des Protokolls und der zusĂ€tzlichen Menge an Metadaten senden.

Bild

StandardanwendungsansÀtze:


  • Wenn Updates sofort im System gesendet werden sollen, ist zustandsbasiert eine schlechte Wahl, da das Versenden des gesamten Status teurer ist als nur ein Update-Vorgang. Delta-basiert funktioniert besser, aber in diesem speziellen Fall ist der Unterschied zu zustandsbasiert gering.
  • Wenn Sie das Replikat nach einem Fehler synchronisieren mĂŒssen, sind zustandsbasiert und deltabasiert die perfekte Wahl. Wenn Sie op-based verwenden mĂŒssen, stehen folgende Optionen zur VerfĂŒgung:

    1) Wirf alle verpassten Operationen ab dem Moment des Ausfalls
    2) Eine vollstÀndige Kopie einer der Replikate und ein Rollback verpasster VorgÀnge
  • Wie oben erwĂ€hnt, erfordert op-based, dass Updates genau einmal an jedes Replikat gesendet werden. Die Lieferanforderung kann nur einmal weggelassen werden, wenn der Effektor idempotent ist. In der Praxis ist es viel einfacher, die erste als die zweite zu implementieren.

Die Beziehung zwischen Op-basiert und State-basiert:


Zwei AnsÀtze können durcheinander emuliert werden, daher werden wir in Zukunft CRDT ohne Bezugnahme auf ein bestimmtes Synchronisationsmodell betrachten.

6. CRDT


6.1 ZĂ€hler


Eine Ganzzahl, die zwei Operationen unterstĂŒtzt: inc und dec. Betrachten Sie als Beispiel mögliche Implementierungen fĂŒr op- und zustandsbasierte Synchronisationen:

Op-basierter ZĂ€hler:


NatĂŒrlich nur Updates senden. Beispiel fĂŒr inc:

function generator() { return function (counter) { counter += 1 } } 

StaatszÀhler:


Die Implementierung ist nicht mehr so ​​offensichtlich, da unklar ist, wie die ZusammenfĂŒhrungsfunktion aussehen soll.

Betrachten Sie die folgenden Optionen:

Monoton ansteigender ZÀhler (nur InkrementzÀhler, G-ZÀhler):

Die Daten werden als Vektor mit einer Dimension gespeichert, die der Anzahl der Knoten (Versionsvektor) entspricht, und jedes Replikat erhöht den Positionswert mit seiner ID.

Die ZusammenfĂŒhrungsfunktion nimmt an den entsprechenden Positionen ein Maximum ein, und der Endwert ist die Summe aller Elemente des Vektors

\ begin {align} inc () &: V [id ()] = V [id ()] + 1 \\ value () &: \ sum_ {i = 0} ^ {n} V [i] \\ Merge (C_1, C_2) &: i \ in [1..n] \ Ergebnis [i] = max (C_1.V [i], C_2.V [i]) \ end {align}


Sie können auch das G-Set verwenden (siehe unten)

Anwendung:

  • Klicks / Treffer zĂ€hlen (sic!)

ZĂ€hler mit DekrementunterstĂŒtzung (PN-ZĂ€hler)

Wir starten zwei G-ZĂ€hler - einen fĂŒr Inkrementierungsoperationen, den zweiten - fĂŒr Dekrementierung

Anwendung:

  • Die Anzahl der angemeldeten Benutzer in einem P2P-Netzwerk, z. B. Skype

Nicht negativer ZĂ€hler

Eine einfache Implementierung existiert noch nicht. Schlagen Sie Ihre Ideen in den Kommentaren vor, diskutieren Sie.

6.2 Registrieren


Eine Speicherzelle mit zwei Operationen - Zuweisen (Schreiben) und Wert (Lesen).
Das Problem ist, dass die Zuweisung nicht kommutativ ist. Es gibt zwei AnsÀtze, um dieses Problem zu lösen:

Last-Write-Wins-Register (LWW-Register):


Wir geben die vollstĂ€ndige Reihenfolge durch die Generierung einer eindeutigen ID fĂŒr jede Operation ein (z. B. Zeitstempel).

Ein Beispiel fĂŒr die Synchronisation ist der Austausch von Paaren (Wert, ID):


Anwendung:

  • Spalten in Cassandra
  • NFS - Datei ganz oder teilweise

Register mit mehreren Werten (Multi-Value Register, MV-Register):


Der Ansatz Ă€hnelt einem G-ZĂ€hler - wir speichern die Menge (Wert, Versionsvektor). Wert registrieren - alle Werte beim ZusammenfĂŒhren - LWW separat fĂŒr jeden Wert im Vektor.


Anwendung:

  • Korb in Amazon. Damit ist ein bekannter Fehler verbunden, der nach dem Entfernen eines Artikels aus dem Warenkorb dort erneut angezeigt wird. Der Grund ist, dass das Register trotz der Tatsache, dass es eine Reihe von Werten speichert, keine Menge ist (siehe Abbildung unten). Amazon betrachtet dies ĂŒbrigens nicht einmal als Fehler - es steigert sogar den Umsatz.
  • Riak. In einem allgemeineren Fall verschieben wir das Problem der Auswahl des tatsĂ€chlichen Werts (Hinweis - es gibt keinen Konflikt!) Auf die Anwendung.

ErklÀrung des Fehlers in Amazon:



6.3 Lose


Die Menge ist der Grundtyp fĂŒr die Erstellung von Containern, Karten und Diagrammen und unterstĂŒtzt Operationen - add und rmv, die nicht kommutativ sind.

Betrachten Sie die naive Implementierung der op-basierten Menge, in der add und rmv ausgefĂŒhrt werden, sobald sie eintreffen (add kommt gleichzeitig zu 1 und 2 Replikaten, dann geht rmv zu 1).


Wie Sie sehen können, haben sich die Repliken schließlich aufgelöst. Betrachten Sie die verschiedenen Optionen zum Erstellen konfliktfreier Mengen:

Wachsendes Set (G-Set):


Die einfachste Lösung besteht darin, zu verhindern, dass Elemente gelöscht werden. Alles, was bleibt, ist die Kommutierungsoperation, die kommutativ ist. Die ZusammenfĂŒhrungsfunktion ist die Vereinigung von Mengen.

Zweiphasenset (2P-Set):


Wir erlauben Ihnen das Löschen, aber Sie können es nach dem Entfernen nicht wieder hinzufĂŒgen. Zur Implementierung richten wir einen separaten Satz entfernter G-Set-Elemente ein (ein solcher Satz wird als Tombstone-Satz bezeichnet).
Beispiel fĂŒr zustandsbasiert:

\ begin {align} lookup (e) &: e \ in A \ land e \ notin R \\ add (e) &: A = A \ cup \ {e \} \\ rmv (e) &: R = R \ cup \ {e \} \\ zusammenfĂŒhren (S_1, S_2) &: \\ Res & ult.A = S_1.A \ cup S_2.A \\ Res & ult.R = S_1.R \ cup S_2.R \ end {align}


LWW-Element Set:


Die nĂ€chste Möglichkeit, eine konfliktfreie Menge zu implementieren, besteht darin, eine vollstĂ€ndige Reihenfolge einzufĂŒhren. Eine der Optionen besteht darin, fĂŒr jedes Element eindeutige Zeitstempel zu generieren.

Wir erhalten zwei Mengen - add-set und remove-set, wenn add () aufgerufen wird, add (element, unique_id ()), wenn ĂŒberprĂŒft wird, ob sich ein Element in der Menge befindet - schauen Sie, wo der Zeitstempel grĂ¶ĂŸer ist - in remove-set oder in add-set


PN-Set:


Variation mit der Reihenfolge der Menge - wir starten einen ZĂ€hler fĂŒr jedes Element, wenn wir es hinzufĂŒgen, erhöhen wir es, wenn wir es löschen, verringern wir es. Ein Element befindet sich in der Menge, wenn sein ZĂ€hler positiv ist.

Beachten Sie den interessanten Effekt: In der dritten Replik fĂŒhrt das HinzufĂŒgen eines Elements nicht zu dessen Erscheinungsbild.

Beobachten-Entfernen-Set, ODER-Set, Add-Win-Set:


Bei diesem Typ hat HinzufĂŒgen Vorrang vor Entfernen. Implementierungsbeispiel: Jedem neu hinzugefĂŒgten Element wird ein eindeutiges Tag zugewiesen (relativ zum Element und nicht zum gesamten Satz). Rmv entfernt ein Element aus der Menge und sendet alle gesehenen Paare (Element, Tag) zur Entfernung an die Replikate.



Remove-Win-Set:


Ähnlich wie beim vorherigen, aber gleichzeitig gewinnt / rmv rmv.

6.4 Grafik


Dieser Typ basiert auf vielen. Das Problem ist folgendes: Wenn gleichzeitig Operationen addEdge (u, v) und removeVertex (u) ausgefĂŒhrt werden - was soll ich tun? Folgende Optionen sind möglich:

  • RemoveVertex-PrioritĂ€t, alle Kanten, die auf diesen Scheitelpunkt fallen, werden gelöscht
  • AddEdge-PrioritĂ€t, gelöschte Scheitelpunkte wiederhergestellt
  • Wir verzögern die AusfĂŒhrung von removeVertex, bis alle gleichzeitigen addEdge ausgefĂŒhrt werden.

Die einfachste Option ist die erste, fĂŒr die Implementierung (2P2P-Graph) reicht es aus, zwei 2P-Sets zu erhalten, eines fĂŒr die Eckpunkte und das zweite fĂŒr die Kanten

6.5 Karte


Karte der Literale:


Zwei zu lösende Probleme:

  • Was tun bei gleichzeitigen Put-Operationen? In Analogie zu ZĂ€hlern können Sie entweder LWW- oder MV-Semantik wĂ€hlen
  • Was tun mit simultan put / rmv? In Analogie zu Sets können Sie entweder Put-Wins- oder RMV-Wins- oder Last-Put-Wins-Semantik verwenden.

CRDT-Zuordnung (Karte der CRDTs):


Ein interessanter Fall, weil Ermöglicht das Erstellen verschachtelter Zuordnungen. FĂ€lle, in denen verschachtelte Typen geĂ€ndert werden, werden nicht berĂŒcksichtigt. Dies sollte vom verschachtelten CRDT selbst entschieden werden.

Karte als rekursiv zurĂŒcksetzen entfernen

Die Entfernungsoperation "setzt" den Typwert auf einen bestimmten Startzustand zurĂŒck. FĂŒr einen ZĂ€hler ist dies beispielsweise ein Nullwert.

Betrachten Sie ein Beispiel - eine allgemeine Einkaufsliste. Einer der Benutzer fĂŒgt Mehl hinzu und der zweite fĂŒhrt eine Kaufabwicklung durch (dies fĂŒhrt zu einem Aufruf des Löschvorgangs fĂŒr alle Elemente). Infolgedessen bleibt eine Einheit Mehl auf der Liste, was logisch erscheint.


Karte entfernen-gewinnt

Die Operation rmv hat Vorrang.

Beispiel: In einem Online-Spiel hat ein Alice-Spieler 10 MĂŒnzen und einen Hammer. Dann treten gleichzeitig zwei Ereignisse auf: Auf Replik A hat sie einen Nagel hergestellt, und auf Replik B wird ihr Charakter durch Entfernen aller Objekte gelöscht:



Beachten Sie, dass bei Verwendung von "Entfernen als rekursiv" möglicherweise ein Nagel verbleibt. Dies ist nicht der richtige Zustand, wenn das Zeichen entfernt wird.

Update gewinnt Karte

Aktualisierungen haben Vorrang oder brechen frĂŒhere VorgĂ€nge ab, um gleichzeitig rmv zu löschen.

Beispiel: In einem Online-Spiel wird der Alice-Charakter auf Replik B aufgrund von InaktivitĂ€t gelöscht, aber gleichzeitig wird AktivitĂ€t auf Replik A ausgefĂŒhrt. Offensichtlich muss der Löschvorgang abgebrochen werden.


Bei der Arbeit mit einer solchen Implementierung gibt es einen interessanten Effekt: Angenommen, wir haben zwei Replikate, A und B, und sie speichern die Menge mit einem SchlĂŒssel k. Wenn dann A den Wert des SchlĂŒssels k löscht und B alle Elemente der Menge löscht, hinterlassen die Replikate am Ende eine leere Menge mit dem SchlĂŒssel k.

Beachten Sie, dass eine naive Implementierung nicht ordnungsgemĂ€ĂŸ funktioniert. Sie können nicht einfach alle vorherigen LöschvorgĂ€nge rĂŒckgĂ€ngig machen. Im folgenden Beispiel wĂ€re bei diesem Ansatz der Endzustand der Anfangszustand, was falsch ist:


Liste


Das Problem bei diesem Typ besteht darin, dass Elementindizes auf verschiedenen Replikaten nach lokalen EinfĂŒge- / LöschvorgĂ€ngen unterschiedlich sind. Um dieses Problem zu lösen, wird der Operational Transformation-Ansatz angewendet. Bei der Anwendung der erhaltenen Änderung sollte der Index des Elements im ursprĂŒnglichen Replikat berĂŒcksichtigt werden.

7. Riak


Betrachten Sie als Beispiel CRDT in Riak:

  • ZĂ€hler: PN-ZĂ€hler
  • Set: OR-Set
  • Karte: Update gewinnt Karte der CRDTs
  • (Boolesches) Flag: OR-Set wo maximal 1 Element
  • Register: Paare (Wert, Zeitstempel)

8. Wer verwendet CRDT?


Der Wiki- Bereich enthÀlt gute Beispiele.

9. Referenzen


Source: https://habr.com/ru/post/de418897/


All Articles