Replicated Snapshots
In Single-Writer-Event-Sourcing speichern Snapshots den State bei einer bestimmten seqNr — Recovery lädt den Snapshot, spielt Events nach dieser seqNr ab.
In Replicated Event Sourcing ist das Bild komplexer — es gibt keine einzelne lineare seqNr; stattdessen sind Events über Replicas hinweg partiell über Vector Clocks geordnet. Snapshots müssen die Vector Clock neben dem State tragen.
Snapshot-Inhalt: - state (nach Anwendung aller kausal gesehenen Events zur Zeit) - vector clock ({ A: 100, B: 95, C: 60 })Bei der Recovery:
1. Snapshot laden.2. Events aus dem Journal NACH der vc des Snapshots lesen.3. Jedes anwenden (Conflict Resolution erneut ausführen, wenn welche nebenläufig sind).4. Bereit.Die vc lässt die wiederherstellende Replica Events überspringen, die kausal dem Snapshot vorausgehen (bereits einbezogen).
Wann snapshotten
Abschnitt betitelt „Wann snapshotten“Gleiche Auswahl-Heuristik wie bei Single-Writer-ES, aber Bias in Richtung häufiger:
- Replizierte Workloads akkumulieren Events von mehreren Replicas gleichzeitig.
- Das Journal wächst schneller (N Replicas × Pro-Replica-Rate).
- Die Recovery muss Conflict Resolution erneut ausführen für jedes nicht gesnappshottete nebenläufige Event.
class Account extends ReplicatedEventSourcedActor<...> { override snapshotPolicy() { return everyNEvents(100); // alle 100 Events }}Für replizierte Entities, die 1000 Events/Tag über alle Replicas akkumulieren, bedeutet snapshotten alle 100 höchstens 100 Events, die bei der Recovery neu verarbeitet werden müssen — Sub-Sekunde.
Was serialisiert wird
Abschnitt betitelt „Was serialisiert wird“Ein ReplicatedSnapshot trägt mehr als State + Clock:
{ state: State, vc: { A: 100, B: 95, C: 60 }, // aktuelle Vector-Clock-Sicht seenIds: [ ... ], // Dedupe-Set: (replica, seqAtReplica) events: [ ... ], // kanonische sortierte Historie — das schwerste Feld localSeq: 205, // _localSeq zur Snapshot-Zeit journalSeqAtSnapshot: 4096, // Recovery setzt ab hier + 1 fort takenBy: 'A', // Replica, die ihn genommen hat (informativ) takenAt: 1716297600000 // Wall-Clock ms (informativ)}Der Snapshot ist ein normaler Snapshot-Blob, der im
Standard-Snapshot-Store persistiert wird; bestehende Stores
(In-Memory, SQLite, Object Storage) handhaben das ohne Änderungen.
Beachte, dass er die vollständige deduplizierte events-Historie
und das seenIds-Dedupe-Set trägt — nicht nur State + Clock —
sodass ein out-of-order eintreffendes Remote-Event neu falten
kann, ohne das gesamte Journal erneut zu lesen. events ist das
schwerste Feld: ein Actor mit 100k Events snapshottet 100k
Envelopes auf der Festplatte.
Recovery-Flow
Abschnitt betitelt „Recovery-Flow“preStart(): ↓ neuesten Snapshot laden ↓ state = snapshot.state ↓ vc = snapshot.vc ↓ Events aus dem Journal lesen ↓ für jedes Event im Journal: ↓ wenn event.vc <= snapshot.vc: überspringen (bereits einbezogen) ↓ sonst wenn event.vc nebenläufig zu vc: Resolver aufrufen ↓ sonst: über onEvent anwenden ↓ bereitDer “Skip”-Fall macht Snapshots die Recovery-Zeit begrenzen — Events, die vor dem Snapshot geschrieben wurden, werden übersprungen.
Snapshot-Stores
Abschnitt betitelt „Snapshot-Stores“Alle Standard-Snapshot-Stores funktionieren:
- InMemorySnapshotStore — Tests.
- SqliteSnapshotStore — Single-Node (selten für Replicated ES).
- ObjectStorageSnapshotStore — über Replicas geteilt.
Für Replicated ES mit mehreren Replicas über Regionen ist ein geteilter Snapshot-Store kritisch — jede Replica stellt schneller wieder her, wenn sie den neuesten Snapshot von jeder Replica laden kann, nicht nur von ihrem eigenen.
{ const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create().withBackend(new S3ObjectStorageBackend(S3ObjectStorageOptions.create() /* geteilter Bucket */)); journal: sharedJournal, snapshotStore: new ObjectStorageSnapshotStore(objectStorageSnapshotStoreOptions),}Vector Clocks wachsen mit pensionierten Replicas
Abschnitt betitelt „Vector Clocks wachsen mit pensionierten Replicas“Langlebige Deployments akkumulieren pensionierte Replicas in Vector Clocks:
vc { A: 1000, B: 500, C: 200, RETIRED-D: 50, RETIRED-E: 30 }Die Komponenten der pensionierten Replicas sind inert, nehmen aber Platz ein + verlangsamen Vergleiche.
Vector-Clock-Garbage-Collection ist außerhalb des Umfangs für v1 — es gibt keinen Pruning-Hook, und ein Snapshot speichert die vollständige, ungeprunte Vector Clock. VC-Einträge wachsen mit der Menge aller je gesehenen Replicas: in Ordnung für einen stabilen Cluster, aber ein Deployment mit hoher Node-Fluktuation wird irgendwann Kompaktierung wollen. Behalte das im Hinterkopf, bevor du dich auf aggressive Replica-Rotation verlässt.
Nebenläufige Writes während des Snapshots
Abschnitt betitelt „Nebenläufige Writes während des Snapshots“Replica A schreibt event_A bei t1.Replica A snappshottet bei t2 (sieht den State mit event_A). snapshot.vc = { A: 1 }
Inzwischen schrieb Replica B nebenläufig event_B bei t1.5. event_B hat vc { B: 1 }; nicht im Snapshot.
Replica A liest event_B bei t3: snapshot.vc { A: 1 } vs event_B.vc { B: 1 } → nebenläufig Resolver aufrufen, anwenden.Nebenläufige Events, die nach dem Snapshot eintreffen, werden zur Lesezeit vom Resolver behandelt — dasselbe wie ohne Snapshots.
Performance
Abschnitt betitelt „Performance“Snapshot-Writes für Replicated ES sind etwas schwerer als Single-Writer:
- Vector-Clock-Serialisierung — typischerweise 50-200 Bytes zusätzlich pro Snapshot.
- Resolver-State-Merging — wenn der Snapshot während Nebenläufig-Write-Abgleich genommen wird, läuft der Merge zuerst.
In den meisten Fällen vernachlässigbar. Größere Snapshots kommen vom State selbst.
Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Replicated Event Sourcing im Überblick — das größere Bild.
- Vector Clocks — was neben dem State gespeichert wird.
- Conflict Resolver — während der Recovery für nebenläufige Events aufgerufen.
- Snapshots — das Single-Writer-Gegenstück.
