Zum Inhalt springen
Deutsch

Snapshot-Store-Backend

ObjectStorageSnapshotStore ist die Snapshot-Store- Implementierung, die Object Storage als Backing-Schicht verwendet.

import {
ObjectStorageSnapshotStore,
ObjectStorageSnapshotStoreOptions,
S3ObjectStorageBackend,
S3ObjectStorageOptions,
PersistenceExtensionId,
} from 'actor-ts';
const s3ObjectStorageOptions = S3ObjectStorageOptions.create()
.withRegion(region)
.withBucket(bucket);
const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create()
.withBackend(new S3ObjectStorageBackend(s3ObjectStorageOptions))
.withCompression({ algorithm: 'gzip' })
.withEncryption(encryption);
const snapshotStore = new ObjectStorageSnapshotStore(objectStorageSnapshotStoreOptions);
system.extension(PersistenceExtensionId).setJournal(someJournal);
system.extension(PersistenceExtensionId).setSnapshotStore(snapshotStore);

Snapshots, die von jedem PersistentActor geschrieben werden, gehen in den S3-Bucket; komprimiert + verschlüsselt gemäß der Config.

Drei Muster:

  1. Cluster-weit geteilte Snapshots — Sharded Entities, die zwischen Nodes wandern, brauchen, dass jeder Node den Snapshot jeder Entity laden kann. Object Storage funktioniert; SQLite pro Node nicht.
  2. Verschlüsselte Snapshots — Server-Side- und/oder Client-Side-Verschlüsselung at rest für Compliance erforderlich.
  3. Günstiger Snapshot-Storage — Object Storage ist pro GB viel günstiger als SQL-Stores für selten-gelesene-gelegentlich-überschriebene Daten.

Für Single-Node-Deployments ist SqliteSnapshotStore schneller + einfacher.

type ObjectStorageSnapshotStoreOptionsType = {
backend: ObjectStorageBackend;
prefix?: string; // Default ''
keepN?: number; // Default 3
compression?: CompressionConfig | CompressionResolver;
encryption?: EncryptionConfig | EncryptionResolver;
maxDecompressedBytes?: number; // Default 512 MiB
};
FeldZweck
backendFilesystem- oder S3-Backend.
prefixObject-Key-Prefix. Nützlich, um Buckets zu teilen.
keepNPro persistenceId behaltene Snapshots; ältere werden beim Save entfernt. Default 3.
compressionAt-Rest-Kompression — siehe Kompression.
encryptionAt-Rest-Verschlüsselung — siehe Verschlüsselung.
maxDecompressedBytesSchutz vor Dekompressions-Bomben — Obergrenze für die Größe eines dekodierten Snapshots. Default 512 MiB.
<prefix>/<persistenceId>/seq-<seqNr>

Beispiele:

snapshots/account-42/seq-100
snapshots/account-42/seq-200
snapshots/account-42/seq-300

Das Framework listet Keys unter <prefix>/<persistenceId>/, um den neuesten Snapshot zu finden.

Für sehr große persistenceId-Räume ist das Listen pro pid in S3 typischerweise schnell (Per-Prefix-Durchsatz). Vermeide es, alle Entity-Typen unter denselben Prefix ohne Per-pid-Unterordner zu packen.

Snapshot-Writes gehen durch das PUT von Object Storage; Reads durch GET. Zahlen für S3 Same-Region:

  • Save — 20-50 ms pro Snapshot.
  • Load latest — 1 LIST + 1 GET = 30-60 ms.

Für Hot-Path-Snapshot-Laden (häufige Actor-Neustarts) wickle es mit CachedSnapshotStore ein:

const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create().withBackend(backend);
const cachedSnapshotStoreOptions = CachedSnapshotStoreOptions.create().withCache(cache);
const cached = new CachedSnapshotStore(
new ObjectStorageSnapshotStore(objectStorageSnapshotStoreOptions),
cachedSnapshotStoreOptions,
);

Reduziert redundante S3-GETs auf Sub-Mikrosekunden-Cache-Hits.

class Account extends PersistentActor<...> {
protected compression() { return { algorithm: 'zstd' as const }; }
protected encryption() { return { keyRing: accountKeyRing }; }
}

Per-Actor-Konfiguration gilt für Snapshots auf dieselbe Weise wie für Durable State. Siehe Per-Actor-Policies.

PersistentActor.persist(event) gelingt
↓ snapshotPolicy() prüfen
↓ wenn true → Snapshot machen
backend.put('snapshots/<pid>/seq-N', serialisierter State)
PersistentActor.preStart
↓ 'snapshots/<pid>/' auflisten, um die neueste seq zu finden
↓ backend.get(latest) → dekodieren → onEvent ab seq+1

Das Framework handhabt Snapshot-Saves + -Loads über dieses Layout; du setzt die Policy.

const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create()
.withBackend(backend)
.withKeepN(5);
new ObjectStorageSnapshotStore(objectStorageSnapshotStoreOptions); // die 5 neuesten behalten; ältere löschen

Pruning ist standardmäßig aktiv: keepN ist per Default 3, sodass jeder Save nur die neuesten Snapshots pro persistenceId behält und die älteren löscht. Erhöhe oder senke die Grenze mit withKeepN; setze keepN: 0, um Pruning zu deaktivieren und jeden Snapshot zu behalten.

Das Framework löscht beim Lesen nicht automatisch — es gibt ein kleines Fenster, in dem alte Snapshots mit neuen koexistieren.

{
const sqliteJournalOptions = SqliteJournalOptions.create().withPath('...');
const objectStorageSnapshotStoreOptions = ObjectStorageSnapshotStoreOptions.create().withBackend(backend);
journal: new SqliteJournal(sqliteJournalOptions),
snapshotStore: new ObjectStorageSnapshotStore(objectStorageSnapshotStoreOptions),
}

Das Journal und der Snapshot-Store sind unabhängig. Häufige Muster:

  • SQLite-Journal + ObjectStorage-Snapshots — lokale Event-Rate, geteilte Snapshots für Sharded Entities.
  • Cassandra-Journal + ObjectStorage-Snapshots — beide über den Cluster geteilt.
  • ObjectStorage alles — wenn S3 dein einziger Storage ist. Langsamer pro Op, aber billig.