Object Storage im Überblick
Das Object-Storage-Backend des Frameworks ist eine S3-kompatible Persistenz-Schicht. Zwei Implementierungen werden mitgeliefert:
| Backend | Verwendung |
|---|---|
FilesystemObjectStorageBackend | Lokale Dateien; Dev + Tests. |
S3ObjectStorageBackend | Alles S3-kompatible (AWS S3, MinIO, R2, B2). |
Verwendet von:
ObjectStorageDurableStateStore— Durable State im Cloud-Storage.ObjectStorageSnapshotStore— Snapshots im Cloud-Storage.
Gebaut auf einem kleinen Interface (PUT / GET / DELETE / LIST + CAS-Unterstützung); beide Backends sprechen dieselbe Oberfläche.
Wann verwenden
Abschnitt betitelt „Wann verwenden“| Du solltest Object Storage verwenden, wenn… |
|---|
| Du Cloud-native bist und S3 (oder ähnlich) deine Storage-Plattform ist. |
| Du geteilte Persistenz über Cluster-Nodes willst, ohne Cassandra zu betreiben. |
| Du Server-Side-Encryption über Cloud-KMS willst. |
| Du unendliches Scaling willst, ohne Storage-Kapazität zu verwalten. |
Für Single-Node-Deployments ist SQLite einfacher. Für High-Throughput-Multi-Node-Persistenz ist Cassandra schneller. Object Storage sitzt dazwischen — cloud-freundlich, anständige Performance, viele Features.
Ein minimales Beispiel
Abschnitt betitelt „Ein minimales Beispiel“import { DurableStateOptions, ObjectStorageDurableStateStore, ObjectStorageDurableStateStoreOptions, S3ObjectStorageBackend, S3ObjectStorageOptions,} from 'actor-ts/persistence';
const s3ObjectStorageOptions = S3ObjectStorageOptions.create() .withRegion('eu-west-1') .withBucket('my-app-state');const backend = new S3ObjectStorageBackend(s3ObjectStorageOptions);
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create().withBackend(backend);const stateStore = new ObjectStorageDurableStateStore( objectStorageDurableStateStoreOptions,);
const durableStateOptions = DurableStateOptions.create<State>() .withPersistenceId(`cart-${userId}`) .withStore(stateStore) .withEmptyState(() => ({ items: [] }));const cart = system.spawnAnonymous(() => new Cart(durableStateOptions));Der State von Cart lebt in S3 unter cart-<userId> als
Object-Key. Reads und Writes gehen durch die S3-API.
Was gespeichert wird
Abschnitt betitelt „Was gespeichert wird“Object-Keys folgen einem vorhersagbaren Layout:
state/ cart-user-42 # ein Object pro persistenceId cart-user-43
snapshots/ account-42/ seq-100 # Snapshots indexiert nach seqNr seq-200 seq-300Das Framework verwaltet dieses Layout; du baust keine Keys manuell.
Das Interface
Abschnitt betitelt „Das Interface“interface ObjectStorageBackend { put(key: string, body: Uint8Array, options?: PutOptions): Promise<{ etag: string }>; get(key: string): Promise<Option<ObjectFetched>>; delete(key: string): Promise<void>; list(options: { prefix: string; limit?: number }): Promise<ObjectInfo[]>;}Kleine Oberfläche — passt zu AWS S3, MinIO, Cloudflare R2, Backblaze B2, Wasabi, etc. Die meisten S3-kompatiblen APIs passen exakt.
CAS für optimistische Concurrency
Abschnitt betitelt „CAS für optimistische Concurrency“await backend.put('state/cart-42', body, { ifMatch: 'previous-etag',});ifMatch lässt Aufrufer Compare-and-Set-Writes machen — wenn
das aktuelle ETag abweicht, schlägt das Put mit
ObjectStorageConcurrencyError fehl. Verwendet von
ObjectStorageDurableStateStore, um nebenläufige Writer ohne
separate Koordination zu erkennen.
ifNoneMatch: '*' ist Create-only — gelingt nur, wenn der
Key noch nicht existiert.
Einige ältere S3-kompatible Stores beachten diese Header nicht richtig. Die Backend-Implementierungen des Frameworks werfen klar in diesem Fall, anstatt still zu ignorieren; prüfe die CAS-Unterstützung deines Providers, bevor du dich darauf verlässt.
Backends
Abschnitt betitelt „Backends“Filesystem
Abschnitt betitelt „Filesystem“import { FilesystemObjectStorageBackend, FilesystemObjectStorageOptions } from 'actor-ts/persistence';
const filesystemObjectStorageOptions = FilesystemObjectStorageOptions.create().withDir('/var/lib/actor-ts');const backend = new FilesystemObjectStorageBackend( filesystemObjectStorageOptions,);Speichert Objects als Dateien unter rootDir. Kein Netzwerk,
keine S3-Kosten. Richtig für:
- Tests — gleicher Code-Pfad wie Produktion, mit lokalen Dateien.
- Lokales Dev — kein MinIO-Container erforderlich.
- Kleine Single-Node-Deployments — wenn du speziell das Object-Storage-Interface ohne S3 willst.
Das Root-Verzeichnis ist die gesamte Welt dieses Backends. Ein
Key mit .., absolutem Präfix oder NUL-Byte wird direkt
abgelehnt, und jede Operation kanonisiert zusätzlich den Pfad, den
sie gleich anfasst — ein Symlink, der im Root liegt und aus ihm
herauszeigt, wird also abgelehnt statt verfolgt. Das Root selbst
darf ein Symlink sein: jede Operation löst es auf und misst das
Containment am Ergebnis, /var/lib/app -> /mnt/data funktioniert
also normal.
„Der Pfad, den sie gleich anfasst“ meint die Objektnamen: den Key
selbst, die Verzeichnisse darüber und seinen .meta.json-Sidecar.
Der Key und seine Eltern werden aus unterschiedlichen Gründen
geprüft, und keine der beiden Prüfungen deckt die andere ab —
rename folgt nie einem Link an der letzten Komponente, ein
verlinktes Elternverzeichnis verschiebt also, wo ein Body
landet, während get und das Compare-and-Swap von put den Key
beide mit readFile öffnen, und das folgt sehr wohl einem Link.
Drei Grenzen sollte man kennen, denn sonst nimmt man mehr an, als
die Prüfung leistet. Sie liest das Dateisystem und handelt
danach, lehnt also einen bereits vorhandenen Link ab, kann aber
das Rennen gegen einen nicht gewinnen, der genau dazwischen
gelegt wird — portables O_NOFOLLOW ist über node:fs nicht
erreichbar. Sie schließt die eigenen Operationen des Backends
ein; sie ist keine Sandbox um ein Verzeichnis, in das andere
lokale Prozesse schreiben dürfen. Wenn nicht vertrauenswürdige
Nutzer Einträge im Storage-Root anlegen können, ist das bereits
das größere Problem.
Und sie deckt die Lock-Datei pro Key nicht ab. Das Nehmen des
Locks ist ein exklusives Create, das einen Link ablehnt statt ihm
zu folgen — aber die Stale-Lock-Wiederherstellung, die nach
Ablauf des Acquisition-Timeouts läuft, macht ein stat auf diesen
Namen, und stat folgt sehr wohl einem Link. Ein an <key>.lock
platzierter Link lässt damit eine Datei außerhalb des Roots
entscheiden, ob das Lock als verwaist gilt, und ein toter Link
dort hält put und delete auf genau diesem Key im Retry, statt
sie zurückkehren zu lassen.
import { S3ObjectStorageBackend, S3ObjectStorageOptions } from 'actor-ts/persistence';
const s3ObjectStorageOptions = S3ObjectStorageOptions.create() .withRegion('eu-west-1') .withBucket('my-app-state') .withEndpoint('https://s3.eu-west-1.amazonaws.com') // optionaler Override .withCredentials({ accessKeyId: process.env.AWS_ACCESS_KEY_ID!, secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!, });const backend = new S3ObjectStorageBackend(s3ObjectStorageOptions);Funktioniert mit jedem S3-kompatiblen Service. Für Nicht-AWS:
// MinIO:const s3ObjectStorageOptions = S3ObjectStorageOptions.create() .withRegion('us-east-1') .withBucket('my-bucket') .withEndpoint('http://minio:9000') .withForcePathStyle(true);new S3ObjectStorageBackend(s3ObjectStorageOptions);
// Cloudflare R2:const s3ObjectStorage2Options = S3ObjectStorageOptions.create() .withRegion('auto') .withBucket('my-bucket') .withEndpoint('https://<account-id>.r2.cloudflarestorage.com');new S3ObjectStorageBackend(s3ObjectStorage2Options);
// Backblaze B2:const s3ObjectStorage3Options = S3ObjectStorageOptions.create() .withRegion('us-west-002') .withBucket('my-bucket') .withEndpoint('https://s3.us-west-002.backblazeb2.com');new S3ObjectStorageBackend(s3ObjectStorage3Options);Optionale Features
Abschnitt betitelt „Optionale Features“| Feature | Seite |
|---|---|
| Kompression (gzip / zstd) | Kompression |
| Verschlüsselung at rest (AES-GCM) | Verschlüsselung |
| Body-Integrität (HMAC-SHA256) | Body-Integrität |
| Storage-Key-Bindung + Rollback-Schutz | Einen Body an seinen Key binden |
| Master-Key-Rotation | Schlüsselrotation |
| Per-Actor-Kompressions- / Verschlüsselungs-Policies | Per-Actor-Policies |
| Snapshot-Store-Backend | Snapshot-Store-Backend |
Alle optional — starte ohne; schichte nach Bedarf darauf.
Performance
Abschnitt betitelt „Performance“Grobe Zahlen für S3:
- Put (kleines Objekt): 20-50 ms.
- Get: 10-30 ms.
- Delete: 30-50 ms.
Filesystem-Backend: Sub-Millisekunde.
Ein LIST im Filesystem-Backend liest nur das Verzeichnis, das
sein Prefix benennt — alles bis zum letzten / des Prefix —,
kostet also den getroffenen Teilbaum und nicht das gesamte
Storage-Root. Genau das hält ein Prefix pro persistenceId wie
snapshots/<pid>/ im Sub-Millisekunden-Bereich, egal wie viele
andere Entities sich das Root teilen. Ein Prefix ohne /
(snapshots) hat kein Verzeichnis, auf das es einschränken
könnte, und läuft weiterhin über alles — bevorzuge daher Keys mit
einem Verzeichnis-Segment pro Entity.
Object Storage ist langsamer als SQLite / Cassandra für einzelne Operationen. Kompensiere mit:
- Snapshot-Policies, die die Recovery begrenzen.
- CachedSnapshotStore-Decorator für Read-Through-Caching.
- Batching, wo immer das Framework es erlaubt.
Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Kompression — gzip / zstd At-Rest-Kompression.
- Verschlüsselung — AES-GCM-Verschlüsselung at rest.
- Snapshot-Store-Backend — Snapshots in Object Storage.
- Durable State — der Haupt-Konsument.
- Persistenz im Überblick — das größere Bild.
