
🌐 Auch auf: English · Français · Español
Deep Dive aus der Reihe rund um die Ansible-Tour. Niveau: fortgeschrittene Linux-Administration. Die Tour erklärt die Rollen; die Deep Dives erklären die Technik darunter – wie sie funktioniert, warum sie hier ist, was genau dieses Setup damit macht, wie man sie im Alltag betreibt und was es sonst noch gegeben hätte.
WordPress und Nextcloud sind PHP-Anwendungen, und in beiden Pods landet jeder dynamische Aufruf in einem PHP-FPM-Worker (Teil 3, Teil 4). Wie viele Worker es gibt, wie lange einer laufen darf und wie viel kompilierten Code sie sich teilen, entscheidet, wie schnell die Seiten sind und was unter Last passiert. Dieser Beitrag erklärt die Mechanik, die hier gewählten Einstellungen, die Messungen dahinter – und zwei Standardwerte, die Nextcloud begrenzten, ohne dass es jemand merkte.
🧭 Wie PHP-FPM und OPcache funktionieren
Master, Pool, Worker
PHP-FPM („FastCGI Process Manager“) ist ein Master-Prozess, der Worker-Prozesse startet und überwacht. Jeder Worker bearbeitet genau einen Aufruf gleichzeitig, von Anfang bis Ende. nginx im selben Pod liefert statische Dateien selbst aus und reicht jeden .php-Aufruf per FastCGI an Port 9000 weiter. Ein Pool ist eine Gruppe von Workern mit eigenen Einstellungen; beide Anwendungen nutzen den einen Pool www aus den offiziellen Images.
Wie viele Worker
pm = static: eine feste Anzahl Worker, die immer laufen.pm = dynamic: Zwischenmin_spare_serversundmax_spare_serversuntätige Worker werden bereitgehalten; neue entstehen bismax_children.pm = ondemand: Worker starten erst, wenn ein Aufruf kommt, und enden nach einer Leerlaufzeit.
pm.max_children ist die echte Grenze der Gleichzeitigkeit: Aufruf Nummer max_children + 1 wartet in der Listen-Queue, bis ein Worker frei wird. pm.max_requests ersetzt einen Worker nach so vielen Aufrufen, was langsames Speicherwachstum durch undichte Erweiterungen begrenzt.
Zwei Arten von Zeitgrenzen
max_execution_time ist PHPs eigene Grenze und zählt unter Linux nur die CPU-Zeit des Skripts – ein Aufruf, der auf die Datenbank oder eine Sperre wartet, verbraucht sie nicht. request_terminate_timeout ist FPMs Grenze für die echte verstrichene Zeit: Läuft sie ab, beendet der Master den Worker. Nur die zweite befreit einen Worker, der hängt.
OPcache: einmal kompilieren
PHP kompiliert jedes Skript zu Bytecode, bevor es läuft. OPcache hält diesen Bytecode in gemeinsamem Speicher, den alle Worker eines Pools zusammen nutzen; eine Datei wird also einmal pro Pod kompiliert statt einmal pro Aufruf. Drei Einstellungen bemessen ihn: memory_consumption (das Segment), interned_strings_buffer (gleiche Zeichenketten nur einmal gespeichert) und max_accelerated_files (die Zahl der Plätze in seiner Hash-Tabelle). Letztere wird intern auf die nächste Zahl aus einer festen Liste von Primzahlen aufgerundet – aus 10.000 werden 16.229, aus 20.000 werden 32.531. Sind die Plätze oder der Speicher voll, werden weitere Dateien einfach nicht gecacht und bei jedem Aufruf neu kompiliert.
validate_timestamps entscheidet, ob OPcache die Änderungszeiten der Dateien prüft (alle revalidate_freq Sekunden). Aus ist schneller, aber dann bleibt eine geänderte Datei alter Bytecode, bis FPM neu startet.
APCu: ein Cache für die Anwendung
APCu ist ein zweites gemeinsames Speichersegment, das die Anwendung selbst mit Daten füllt – Nextcloud legt dort App- und Konfigurations-Lookups als lokalen Cache ab. Wie OPcache gilt es pro Pod, nicht zwischen Pods; alles, was über Prozesse und Pods hinweg konsistent sein muss, gehört nach Redis.
🛡️ Warum es hier ist
- Es ist das, was die offiziellen Images mitbringen.
wordpress:fpmundnextcloud:fpmenthalten beide PHP-FPM; nginx davor ist der dokumentierte Aufbau. - Vorhersehbare Gleichzeitigkeit. Eine feste Obergrenze an Workern, die zum Speicherlimit des Containers passt, ist besser als ein Server, der alles annimmt und dann vom OOM-Killer beendet wird.
- Feine Steuerung. Poolgröße, Zeitgrenzen, Statusseite und Slow-Log lassen sich pro Anwendung setzen, unabhängig vom Webserver.
🔧 Was dieses Repository macht
| Blog | Nextcloud A (größer) | Nextcloud B | |
|---|---|---|---|
| PHP | 8.4 | 8.5 | 8.5 |
pm | dynamic | dynamic | dynamic |
max_children | 12 | 30 | 20 |
| start / min spare / max spare | 4 / 2 / 8 | 8 / 4 / 16 | 6 / 3 / 12 |
max_requests | 500 | 500 | 500 |
request_terminate_timeout | 300 s | 3.600 s | 3.600 s |
| Container-Limit | 1 GiB, 2 Kerne | 6 GiB, 3 Kerne | 5 GiB, 2 Kerne |
| OPcache | 128 MB, 10.000 Dateien, Zeitstempel alle 60 s geprüft | 256 MB, 20.000 Dateien, Zeitstempel aus | gleich |
| APCu | – | 128 MB | 128 MB |
Die Pool-Einstellungen liegen in einer ConfigMap, eingehängt als z-monitoring.conf in php-fpm.d/ – das „z“ sorgt dafür, dass sie zuletzt gelesen wird und über die www.conf des Images gewinnt. Die Nextcloud-Werte kommen aus Host-Variablen mit Standardwerten im Template, sodass sich ein kleiner Testhost und die beiden Produktivserver ein Template teilen.
Der Standardwert, der Nextcloud auf fünf begrenzte
In der ersten Version des Nextcloud-Stacks hatte der Pool keine eigenen Einstellungen. Der Container lief deshalb mit FPMs Standard pm.max_children = 5 – unabhängig von seinem Speicherlimit von 4 GiB. Desktop-Sync-Clients, Mobil-Apps und Collaboras WOPI-Rückrufe belegen alle Worker; der sechste gleichzeitige PHP-Aufruf musste warten. Behoben wurde das im Juli 2026 mit einem Pool von 12, später 20; im Oktober zeigte Prometheus Spitzen von 17 aktiven Workern auf dem größeren Server, die ursprüngliche Grenze von fünf war also sehr real. Die Optimierungsrunde vom 16. Oktober 2026 hob die Pools auf 30 und 20. Die Faustregel: Speicher pro Worker mal max_children, plus OPcache und APCu, muss deutlich unter dem Container-Limit bleiben.
Der Bytecode-Cache, der zu klein war
Das Nextcloud-Image setzt max_accelerated_files = 10000, intern 16.229 Plätze. Nextcloud mit seinen Apps hat auf dem größeren Server 15.486 PHP-Dateien und auf dem kleineren 17.811. Auf dem kleineren Server passten also über 1.500 Dateien nie in den Cache und wurden bei jedem Aufruf, der sie brauchte, neu kompiliert. Seit dem 16. Oktober laufen beide Server mit 20.000 (32.531 Plätze) und 256 MB. Der Blog ist mit 4.452 PHP-Dateien weit von seiner Grenze entfernt.
Zeitstempel aus – und was das kostet
Bei Nextcloud gilt validate_timestamps = 0: kein stat() pro Datei und Aufruf. Der Preis: Eine geänderte Datei wird erst bemerkt, wenn der Pod neu startet. Image-Updates starten den Pod ohnehin neu, aber occ upgrade und occ app:update überschreiben PHP-Dateien an Ort und Stelle. Das Update-Playbook rollt den Nextcloud-Pod deshalb danach ein zweites Mal aus. Für diesen Artikel stellte sich heraus, dass dem optionalen App-Update im Haupt-Playbook dieser zweite Neustart fehlte – aktualisierte Apps wären als ihr alter Code weitergelaufen. Behoben. Der Blog lässt die Zeitstempel mit 60 Sekunden Intervall an, weil WordPress Plugins über seine eigene Admin-Seite an Ort und Stelle aktualisiert.
Harte Grenzen und das Slow-Log
[www]
pm.status_path = /fpm-status
slowlog = /proc/1/fd/2
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 0
request_terminate_timeout = 3600s ; blog: 300srequest_terminate_timeoutwar vor dem 16. Oktober ebenfalls nicht gesetzt: Ein Aufruf, der an einer Sperre oder einem externen Speicher hing, hielt seinen Worker bis zum Neustart des Pods. 3.600 s bei Nextcloud, weil ein gestreamter Ordner-Download berechtigt viele Minuten laufen kann; 300 s beim Blog.- Das Slow-Log geht nach stderr des Containers und damit ins Pod-Log. Ein Stacktrace bräuchte
ptrace, das ein Container ohne Capabilities nicht hat; Tiefe 0 protokolliert nur die Zeile „executing too slowly“. FPM versucht trotzdem anzuhängen und schreibt pro langsamem Aufruf ein harmlosesfailed to ptrace– akzeptiert, weilSYS_PTRACEden Container schwächen würde. - Die Statusseite ist über nginx gar nicht erreichbar; ein
php-fpm_exporter-Sidecar im selben Pod liest sie per FastCGI und liefert die Zahlen an Prometheus (Grafana-Dashboard 4912).
📊 Gemessen
| Letzte 7 Tage | Blog | Nextcloud A | Nextcloud B |
|---|---|---|---|
| PHP-Aufrufe pro Tag | ~21.700 | ~23.600 | ~16.200 |
| Spitze aktive Worker | 8 von 12 | 13 von 30 | 9 von 20 |
max_children erreicht | 0 | 0 | 0 |
| Langsame Aufrufe (> 5 s) | 0 | 0 | 0 |
| Worker-Speicher (RSS / eigener Anteil PSS) | ~78 / ~16 MB | 108–157 / 14–49 MB | 71–116 / 8–27 MB |
| Container-Speicher Spitze | 538 MiB von 1 GiB | 763 MiB von 6 GiB | 277 MiB von 5 GiB |
| Gedrosselte CPU-Perioden | 2,1 % | 2,2 % | 0,2 % |
Drei Lesarten. Erstens überzeichnet der RSS eines Workers seine Kosten: Er zählt die gemeinsamen OPcache- und APCu-Seiten in jedem Worker mit. PSS teilt gemeinsame Seiten auf die Prozesse auf, die sie nutzen, und ist die ehrliche Zahl – 16 MB pro Blog-Worker, nicht 78. Zweitens sind die Nextcloud-Pools jetzt großzügig: Die Spitze nutzte weniger als die Hälfte, und die Container-Limits liegen weit über den Spitzen. Drittens gab es beim Blog einen Ausreißer: eine einzige Minute am 16. Oktober mit 8 aktiven Workern und 30 wartenden Aufrufen in der Queue – ein Schwall von rund 40 gleichzeitigen Aufrufen. Von jenem Tag ist kein Log erhalten (die Log-Sammlung begann drei Tage später), die Ursache ist also unbekannt. Es war eine Minute in einer Woche, und seither blieb die Queue bei null.
🧰 Betrieb im Alltag
# FPM status as the exporter sees it (it talks FastCGI to port 9000 itself)
k3s kubectl -n nextcloud exec deploy/nextcloud -c nginx -- \
curl -s http://127.0.0.1:9253/metrics | grep -E '^phpfpm_(active|idle|listen_queue|max_children)'
# slow requests
k3s kubectl -n nextcloud logs deploy/nextcloud -c nextcloud-fpm | grep -i 'executing too slow'
# memory per worker, honestly (PSS, in MB)
for p in $(pgrep -f '^php-fpm: pool www'); do
awk '/^Pss:/{s+=$2} END{printf "%d ", s/1024}' /proc/$p/smaps_rollup; done; echo
# new PHP code after an in-place update (Nextcloud)
k3s kubectl -n nextcloud rollout restart deployment/nextcloud- Worker bemessen. PSS unter Last messen, mit dem geplanten
max_childrenmultiplizieren, OPcache und APCu dazurechnen, mit dem Container-Limit vergleichen. Dann „max_children reached“ und die Listen-Queue in Grafana beobachten; bleiben beide bei null, ist der Pool groß genug. - OPcache leeren.
opcache_reset()auf der Kommandozeile leert den eigenen Cache der CLI, nicht den von FPM. Für FPM ist der verlässliche Weg ein Neustart – unter Kubernetes ein Rollout. - Eine hängende Seite. Die Metriken des Exporters pro Prozess zeigen, wie lange jeder Worker schon beschäftigt ist; zusammen mit dem Slow-Log geht das meist schneller als jede Suche in Access-Logs.
🆘 Spickzettel: Symptom → Befehl
Die Kurzfassung dieses Artikels für den Moment, in dem etwas nicht stimmt. Auf dem Host als root ausführen; <…> sind Platzhalter.
| Symptom | Befehl |
|---|---|
| Alle Worker belegt, Anfragen warten | k3s kubectl -n nextcloud exec deploy/nextcloud -c nginx -- \ |
| „server reached pm.max_children“ | k3s kubectl -n nextcloud logs deploy/nextcloud -c nextcloud-fpm | grep max_children |
| Welche Anfragen sind langsam? | k3s kubectl -n nextcloud logs deploy/nextcloud -c nextcloud-fpm | grep -i 'executing too slow' |
| 502 Bad Gateway | k3s kubectl -n nextcloud logs deploy/nextcloud -c nginx --tail=30 |
| Wie viel Speicher braucht ein Worker wirklich? | for p in $(pgrep -f '^php-fpm: pool www'); do |
| Nach einem Update läuft noch alter Code | k3s kubectl -n nextcloud rollout restart deployment/nextcloud # OPcache: validate_timestamps=0 |
| Dasselbe auf dem Blog | k3s kubectl -n wordpress logs deploy/wordpress -c wordpress-fpm --tail=30 |
🔭 Wie es weitergeht
- PHP 8.5 für den Blog, sobald das offizielle WordPress-Image es empfiehlt; Nextcloud läuft schon mit 8.5.
- JIT ist aus. Für I/O-lastige Webanwendungen wie diese bringt er wenig; er wäre ein Experiment mit Messungen davor und danach, kein Standard.
- Ein Alarm auf die Listen-Queue – erledigt am 21. Oktober 2026: Er mailt, wenn Anfragen fünf Minuten lang auf einen freien Worker warten, was in den 14 gemessenen Tagen nie vorkam.
⚖️ Alternativen – und warum PHP-FPM
| Option | Stärke | Warum nicht hier |
|---|---|---|
| Apache mit mod_php | Einfachster Aufbau, eine Prozessart | PHP steckt in jedem Webserver-Prozess, auch in denen, die statische Dateien ausliefern; keine eigene Pool-Steuerung. |
| FrankenPHP / RoadRunner | Langlebige Worker, die Anwendung bleibt im Speicher | Braucht eine dafür gebaute Anwendung; WordPress und Nextcloud erwarten pro Aufruf einen frischen Zustand. |
| Swoole | Asynchrones PHP | Nicht kompatibel mit diesen Anwendungen. |
| PHP-FPM + nginx | Was die Images mitbringen, feine Steuerung, gut verstanden | Gewählt. |
Nichts in diesem Beitrag ist exotisch. Die zwei echten Gewinne – fünfundzwanzig Worker mehr und ein Cache, in den der Code passt – kamen daher, nachzusehen, was die Standardwerte eigentlich waren, nicht aus cleverer Optimierung. Standardwerte sind die Vermutung von jemand anderem über die eigene Last; bei PHP-FPM lagen zwei davon für Nextcloud um den Faktor vier daneben.


