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
- Kein RWX-Volume zwischen Pods erforderlich
- Zentrale Cache-Gültigkeit über die TYPO3-Cache-API
- Deterministische Re-Materialisierung über sha256-Hash-Validierung
- Tag-basierte Invalidierung clusterweit (via TYPO3
TaggableBackendInterface) - Garbage Collection über CLI (
clusterfilebackend:gc), delegiert ans Metadata-Cache-Backend - Deployment-Time-Warmup über CLI (
clusterfilebackend:warmup) plus Event-Listener auf TYPO3sCacheWarmupEvent
Was es nicht ist
- Kein Ersatz für TYPO3 FAL, fileadmin, den TYPO3-Core-Code-Cache (
var/cache/code/corebleibt im Container-Image), keinen Session-Store, keinen generischen Blob-Store, kein Distributed Filesystem - Bringt kein eigenes Redis/Valkey-Wissen mit — wer Redis als Cluster-Storage will, installiert ein TYPO3-Cache-Backend dafür (z. B. den
KeyValueBackendausmoselwal/keyvalue-store) und verweistClusterFileBackendpermetadataCacheIdentifierdarauf
Voraussetzungen
- TYPO3 14.3+ (Composer-Mode-only, kein
ext_emconf.php, kein Classic-Mode) - PHP 8.5+
- Composer-Paket
moselwal/cluster-file-backendab^1.0.1, Extension-Keycluster_file_backend, NamespaceMoselwal\Typo3ClusterCache\ - Lizenz MIT
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
- Composer-Installation:
composer require moselwal/cluster-file-backend:^1.0.1 - 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 Performancemoselwal/keyvalue-storeinstallieren und dessenKeyValueBackendnutzen. - TYPO3-Cache-Frontend (Konvention:
cluster_meta) für die Metadaten registrieren. - Die dateibasierten TYPO3-Caches (
pages,pagesection,rootline,imagesizes,assets,hash) aufClusterFileBackendumstellen und permetadataCacheIdentifieraufcluster_metaverweisen. - Pod-lokales
emptyDirunter/app/var/cache/cluster/(oder dem konfiguriertenlocalPath) mounten.
Was das Paket mitliefert
| Artefakt | Pfad | Zweck |
|---|---|---|
| Default-Config (ohne Extra-Deps) | Configuration/Example/cache-configurations.example.php | Datenbank-basierte Metadaten plus Cluster-File-Caches — läuft auf jeder TYPO3-Installation |
| Redis/Valkey-Config (optional) | Configuration/Example/cache-configurations-redis.example.php | Performance-Variante mit moselwal/keyvalue-store |
| JSON-Schema | Configuration/Backend/ClusterFileBackend.options.schema.json | Wird beim Backend-Konstruktor validiert — fehlerhafte Konfiguration führt zu InvalidCacheException mit Feldname |
| CLI-Kommandos | Configuration/Commands.php | clusterfilebackend:gc, clusterfilebackend:warmup |
| Event-Listener | Configuration/Services.yaml | Hängt sich in TYPO3s CacheWarmupEvent — bin/typo3 cache:warmup triggert die Cluster-Warmup mit |
| DI-Bindings | Configuration/Services.yaml | Auto-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().
| Optionsfeld | Default | Bedeutung |
|---|---|---|
compression | zstd | zstd | gzip | none |
serializer | igbinary | igbinary | php |
defaultLifetimeSeconds | 3600 | TTL wenn der Caller null übergibt |
maxPayloadBytes | 10485760 (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.
| Operation | TYPO3-Core-FileBackend | ClusterFileBackend | Speedup |
|---|---|---|---|
flushByTag | Θ(n) pro Pod — DirectoryIterator über jede Cache-Datei, 2× file_get_contents pro Datei | O(m) — Backend liest Tag-Index direkt | Andere Komplexitätsklasse plus Tag-Indizes |
findIdentifiersByTag | Θ(n) pro Pod | O(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-seitig | Pod-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.
| Setup | File-Reads | unlink-Calls | Round-Trips |
|---|---|---|---|
Core-FileBackend bei flushByTag('site_1') | 2 · n = 20 000 | m = 100 | ≈ 2 n + m = 20 100 lokale FS-I/O pro Pod |
| ClusterFileBackend (Redis) | 0 | 0 | 2 (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:
- Pod-alt schreibt Payload v1 → Metadaten enthalten hashv1.
- 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.
- Pod-alt liest, sieht hashv2, hat lokal keinen Blob → Blob-Miss → baut v1 neu → Metadaten zurück auf hashv1.
- 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
- Pre-Flush via
clusterfilebackend:warmupin der Deploy-Pipeline — dränt stale Einträge ab, bevor das neue Image Traffic annimmt. - Cache-Identifier umbenennen (z. B.
pages→pages_v2in dencacheConfigurations). Schwerer Hammer, nur für größere Schema-Umbauten.
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
- Patch-Updates (igbinary-Patch, PHP-Patch, App-Bugfix ohne Cache-Layout-Wechsel): normaler Rolling-Deploy, keine Extra-Schritte.
- Minor- und Major-Updates (PHP-Minor-Bump, BackendVersion-Bump, Cache-Layout-Änderung): Rolling-Deploy bleibt sicher, aber mit erwartetem Blob-Miss-Spike. Für Zero-Degradation-Deploys eine
Recreate-Strategie oder einen Pre-Flush via Warmup-Kommando fahren.
Häufige Fallstricke
localPathmuss schreibbar sein. Bei read-only/app-Image einemptyDirodertmpfsan diesem Pfad mounten.- Identisches Container-Image für alle Pods. Unterschiedliche PHP- oder igbinary-Versionen führen zu abweichenden Hashes → permanente Blob-Misses. Major-Versionen reichen — Patch-Versionen sind seit v1.0.1 nicht mehr Teil des Hashes.
IMAGE_TAG(oder Ihr Äquivalent) in Production verdrahten. Ohne diese Variable nutzt das Backend eine package-interne Versions-Konstante, die sich über Deploys NICHT ändert — brechende Cache-Layout-Änderungen können dann stillschweigend stale oder korrupten Content ausliefern. Siehe „Rolling-Deploys mit Version-Skew".metadataCacheIdentifiermuss vor jedem Cache registriert sein, derClusterFileBackendnutzt. TYPO3 lädtcacheConfigurationsin Array-Insertion-Order — alsocluster_metazuerst definieren.- Nur Composer-Mode. Kein
ext_emconf.php, kein Classic-Mode.
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.
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.
