Zum Inhalt springen
Deutsch

Durable Storage

Standardmäßig leben DistributedData-Replikas nur im Speicher. Ein Full-Cluster-Restart (jeder Node gleichzeitig down) verliert den gesamten State — jeder Key startet wieder bei empty.

Für State, der einen Cold Start überleben muss, gib DistributedData ein durables Backend mit DistributedDataOptions.withDurableStore(...):

import {
DistributedDataId,
DistributedDataOptions,
InMemoryDurableStateStore,
} from 'actor-ts';
// Any DurableStateStore works — InMemory here; a SQL / object-storage
// backend in production (see "Store choices" below).
const store = new InMemoryDurableStateStore();
const distributedDataOptions = DistributedDataOptions.create()
.withGossipInterval(1_000)
.withDurableStore(store);
const dd = system.extension(DistributedDataId).start(
cluster,
distributedDataOptions,
);

withDurableStore nimmt direkt einen DurableStateStore — keinen Wrapper. Intern umwickelt DistributedData ihn mit einem DurableDistributedDataStore, der anhand der eigenen Replika-ID dieses Nodes abgelegt wird, also ist Durability pro Replika: jedes Cluster-Mitglied besitzt einen durablen Datensatz.

Was du damit bekommst:

  • Bei preStart — bevor die Replika Gossip beitritt, lädt sie ihre persistierte Sicht aus dem Store.
  • Nach jeder Mutation — lokales update, eingehender Gossip-Merge und delete speichern die Sicht neu.

Nach einem Cold Start liefert dd.get(key) den zuletzt persistierten Wert — nicht undefined — und Gossip merged von dort über die Replikas hinweg neu.

Es gibt keine Key-Whitelist. Die gesamte lokale Sicht der Replika (jeder Key, den sie hält) wird serialisiert und als ein durabler Datensatz pro Replika geschrieben, unter der ID ddata|<replicaId>. Jeder Wert wird über das toJSON() des CRDT serialisiert.

Weil die gesamte Sicht ein Datensatz ist, schreibt jede Mutation den ganzen Datensatz neu — derselbe Full-State-Rewrite-Trade-off wie bei Durable-State-Actors. Für kleinen, heißen State (Counter, Flags, kleine Sets) ist das billig; für eine große Sicht unter hoher Mutationsrate kostet es Bandbreite und Write-Amplification (siehe die Cautions unten).

Jede DurableStateStore-Implementierung funktioniert — konstruiere sie genau wie für einen Durable-State-Actor und übergib sie dann an withDurableStore:

StoreVerwendung
InMemoryDurableStateStoreTests / Dev — im Prozess durable, beim Exit weg.
PostgresDurableStateStoreCluster-geteilte SQL-Durability.
MariaDbDurableStateStoreDie SQL-Alternative aus der MySQL-Familie.
ObjectStorageDurableStateStoreDateisystem- oder S3-backed Datensätze.

Siehe Durable State für die Konstruktionsoptionen jedes Stores — DistributedData verwendet dieselben Stores wieder, es gibt also nichts DD-Spezifisches zu konfigurieren.

Der Store ist nach Replika-ID indexiert, die beiden Deployment-Formen unterscheiden sich also darin, wo der Datensatz jeder Replika liegt, nicht darin, was ein Node lädt:

Per-node local store (e.g. object storage at a local path):
node-A's store ← ddata|node-A
node-B's store ← ddata|node-B
cold start → each node loads its own record; gossip reconverges
Shared backend (one Postgres / one S3 bucket):
shared store ← ddata|node-A, ddata|node-B, … (one record per replica)
cold start → each node loads its own record; gossip reconverges
  • Per-Node lokal — vollständige lokale Recovery, partitions-tolerant; aber der Datensatz eines zerstörten Nodes ist weg (Gossip + Überlebende stellen die Werte wieder her, da CRDTs konvergieren).
  • Shared — der Datensatz jeder Replika überlebt an einem Ort, selbst wenn der Node zerstört wird; kostet einen Roundtrip zum Shared Store beim Start und macht diesen Store zu einer geteilten Abhängigkeit.

Für die meisten Apps ist beides okay — CRDT-Konvergenz bedeutet, dass der Verlust des Datensatzes eines Nodes selten fatal ist.

withDurableStore ist der Weg, den du fast immer willst. Der Wrapper, den es baut, ist auch für sich nutzbar — ein positionaler Konstruktor über einem DurableStateStore plus einer Replika-ID, mit load() / save() / clear():

import { DurableDistributedDataStore, GCounter } from 'actor-ts';
import { InMemoryDurableStateStore } from 'actor-ts';
const store = new InMemoryDurableStateStore();
const durable = new DurableDistributedDataStore(store, 'replica-a');
const view = new Map([['hits', GCounter.empty().increment('replica-a', 5)]]);
await durable.save(view); // persist the whole view
const restored = await durable.load(); // Map<string, Crdt> (empty if nothing stored)
await durable.clear(); // forget this replica's record

Das ist, was das Framework unter der Haube nutzt; greife nur für Tooling oder Tests direkt darauf zu.

const distributedDataOptions = DistributedDataOptions.create()
.withDurableStore(store);
const dd = system.extension(DistributedDataId).start(
cluster,
distributedDataOptions,
);
// On preStart, before joining gossip:
// 1. load() this replica's record from the durable store.
// 2. decode each entry into its CRDT.
// 3. seed the local view.
// 4. then join the gossip layer.
const value = dd.get('hits'); // already reflects the persisted view

Der Start ist durch die Lesegeschwindigkeit des Durable Store begrenzt. Für einen lokalen Store mit einer Handvoll Keys sub-millisekündlich; für ein geteiltes SQL- / S3-Backend einstellige Sekunden.

Für At-Rest-Verschlüsselung oder Kompression konfiguriere sie am zugrunde liegenden Store, wo das Backend es unterstützt (z. B. der Object-Storage-Store — siehe Object Storage Encryption).