Kai Ole Hartwig
8 Min. Lesezeit
Niedrig

TYPO3-Pods in zwölf Sekunden statt einer Minute

Für alle, die TYPO3 auf Kubernetes betreiben und deren Pods beim Start länger brauchen, als ein Besucher warten möchte. Die Feldnotiz zum Image-Volume hat den Download des Codes aus dem Kaltstart entfernt. Geblieben war ein Setup, das in jedem Pod lief. Am Ende desselben Tages war ein neuer Pod nach zwölf statt nach 54 Sekunden bereit. Dieser Beitrag zeigt, wie das Setup aus dem Pod verschwand, wo dabei zweimal etwas fehlte und was der Build jetzt zusätzlich mitliefert.

01 — Wohin die 54 Sekunden gingen

Ein frischer Pod durchlief bisher eine feste Kette von Init-Containern. Zwei prüften Signatur und SBOM, einer lud den Code mit ORAS, der letzte führte das TYPO3-Setup aus: extension:setup, die Schema-Migration, cache:flush und cache:warmup. Beim Pilot-Mandanten kamen so 54 Sekunden zusammen. 13 davon entfielen auf den Download, 29 auf das Setup.

Den Download hat das Image-Volume erledigt. Beim Setup lohnte sich ein zweiter Blick: Einzeln gemessen brauchten seine Schritte nur gut zwölf Sekunden. Der Rest war hausgemacht. Das Skript rief extension:setup zweimal auf. Danach leerte cache:flush alles, was die Schritte davor gerade aufgebaut hatten, und die Aufwärmung begann wieder bei null.

Schwerer wog etwas anderes. Der Flush leerte auch die gemeinsamen Seiten-Caches aller Pods des Mandanten – bei jedem Start, bei jedem Hochskalieren, bei jedem Neustart. Ein Pod, der wegen Last dazukam, machte den anderen also zuerst einmal mehr Arbeit.

02 — Das Setup zieht in einen Hook

Ein Schema muss einmal je Release migriert werden, nicht einmal je Pod. Bisher ging das nicht anders: Der Code lag nur im emptyDir des jeweiligen Pods, ein Argo-Hook hätte schlicht keinen Code gehabt. Mit dem Image-Volume hängt ein Hook-Pod dasselbe Image ein wie die App-Pods, festgelegt auf denselben Digest.

Daraus wurden zwei Hooks und ein schlankerer Pod:

Der dritte Punkt ersetzt einen Umweg. Bisher startete nach jedem Sync ein Hook die App neu. Der Grund: Ein Flush unter einem laufenden FrankenPHP-Worker lässt die geladenen DI-Klassen mit frisch erzeugten kollidieren. Ein Flush aus einem fremden Pod leert dagegen nur die gemeinsamen Cache-Einträge und lässt die Dateien der Worker in Ruhe. Das zweite Rollout nach jedem Sync entfällt damit.

03 — Erste Falle: die Netzwerkregel kam zu spät

Der erste Sync mit den Hooks scheiterte schon im PreSync. Der Hook-Pod wartete zwei Minuten auf die Datenbank und gab dann auf:

 

wait-db: db:3306 not reachable after 120s

 

Im Namespace dürfen die Komponenten nur so miteinander reden, wie NetworkPolicies es erlauben. Die Freigabe für den neuen Hook stand im selben Chart wie der Hook selbst. Argo wendet NetworkPolicies aber erst in der Sync-Phase an, also nach dem PreSync. Ein scheiternder PreSync erreicht diese Phase nie – von allein hätte sich der Zustand nicht mehr aufgelöst. Besucher merkten davon nichts, denn vor dem PreSync ändert sich am Deployment nichts.

Die Lösung folgt einer Regel, die die Plattform schon bei den Pod-Zertifikaten befolgt: erst freigeben, dann umschalten. Die Netzwerkfreigaben für die Hooks entstehen jetzt, sobald der Code als Image-Volume kommt. Sie stehen damit einen Sync bereit, bevor ein Mandant auf die Hooks umgestellt wird.

04 — Zweite Falle: Seiten ohne CSS

Der zweite Versuch lief scheinbar sauber. Der Hook brauchte 15 Sekunden, ein Pod war nach 23 Sekunden bereit, jede Überwachung meldete grün. Knapp eine Stunde später fiel auf, dass auf mehreren Websites die Gestaltung fehlte. Die Seiten antworteten mit 200, ihre Stylesheets unter /_assets/ mit 404.

Das Setup hatte nebenbei eine zweite Aufgabe erledigt, die nirgends als solche beschrieben war. extension:setup veröffentlicht die öffentlichen Ressourcen aller Extensions nach public/_assets. Im Pod lag dieses Verzeichnis auf einem beschreibbaren emptyDir. Lief das Setup im Hook, landeten die Assets im Volume des Hook-Pods, und der App-Pod sah ein leeres Verzeichnis. Im Image waren sie nie gewesen: Der Composer-Hook für asset:publish war in der CI abgeschaltet – mit der Begründung, dass der Pod das beim Start erledigt.

Bemerkt hat es zuerst ein Mensch, nicht die Technik. Alle Überwachungen prüften, ob eine Seite mit 200 antwortet, und eine Seite ohne CSS tut genau das. Seitdem holt jede Prüfung zusätzlich eine Datei ab, die die Seite verlinkt. Zurückgerollt war der Fehler in wenigen Minuten, weil das Image-Volume bleiben konnte und nur das Setup zurück in den Pod zog.

05 — Assets und Caches aus dem Build

Die Assets gehören ins Image. Seit TYPO3 14.2 gibt es dafür den Befehl asset:publish, und er braucht keine Datenbank. Die Verzeichnisse unter _assets/ heißen nach dem MD5-Hash des Pfads relativ zum Projektverzeichnis, ihre Einträge sind relative Symlinks auf vendor/*/Resources/Public. Ein Build unter einem ganz anderen Pfad erzeugt deshalb genau die Namen, die TYPO3 später unter /app in die Seiten schreibt. Der App-Pod legt seitdem kein beschreibbares Verzeichnis mehr über public/_assets. Bringt ein Image dort nichts mit, verweigert ein Init-Container den Start.

Die Caches waren schwieriger. Ein volles cache:warmup dauerte im Pod sieben bis elf Sekunden, ein TYPO3-Bootstrap mit vorhandenen Code-Caches nur 0,3 Sekunden. Die gemeinsamen Laufzeit-Caches wie Übersetzungen liegen ohnehin warm in Valkey, sobald ein Mandant läuft. Es genügt also, im Build die Code-Caches für DI-Container, TCA, Routen und Middleware zu erzeugen. Dabei waren drei Dinge zu beachten:

Ob das trägt, zeigte ein Vergleich. Build und laufender Pod kamen auf dieselbe Cache-Kennung, und ein kopierter Cache blieb nach dem ersten Bootstrap Byte für Byte unverändert. Scheitert das Aufwärmen im Build, geht das Image ohne Caches hinaus, und der Pod wärmt wie früher selbst.

06 — Was übrig bleibt

Ein neuer Pod ist jetzt nach zwölf Sekunden bereit. Drei davon kostet die Signaturprüfung, etwa drei der Containerstart samt Boot des FrankenPHP-Workers, der Rest geht auf Scheduling und Einhängen. Der Init-Container, der die Caches aus dem Image kopiert, braucht weniger als eine Sekunde.

StandBereit nach
Code per ORAS, Setup in jedem Pod54 s
Code als Image-Volume42 s
Setup als Hook, Assets aus dem Build22 s
Code-Caches aus dem Build12 s

Auch der Ablauf eines Syncs ist einfacher geworden. Statt zwei oder drei Rollouts gibt es eines, das Setup läuft genau einmal, und kein neuer Pod leert mehr die Caches der anderen. Sylius folgt, sobald sein Setup keine Dateien mehr nach /app schreibt.

Zwölf Sekunden sind zugleich die Voraussetzung für den nächsten Schritt. Ein Backend, das außerhalb der Arbeitszeit auf null Pods steht, muss schnell aufwachen – eine Redakteurin soll beim Login nicht lange warten. Mit einer Minute Kaltstart war das keine Option. Mit zwölf Sekunden wird es eine.

Häufige Fragen

Warum nicht gleich alle Caches im Build wärmen?+

Weil nicht alle Caches zum Code gehören. Übersetzungen, Seiten und Rootlines sind gemeinsame Laufzeit-Caches, die alle Pods eines Mandanten in Valkey teilen und die zur Laufzeit ohnehin schon warm sind. Die Site-Konfiguration hängt an Umgebungsvariablen je Mandant. Ins Image gehört nur, was allein aus Code und Konfiguration entsteht.

Was passiert, wenn der PreSync-Hook scheitert?+

Argo hält den Sync an, bevor sich das Deployment ändert. Die laufenden Pods bleiben, wie sie sind, und liefern weiter aus. Der Hook läuft deshalb mit set -e und ohne die Toleranz des alten Skripts, das Fehler nach zwei Minuten Wiederholung einfach hinnahm.

Muss jede Extension dabei mitspielen?+

Jede Extension, die zur Laufzeit in den Code-Baum schreibt, fällt mit schreibgeschütztem Code auf. Bei Content Blocks waren es Links unter vendor/, bei einem Mandanten zwei neue Standardwerte, die extension:setup in die settings.php schreiben wollte. Beides gehört in den Build oder ins Repository, und ein Prüf-Container meldet es, bevor ein Pod Anfragen annimmt.

Fazit

Ein Setup, das in jedem Pod läuft, kostet mehr als seine eigene Laufzeit. Es erledigt nebenbei Dinge, auf die sich der Rest still verlässt. Hier waren es die öffentlichen Assets, und aufgefallen sind sie erst in der Stunde, in der sie fehlten. Geblieben ist: Setup einmal je Sync, Assets und Code-Caches aus dem Build, und ein Pod, der nach zwölf Sekunden arbeitet.

Die zweite Lehre betrifft die Überwachung. Eine Prüfung, die nur fragt, ob eine Seite antwortet, hätte auch diesen Fehler für gesund gehalten. Heute fragt sie zusätzlich, ob ankommt, was die Seite braucht.

Wie lange braucht ein neuer Pod deiner TYPO3-Instanz?

Ich sehe mir an, was beim Start deiner TYPO3-Pods passiert: welche Schritte in jeden Pod gehören, welche einmal je Release reichen und was der Build schon mitbringen kann.

Termin buchen →

Über den Autor

Foto von Kai Ole Hartwig.

Kai Ole Hartwig

Freiberuflicher DevSecOps-Berater · OnlyOle Consulting

Programmiert seit 2002 – autodidaktisch gelernt, 2012 mit KO-Web selbständig gemacht. Über 100 Projekte, Fokus auf Security, Performance, Automatisierung und Qualität. Heute freiberuflich: DevSecOps-Beratung, Schulungen und Softwareentwicklung.