cluster-file-backend — TYPO3-Cache für Kubernetes, ohne Shared Filesystem.

Drop-in-Ersatz für FileBackend und SimpleFileBackend in Kubernetes-Deployments. Cache-Gültigkeit kommt aus einem zweiten TYPO3-Cache-Frontend (Backend frei wählbar), Payloads werden pod-lokal als atomar geschriebene Dateien materialisiert. Kein RWX-Volume zwischen Pods, deterministische Re-Materialisierung über sha256-Hash-Validierung, Tag-basierte Invalidierung clusterweit, Deployment-Time-Warmup.

Architektur in einem Diagramm

Dieses Paket weiß nichts über Redis/Valkey/KV-Stores. Es spricht ausschließlich mit der TYPO3-Cache-API und delegiert die Cluster-Persistenz an ein vom Anwender gewähltes TYPO3-Cache-Backend.

TYPO3 Cache API → ClusterFileBackend
                      │
                      ├─► Metadata-Cache (zweites TYPO3-Cache-Frontend,
                      │   Backend frei wählbar: Typo3DatabaseBackend,
                      │   KeyValueBackend, MemcachedBackend, …)
                      │
                      └─► Local Payload Store (pod-lokal, emptyDir)

 

Was es ist

Was es nicht ist

Voraussetzungen

Setup-Voraussetzungen — was Sie einmalig tun

Das Paket registriert die Caches bewusst nicht automatisch. Hostnamen, Ports, TLS, Pfade sind grundsätzlich site-spezifisch. Die folgenden Schritte sind ein einmaliges Setup.

Fünf erforderliche Schritte

  1. Composer-Installation: composer require moselwal/cluster-file-backend:^1.0.1
  2. Cluster-fähiges Cache-Backend für die Metadaten bereitstellen. Der Default verwendet TYPO3-Cores Typo3DatabaseBackend — funktioniert ohne zusätzliche Dependency, solange die Datenbank von allen Pods erreichbar ist (Galera, RDS Multi-AZ, …). Für höhere Performance moselwal/keyvalue-store installieren und dessen KeyValueBackend nutzen.
  3. TYPO3-Cache-Frontend (Konvention: cluster_meta) für die Metadaten registrieren.
  4. Die dateibasierten TYPO3-Caches (pages, pagesection, rootline, imagesizes, assets, hash) auf ClusterFileBackend umstellen und per metadataCacheIdentifier auf cluster_meta verweisen.
  5. Pod-lokales emptyDir unter /app/var/cache/cluster/ (oder dem konfigurierten localPath) mounten.

Was das Paket mitliefert

ArtefaktPfadZweck
Default-Config (ohne Extra-Deps)Configuration/Example/cache-configurations.example.phpDatenbank-basierte Metadaten plus Cluster-File-Caches — läuft auf jeder TYPO3-Installation
Redis/Valkey-Config (optional)Configuration/Example/cache-configurations-redis.example.phpPerformance-Variante mit moselwal/keyvalue-store
JSON-SchemaConfiguration/Backend/ClusterFileBackend.options.schema.jsonWird beim Backend-Konstruktor validiert — fehlerhafte Konfiguration führt zu InvalidCacheException mit Feldname
CLI-KommandosConfiguration/Commands.phpclusterfilebackend:gc, clusterfilebackend:warmup
Event-ListenerConfiguration/Services.yamlHängt sich in TYPO3s CacheWarmupEventbin/typo3 cache:warmup triggert die Cluster-Warmup mit
DI-BindingsConfiguration/Services.yamlAuto-Discovery für MetricsPort, ClockPort, CompressorPort

Konstruktor-Validation

Der ClusterFileBackend-Konstruktor validiert seine Optionen gegen ein JSON-Schema. Pflichtfelder (sonst InvalidCacheException): localPath (absoluter Pfad), metadataCacheIdentifier (Name des Metadata-Cache-Frontends), namespace.environment (prod, staging, testing oder development) und namespace.instance (Slug [a-z0-9-]{1,64}). Wenn der konfigurierte metadataCacheIdentifier nicht als TYPO3-Cache registriert ist, scheitert der Konstruktor sofort mit einer Nachricht, die den Config-Pfad benennt — kein stilles Fehlschlagen beim ersten set().

OptionsfeldDefaultBedeutung
compressionzstdzstd | gzip | none
serializerigbinaryigbinary | php
defaultLifetimeSeconds3600TTL wenn der Caller null übergibt
maxPayloadBytes10485760 (10 MB)Schreibvorgänge darüber werden mit InvalidDataException abgelehnt

Konfiguration — Quick-Start und Varianten

Quick-Start (null Extra-Dependencies)

Den Inhalt von vendor/moselwal/cluster-file-backend/Configuration/Example/cache-configurations.example.php in die config/system/settings.php (oder additional.php) kopieren und environment, instance und localPath auf das Deployment anpassen. Dieses Beispiel nutzt TYPO3-Cores Typo3DatabaseBackend für den Metadaten-Cache — cluster-sicher, sobald die Datenbank cluster-betrieben ist.

Redis/Valkey-Variante

Für Sub-Millisekunden-Latenz auf den Metadaten Configuration/Example/cache-configurations-redis.example.php nehmen. Verwendet den KeyValueBackend aus moselwal/keyvalue-store mit optionaler TLS- und Sentinel-Unterstützung.

Manuelles Setup

Schritt 1: Ein TYPO3-Cache-Frontend für die Metadaten definieren. Jedes Backend, das TaggableBackendInterface implementiert (für flushByTag), funktioniert.

 

$GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']['cluster_meta'] = [
    'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
    'backend'  => \TYPO3\CMS\Core\Cache\Backend\Typo3DatabaseBackend::class,
    'options'  => [],
    'groups'   => ['system'],
];

 

Schritt 2:ClusterFileBackend auf den Metadaten-Cache verweisen — für alle dateibasierten Caches gleichzeitig.

 

foreach (['pages', 'pagesection', 'rootline'] as $cacheName) {
    $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$cacheName] = [
        'frontend' => \TYPO3\CMS\Core\Cache\Frontend\VariableFrontend::class,
        'backend'  => \Moselwal\Typo3ClusterCache\Infrastructure\Cache\Backend\ClusterFileBackend::class,
        'options'  => [
            'localPath'               => '/app/var/cache/cluster/' . $cacheName,
            'metadataCacheIdentifier' => 'cluster_meta',
            'namespace' => [
                'environment' => 'prod',
                'instance'    => 'website-a',
            ],
        ],
        'groups' => ['pages'],
    ];
}

Kubernetes-Deployment, Warmup und Garbage Collection

Pod-Volume für Payloads

 

volumes:
  - name: cluster-cache
    emptyDir: { sizeLimit: 2Gi }
volumeMounts:
  - name: cluster-cache
    mountPath: /app/var/cache/cluster

 

Deployment-Time-Warmup

Nach einem Rolling-Deploy sollen neue Pods typischerweise erst prüfen, ob sie den Metadaten-Cache erreichen und ob localPath beschreibbar ist, bevor sie Traffic annehmen. Der Warmup lässt sich explizit triggern:

 

./vendor/bin/typo3 clusterfilebackend:warmup \
    --namespace=cfb:prod:website-a:pages \
    --namespace=cfb:prod:website-a:pagesection \
    --namespace=cfb:prod:website-a:rootline

 

Das Kommando emittiert eine JSON-Zeile pro Namespace und beendet sich mit Exit-Code ≠ 0, wenn irgendein Namespace die Health-Checks nicht besteht. Damit lässt es sich in Readiness-/Startup-Probes oder Post-Deploy-Jobs einbinden.

Alternativ den TYPO3-Standard-Warmup nutzen — der Event-Listener hängt sich automatisch ein:

 

./vendor/bin/typo3 cache:warmup

 

Garbage Collection als CronJob

 

apiVersion: batch/v1
kind: CronJob
metadata:
  name: clusterfilebackend-gc-pages
spec:
  schedule: "*/15 * * * *"
  concurrencyPolicy: Forbid
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: typo3-cli
              args: ["clusterfilebackend:gc", "--namespace=cfb:prod:website-a:pages"]

 

Architektur intern

DDD-4-Layer (Domain → Application → Infrastructure → Presentation), enforced via deptrac. Die einzige Außenschnittstelle für „zentrale Wahrheit" ist der MetadataCachePort, implementiert vom Typo3MetadataCache-Adapter, der jeden beliebigen TYPO3-FrontendInterface annimmt.

Cluster-Konsistenz — was passiert beim Cache-Clear?

Häufige Frage: „Wenn ein Redakteur im TYPO3-Backend auf Alle Caches löschen klickt, woher wissen alle Pods davon?"

Kurze Antwort: Der Pod, der den Klick verarbeitet, löscht den zentralen Metadaten-Cache. Alle anderen Pods sehen das beim nächsten get(), weil sie den zentralen Metadaten-Cache abfragen und nicht ihr lokales Filesystem. Kein Pod-zu-Pod-Sync nötig, weil die Metadaten-Wahrheit nie auf einem Pod liegt.

Detailliert

 

Pod A: TYPO3-Backend „Alle Caches löschen" / Editor speichert Page /
       `bin/typo3 cache:flush`
   │
   ▼
ClusterFileBackend::flush()                 auf Pod A
   │
   ▼  delegiert an Metadaten-Cache-Frontend (z. B. cluster_meta)
$metadataCache->flush()
   │
   ▼  TYPO3-Cache-API ruft das konfigurierte Backend
KeyValueBackend / DatabaseBackend / MemcachedBackend → flush()
   │
   ▼  passiert SERVER-SEITIG (Redis FLUSHDB, SQL TRUNCATE, Memcached flush_all)
Alle Pods sehen die leeren Metadaten sofort

 

Beim nächsten get(id) auf irgendeinem Pod:

 

$metadata = $this->metadataCache->get($identifier);   // → null (Cache geflushed)
if ($metadata === null) {
    // cache_miss_total{reason=no-metadata}++
    return null;   // ← Pod konsultiert sein lokales FS gar nicht erst
}

 

Test-Verifikation

Tests/Unit/Deployment/CrossPodFlushTest.php enthält fünf Tests, die das belegen: flush() propagiert ohne Sync sofort zu Pod B; flushByTag() invalidiert nur passende Einträge; lokale Files überleben den Flush als harmlose Waisen; Re-Write nach Flush stellt Konsistenz wieder her; Flush funktioniert für beliebige Pod-Anzahlen (keine Skalierungs-Annahme).

Komplexität — warum es im Cluster schneller ist

Sei C die Menge aller Cache-Einträge und Ct ⊆ C die Teilmenge der Einträge mit Tag t. Wir schreiben n := |C| für die Gesamtanzahl und m := |Ct| für die Anzahl der mit t getaggten Einträge — es gilt m ≤ n. Damit lässt sich der Unterschied sauber benennen: ClusterFileBackend liegt nicht nur bei kleinerem Argument, sondern in einer anderen Komplexitätsklasse, weil es Backend-native Algorithmen nutzt und keinen Pod-Faktor multipliziert.

Sei zusätzlich P die Anzahl der Pods und e ≤ n die Anzahl der TTL-abgelaufenen Einträge.

OperationTYPO3-Core-FileBackendClusterFileBackendSpeedup
flushByTagΘ(n) pro Pod — DirectoryIterator über jede Cache-Datei, 2× file_get_contents pro DateiO(m) — Backend liest Tag-Index direktAndere Komplexitätsklasse plus Tag-Indizes
findIdentifiersByTagΘ(n) pro PodO(m)dito
collectGarbageΘ(n) pro Pod, gesamt Θ(n · P)O(1) aktiv (Redis TTL Auto-Expire) oder O(e) server-seitig (DB)Backend-native plus Cluster-once
flushΘ(n) pro Pod, gesamt Θ(n · P)Θ(n) einmal server-seitigPod-Faktor entfällt, Konstanten ~100–1000× kleiner

Konkretes Beispiel

n = 10 000 Cache-Einträge, davon m = 100 mit Tag site_1, P = 5 Pods.

SetupFile-Readsunlink-CallsRound-Trips
Core-FileBackend bei flushByTag('site_1')2 · n = 20 000m = 100≈ 2 n + m = 20 100 lokale FS-I/O pro Pod
ClusterFileBackend (Redis)002 (SMEMBERS + Pipeline DEL) einmal cluster-weit

Rolling-Deploys mit Version-Skew

Während eines Rolling-Deploys liefern alte und neue Pods gleichzeitig Traffic aus. ClusterFileBackend bewahrt in jedem Skew-Szenario die Korrektheit, aber zwei Fälle ändern das Performance-Profil während des Deploy-Fensters — die sollte man verstehen.

A) Anwendungs-Code mit geändertem Cache-Layout

Wenn das neue Image für denselben Cache-Identifier eine andere Payload-Struktur schreibt (zusätzliche Felder, geänderte serialisierte Klassen, anders aufgebaute Value-Objects) und Sie nicht explizit invalidieren, passiert Folgendes:

  1. Pod-alt schreibt Payload v1 → Metadaten enthalten hashv1.
  2. Pod-neu liest, sieht hashv1, hat lokal keinen Blob → Blob-Miss → TYPO3-Frontend ruft den Rebuild des Callers → Pod-neu schreibt Payload v2 → Metadaten werden mit hashv2 überschrieben.
  3. Pod-alt liest, sieht hashv2, hat lokal keinen Blob → Blob-Miss → baut v1 neu → Metadaten zurück auf hashv1.
  4. Hash-Thrashing für die Dauer des Rolling-Deploys.

Das größere Risiko ist stiller Layout-Drift: kann Pod-neu die Bytes von Pod-alt zwar technisch deserialisieren, das resultierende Objekt ist aber falsch (fehlende Felder, alte Enum-Cases, entfernte Properties), sieht der User stale oder korrupten Content. PHPs unserialize verifiziert die Klassen-Shape jenseits des Klassennamens nicht.

Empfehlung: Cache-Identität an den Deploy koppeln

Damit jedes Release automatisch eine neue BackendVersion bekommt und stale Einträge unerreichbar werden, liest ClusterFileBackend eine Environment-Variable — per Default IMAGE_TAG — und faltet ihren Wert via crc32 in den Payload-Hash. Im Deployment-Manifest:

 

# Helm-Values, Kustomize-Patch oder plain Pod-Spec
env:
  - name: IMAGE_TAG
    value: "{{ .Values.image.tag }}"  # oder $CI_COMMIT_SHA, Release-Semver, ...

 

Pro Cache lässt sich der Variablenname überschreiben, falls die CI-Konvention anders heißt:

 

'options' => [
    'localPath'              => '/app/var/cache/cluster/pages',
    'metadataCacheIdentifier' => 'cluster_meta',
    'namespace'              => ['environment' => 'prod', 'instance' => 'site'],
    'backendVersionEnvVar'   => 'CI_COMMIT_SHA',
],

 

Ist die Variable unset oder leer, fällt das Backend auf die package-interne BackendVersion::current() zurück — sicher für lokale Entwicklung, in Production sollten Sie die Variable aber explizit verdrahten, um deploy-scoped Invalidierung zu bekommen.

Alternative Invalidierungs-Strategien

Bei nicht-brechenden Layout-Änderungen (additiv, vom alten Code ignoriert) kann man das temporäre Thrashing akzeptieren — die Korrektheit bleibt erhalten.

B) PHP-Major/Minor-Version-Wechsel

Der Identity-Hash enthält PHP_MAJOR.PHP_MINOR (Classes/Application/Hash/ComputePayloadHash.php). PHP 8.4 ↔ 8.5 (oder jeder andere Major/Minor-Sprung) erzeugt automatisch divergente Hashes — keine manuelle Aktion nötig. Korrektheit garantiert. Die Kosten sind dasselbe Thrashing wie in (A) für die Dauer des Rollouts. blob_miss_total in Prometheus beobachten; ein anhaltender Spike über das Deploy-Fenster hinaus deutet darauf hin, dass die Version-Skew nicht konvergiert (z. B. ein Pod im alten Image hängen geblieben).

PHP-Patch-Updates (8.5.4 → 8.5.5) invalidieren nicht — nur Major und Minor sind im Hash.

Operative Empfehlung

Häufige Fallstricke

Nächster Schritt

TYPO3 unter Kubernetes betreiben?

Wenn Sie TYPO3 in einem K8s-Cluster ohne RWX-Volume betreiben oder ein bestehendes FileBackend-Setup für Multi-Pod auslegen, hilft cluster-file-backend. Sprechen Sie uns für Architektur-Beratung, Migration oder Plattform-Setup an.

K8s-Setup besprechen

Oder direkt schreiben: kontakt@moselwal.de

Setzen wir ein bei …

Dieses Paket trägt die fileadmin- und Object-Storage-Schicht in TYPO3 Kubernetes — eine der Voraussetzungen für Multi-Pod-Cluster, die unter Open Source & Digitale Souveränität beschrieben sind. In der betreuten Variante: AI-Ready CMS as a Service.