Zum Inhalt springen
Deutsch

Kompression

Object Storage unterstützt At-Rest-Kompression — Bodies werden vor dem Put komprimiert, beim Get dekomprimiert. Drei Modi; wähle den CPU-gegen-Größe-Kompromiss, der zum Payload passt.

import {
ObjectStorageDurableStateStore,
S3ObjectStorageBackend,
} from 'actor-ts';
const store = new ObjectStorageDurableStateStore({
backend: new S3ObjectStorageBackend({ /* ... */ }),
compression: {
algorithm: 'zstd',
level: 6, // optional; zstd 1–22 (Default 3)
},
});

Jetzt wird jeder persistierte State vor dem Upload zstd-komprimiert. Reads dekomprimieren transparent.

AlgorithmusKompressionsverhältnisCPU-KostenWann
zstdAm bestenNiedrig–moderat (Decode sehr schnell)Große State-Blobs — bestes Verhältnis bei konkurrenzfähiger Geschwindigkeit.
gzipGut (typisch 50-70 %)ModeratUniverseller Default — node:zlib, läuft auf jedem Runtime.
noneKeineNullBereits komprimiert oder winzige Payloads (siehe Hinweis unten).

Der Store-Default ist gzip — es braucht keine native Runtime-Unterstützung und keine Zusatz-Dependency. Für text-lastigen State (JSON) erreichen gzip und zstd beide ~70 % Reduktion; zstd kommt schneller dahin und dekomprimiert flotter. Für bereits komprimierte Daten (verschlüsselte Bytes, Image-Daten) bringt Kompression fast keinen Wert und verschwendet CPU — nimm none.

Es gibt kein brotli und kein deflate — nur none, gzip und zstd.

type CompressionAlgo = 'none' | 'gzip' | 'zstd';
interface CompressionConfig {
algorithm: CompressionAlgo;
level?: number; // algorithmus-spezifisch; geclamped; undefined = Default
}

Level sind algorithmus-spezifisch. Out-of-range-Werte werden geclamped; ohne level gilt der Implementierungs-Default:

  • gzip: 0 (keine Kompression) – 9 (am besten). Default 6.
  • zstd: 1 (am schnellsten) – 22 (am besten). Default 3.
  • none: ignoriert.

Für die meisten Workloads sind die Defaults in Ordnung — dreh nur hoch, wenn Storage-Kosten dominieren.

zstd-Support unterscheidet sich je Richtung:

  • Compress (Write) — nur nativ: Bun (Bun.zstdCompressSync) oder Node ≥ 22.15 (zlib.zstdCompressSync). Es gibt keinen Pure-JS-Fallback fürs Schreiben; die optionale fzstd-Peer-Dependency kann nur dekomprimieren. zstd auf einem Runtime ohne native Unterstützung schlägt früh fehl — eager bei der Plugin-Registrierung, nicht kryptisch beim ersten Persist.
  • Decompress (Read) — erst nativ, dann die optionale fzstd-Peer-Dependency, sodass ein Runtime ohne natives zstd anderswo geschriebene zstd-Bodies trotzdem lesen kann.

gzip nutzt node:zlib und läuft überall (Bun, Node, Deno) ohne Zusatz-Dependency.

Beim Put:
Wert serialisieren → JSON-Bytes → compress(bytes) → [verschlüsseln] →
mit ATS1-Manifest rahmen → backend.put(framed)
Beim Get:
backend.get → gerahmte Bytes → ATS1-Manifest lesen → [entschlüsseln] →
gemäß Algorithmus-Flag dekomprimieren → JSON → deserialisieren

Jeder Body trägt einen kleinen ATS1-Manifest-Header; ein 2-Bit-Feld im Flags-Byte hält den Kompressions-Algorithmus fest (none / gzip / zstd). Die Dekompression wird ausschließlich von diesem Flag gesteuert — nicht von einem HTTP-Header. (Auf S3 setzt das Framework zusätzlich Content-Encoding als Marker, aber das ist nie die Quelle der Wahrheit fürs Decoding.)

Weil jeder Body seinen Algorithmus selbst beschreibt, funktioniert das Mischen von none / gzip / zstd im selben Bucket — jedes Objekt dekodiert gemäß seinem eigenen Manifest.

Das Level ist eine reine Encoder-Einstellung. Es wird nicht auf dem Wire gespeichert (das Manifest hält den Algorithmus fest, nicht das Level), und der Decompressor braucht es nie — gzip- und zstd-Frames sind selbst-beschreibend.

Daher braucht ein Level-Wechsel keine Migration: Bodies, die mit dem alten Level geschrieben wurden, dekodieren weiter, neue Bodies nutzen das neue Level, und beide mischen sich problemlos. Du kannst das Level auch jederzeit per Actor anheben oder senken, ohne bestehende Daten anzufassen.

serialisieren → JSON-Bytes → compress → [verschlüsseln] → speichern

Kompression läuft nach der Serialisierung und vor der Verschlüsselung — Ciphertext zu komprimieren ist sinnlos (hochentrop), und die feste Reihenfolge vermeidet zudem CRIME-artige Seitenkanäle. Für maximale Größenreduktion bei großem text-lastigem State ist zstd auf höherem Level meist der beste einzelne Hebel.

SzenarioAlgorithmus
Großer text-lastiger State (großes JSON, lange Beschreibungen)zstd
Gemischt Text + Zahlenzstd oder gzip
Maximale Portabilität, natives zstd nicht garantiertgzip
Bereits verschlüsselte / komprimierte Bytesnone (kein Nutzen)
Storage-Kosten-begrenzt, Write-once-Read-manyzstd (Level 15-19)
CPU-begrenzter Write-Pfadgzip Level 1 oder zstd Level 1

Single-Thread, ~100 KB Blob — nur Größenordnung; echte Zahlen hängen von Payload und Runtime ab:

  • gzip Level 6 — ~1-3 ms Encode, ~0,5 ms Decode.
  • gzip Level 1 — ~0,5 ms Encode, schnelles Decode.
  • zstd Level 3 (Default) — Encode vergleichbar mit gzip, besseres Verhältnis, sehr schnelles Decode.
  • zstd Level 19 — spürbar langsamer beim Encode, Decode weiter schnell.
  • zstd Level 20-22 (ultra) — deutlich langsamer beim Encode für marginalen Gewinn; reserviere es für Write-once-Read-many-Bulk-Daten.

zstds Asymmetrie (günstiges Decode auf jedem Level) macht es zu einem starken Default für lese-lastigen State. Für typische State-Store-Workloads (kleine Objekte, moderate Schreibrate) sind die CPU-Kosten unsichtbar.

class SmallStateActor extends DurableStateActor<...> {
protected compression() { return { algorithm: 'none' as const }; }
}
class LogStateActor extends DurableStateActor<...> {
protected compression() { return { algorithm: 'zstd' as const, level: 19 }; }
}

Überschreibe den Store-Level-Default pro Actor. Nützlich, wenn:

  • Die meisten Actors kleinen State haben (keine Kompression nötig).
  • Einige großen text-lastigen State haben (hohe Kompression).

Siehe Per-Actor-Policies.