pm.max_children für PHP-FPM richtig berechnen statt raten

Veröffentlicht am 20 Min. Lesezeit

Die Meldung server reached pm.max_children heißt nicht, dass Sie den Wert verdoppeln sollen. Messen, rechnen, Betriebsart wählen und danach belegen, dass der Wert wirklich passt.

Im Protokoll von PHP-FPM steht irgendwann diese Zeile, und sie liefert den Rat gleich mit:

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it

Der Rat ist nicht falsch, nur unvollständig. pm.max_children ist die einzige Stellschraube in PHP-FPM, bei der ein zu hoher Wert gefährlicher ist als ein zu niedriger. Zu niedrig kostet Wartezeit und im Extremfall einen 502. Zu hoch kostet den Arbeitsspeicher des gesamten Servers, und dann sucht sich der Kernel selbst aus, welchen Prozess er beendet. Erfahrungsgemäß ist das nicht PHP, sondern die Datenbank.

Diese Anleitung zeigt den Weg vom Raten zum Rechnen: den Speicherbedarf eines Arbeitsprozesses messen, das Budget bestimmen, die Betriebsart wählen und anschließend belegen, dass der Wert stimmt.

Alle Angaben beziehen sich auf Debian 13 (trixie), Debian 12 (bookworm), Ubuntu 24.04 LTS und Ubuntu 22.04 LTS. Die Befehle sind für den Betrieb als root geschrieben, als normaler Benutzer stellen Sie sudo voran. Ersetzen Sie überall die PHP-Versionsnummer durch die Ihres Systems.

SystemPHPDienstPool-DateiFPM-Protokoll
Debian 13 (trixie)8.4php8.4-fpm/etc/php/8.4/fpm/pool.d/www.conf/var/log/php8.4-fpm.log
Debian 12 (bookworm)8.2php8.2-fpm/etc/php/8.2/fpm/pool.d/www.conf/var/log/php8.2-fpm.log
Ubuntu 24.04 LTS8.3php8.3-fpm/etc/php/8.3/fpm/pool.d/www.conf/var/log/php8.3-fpm.log
Ubuntu 22.04 LTS8.1php8.1-fpm/etc/php/8.1/fpm/pool.d/www.conf/var/log/php8.1-fpm.log

Welche Versionen installiert sind, zeigt ein Blick ins Verzeichnis. Auf Servern nach einem Distributionsupgrade sind es oft zwei:

ls /etc/php/
ls /etc/php/*/fpm/pool.d/

Was pm.max_children wirklich begrenzt

Der Wert begrenzt nicht Besucher und nicht Verbindungen, sondern die Anzahl der PHP-Anfragen, die im selben Augenblick ausgeführt werden. Eine Anfrage von 80 Millisekunden belegt einen Arbeitsprozess für 80 Millisekunden und gibt ihn dann frei. Daraus folgt fast alles Weitere:

  • Viel Verkehr braucht wenige Prozesse, solange die Skripte schnell sind. 40 Anfragen pro Sekunde mit je 80 Millisekunden ergeben gut drei gleichzeitige Anfragen, nicht 40.
  • Eine langsame Stelle kippt die Rechnung. Ein Aufruf an eine fremde Programmierschnittstelle ohne eigene Frist hält einen Prozess fünf Sekunden besetzt, obwohl dieser nichts tut. Fünf solcher Aufrufe pro Sekunde binden dauerhaft 25 Prozesse.
  • Wartende Verbindungen belegen keinen Prozess. Eine Keep-Alive-Verbindung zu nginx kostet einen Dateideskriptor, aber keinen Arbeitsprozess.

Sind alle Prozesse belegt, warten neue Anfragen in der Annahmewarteschlange des Sockets. Deren Größe steht in der Pool-Datei unter listen.backlog, ab Werk 511, und der Kernel begrenzt sie zusätzlich über net.core.somaxconn, das auf allen vier Systemen bei 4096 liegt:

sysctl net.core.somaxconn

Solange die Warteschlange reicht, merkt der Besucher nur längere Ladezeiten. Ist auch sie voll, weist der Kernel die Verbindung ab, nginx protokolliert 11: Resource temporarily unavailable, und beim Besucher kommt ein 502 an. Zur Abgrenzung von den übrigen Ursachen: nginx 502 Bad Gateway beheben.

Ein Detail, das viel Verwirrung stiftet: Die Warnung erscheint einmal je Sättigungsphase, nicht je betroffener Anfrage. FPM setzt intern eine Merkmarke und löscht sie erst, wenn wieder ein Prozess frei ist. Eine einzelne Zeile kann für eine ganze Stoßzeit stehen. Wer Zeilen zählt, unterschätzt das Problem. Der ehrliche Messwert ist der Zähler max children reached auf der Statusseite, die weiter unten aufgerufen wird.

Der Rückweg, bevor Sie etwas ändern

Zwei Dinge können hier schiefgehen, und beide treffen den laufenden Betrieb.

Erstens: FPM startet nicht mehr. Ein systemctl reload schickt dem Hauptprozess das Signal USR2, woraufhin dieser sich mit der neuen Konfiguration selbst neu startet. Ist sie ungültig, bricht die Initialisierung ab und der Hauptprozess beendet sich. Aus einer laufenden Instanz wird also eine tote. Deshalb ausnahmslos erst prüfen, dann laden:

php-fpm8.2 -t

Bei Erfolg endet die Ausgabe mit test is successful.

Zweitens: Der Wert ist zu hoch. Dann ist der Server nicht sofort kaputt, sondern erst bei der nächsten Lastspitze, und zwar so gründlich, dass auch eine SSH-Anmeldung hängt oder gar nicht zustande kommt. Dafür brauchen Sie einen zweiten Weg auf den Server.

Bei den KVM-Rootservern und Dedicated Servern von KernelHost ist das die VNC-Konsole im Kundenbereich. Sie hängt an der Virtualisierungsschicht beziehungsweise am Anschluss selbst und ist auch dann erreichbar, wenn der SSH-Dienst wegen Speichermangels nicht mehr antwortet. Melden Sie sich vorher einmal darüber an. Ein Rettungsweg, den man im Notfall zum ersten Mal ausprobiert, ist keiner.

Legen Sie außerdem eine Kopie der Pool-Datei an, mit Zeitstempel, damit ein zweiter Versuch die erste Kopie nicht überschreibt:

mkdir -p /root/backups
cp -a /etc/php/8.2/fpm/pool.d/www.conf /root/backups/www.conf.$(date +%F-%H%M)
ls -l /root/backups/

Der Weg zurück ist dann drei Zeilen lang:

cp -a /root/backups/www.conf.2026-09-03-1015 /etc/php/8.2/fpm/pool.d/www.conf
php-fpm8.2 -t
systemctl reload php8.2-fpm

Ein Geländer für den Fall, dass Sie sich verrechnen

Ein Speicherlimit auf der systemd-Einheit sorgt dafür, dass eine Fehleinschätzung einen PHP-Arbeitsprozess trifft und nicht die Datenbank: Der Kernel beendet dann innerhalb der Kontrollgruppe von FPM, statt sich systemweit den größten Prozess zu suchen.

mkdir -p /etc/systemd/system/php8.2-fpm.service.d
cat > /etc/systemd/system/php8.2-fpm.service.d/speicher.conf <<'EOF'
[Service]
MemoryHigh=1500M
MemoryMax=2G
EOF
systemctl daemon-reload
systemctl restart php8.2-fpm

Prüfen, ob es angekommen ist:

systemctl show php8.2-fpm -p MemoryHigh -p MemoryMax

MemoryHigh bremst und räumt auf, MemoryMax ist die harte Grenze. Alle vier Systeme nutzen die vereinheitlichte Kontrollgruppen-Hierarchie, die Werte wirken also unmittelbar. Das ist ein Geländer, kein Ersatz für die Rechnung: Wird die Grenze erreicht, sieht der betroffene Besucher weiterhin einen Fehler, nur eben nicht der ganze Server. Zum sauberen Ablegen solcher Ergänzungsdateien: systemd-Service erstellen.

Und die dritte Regel, die keinen Befehl braucht: eine Änderung nach der anderen. Wer die Betriebsart, pm.max_children und die Spare-Werte gleichzeitig anfasst, weiß hinterher nicht, was gewirkt hat.

Bestandsaufnahme: welcher Pool gilt

Jeder Pool hat sein eigenes pm.max_children, und für den Speicher zählt die Summe über alle Pools:

grep -n "^pm" /etc/php/*/fpm/pool.d/*.conf

Im Auslieferungszustand steht auf allen vier Systemen dasselbe:

pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3

Das sind keine Empfehlungen, sondern Platzhalter, mit denen PHP auch auf einer sehr kleinen Testmaschine startet. Dazu kommt eine globale Obergrenze über alle Pools hinweg:

grep -n "^process.max" /etc/php/*/fpm/php-fpm.conf

Bleibt die Ausgabe leer, ist die Zeile auskommentiert und es gilt der Standardwert 0, also keine globale Begrenzung. Auf Servern mit vielen Pools verhindert genau diese Zeile, dass die Summe den Speicher sprengt, sobald mehrere Pools gleichzeitig unter Last stehen.

Maßgeblich ist am Ende nicht die Datei, sondern das, was FPM daraus macht. Der Schalter -tt gibt die Konfiguration vollständig aufgelöst aus, samt aller Vorgabewerte, die in keiner Datei stehen:

php-fpm8.2 -tt 2>&1 | grep -E "^\[|pm |pm\.|listen ="

Diese Ausgabe ist später auch der Nachweis, dass eine Änderung angekommen ist.

Den Speicherbedarf eines Arbeitsprozesses messen

Hier werden die meisten Anleitungen ungenau. Drei Zahlen werden regelmäßig verwechselt:

  • memory_limit, ab Werk 128M im FPM-Betrieb: eine Obergrenze je Anfrage, kein Verbrauch. Ein Skript, das 12 MB braucht, braucht auch mit 512M nur 12 MB.
  • RSS: alles, was gerade im Arbeitsspeicher liegt, einschließlich des gemeinsam genutzten Opcache und der geteilten Bibliotheken. Wer RSS über 20 Prozesse aufaddiert, zählt denselben Opcache zwanzigmal.
  • PSS: gemeinsam genutzte Seiten werden durch die Zahl ihrer Nutzer geteilt. Die einzige der drei Zahlen, die sich sinnvoll aufsummieren lässt.

Für die Rechnung brauchen Sie PSS. Der Kernel liefert ihn fertig zusammengefasst in /proc/<pid>/smaps_rollup, lesbar als root:

pgrep -f "php-fpm: pool www" | while read -r p; do
  awk '/^Pss:/ {print $2}' "/proc/$p/smaps_rollup"
done | awk '{s+=$1; n++} END {
  if (!n) { print "keine Arbeitsprozesse gefunden"; exit }
  printf "Prozesse: %d   Summe: %.0f MiB   Mittel: %.1f MiB\n", n, s/1024, s/1024/n
}'

Zum Vergleich derselbe Pool über RSS:

ps -eo pid,rss,args --sort=-rss | grep '[p]hp-fpm' | head

Die RSS-Summe liegt je nach Opcache-Größe deutlich höher. Wer damit rechnet, bekommt ein zu kleines pm.max_children und kauft sich Wartezeit, die es nicht bräuchte.

Zwei Bedingungen entscheiden, ob die Messung etwas taugt. Messen Sie unter echter Last, nicht nach einem Neustart: Ein frisch abgespaltener Prozess ist billig, weil er die Speicherseiten des Hauptprozesses zunächst nur mitbenutzt, und wird erst mit den ersten Anfragen teuer. Und nehmen Sie nicht den Mittelwert allein: Liegt das Mittel bei 60 MiB und der größte Prozess bei 190 MiB, rechnen Sie nicht mit 60.

Die zweite Quelle: das Zugriffsprotokoll von FPM

FPM protokolliert auf Wunsch je Anfrage Spitzenverbrauch und Laufzeit. Beide Zeilen stehen auskommentiert in der Pool-Datei:

access.log = /var/log/php8.2-fpm.access.log
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"

Die Schreibweise %{mili}d ist so korrekt, sie stammt unverändert aus der mitgelieferten Datei und liefert die Laufzeit in Millisekunden, %{kilo}M den Spitzenverbrauch in Kilobyte. Danach prüfen, laden und nachsehen, ob die Datei entsteht:

php-fpm8.2 -t
systemctl reload php8.2-fpm
ls -l /var/log/php8.2-fpm.access.log

Lassen Sie das Protokoll einen vollen Tag mitlaufen, damit die Stoßzeiten enthalten sind. Danach werten Sie den Spitzenverbrauch als Quantile aus:

awk '{print $(NF-1)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Anfragen: %d   Median: %d kB   p95: %d kB   Maximum: %d kB\n", NR, v[int((NR+1)/2)], v[p], v[NR]
}'

Dieselbe Auswertung für die Laufzeit, die Sie gleich für die zweite Rechnung brauchen, holt die Spalte davor:

awk '{print $(NF-2)+0}' /var/log/php8.2-fpm.access.log | sort -n | awk '{v[NR]=$1} END {
  if (!NR) exit
  p=int(NR*0.95); if (p<1) p=1
  printf "Median: %.0f ms   p95: %.0f ms   Maximum: %.0f ms\n", v[int((NR+1)/2)], v[p], v[NR]
}'

Zwei Einschränkungen. Erstens hängen die Feldpositionen an der oben gezeigten Formatzeile, weil diese mit Laufzeit, Speicher und CPU-Anteil endet. Wer access.format ändert, muss sie anpassen. Zweitens ist %{kilo}M die Speicherbuchhaltung von PHP selbst und damit eine Untergrenze: Programmcode, Erweiterungen und der Speicher, den die C-Bibliothek nach einer Anfrage nicht sofort zurückgibt, fehlen darin. Für die Formel gilt deshalb PSS. Das Zugriffsprotokoll dient dazu, die Ausreißer-Skripte zu finden, die einen Prozess dauerhaft groß machen.

Schalten Sie das Protokoll danach wieder ab oder richten Sie eine Rotation dafür ein. Die mitgelieferte Regel erfasst nur das Fehlerprotokoll, und eine volle Platte erzeugt Fehlerbilder, die mit PHP-FPM nichts mehr zu tun haben: Festplatte voll unter Linux aufräumen.

Die Rechnung

Es gibt zwei Zahlen: eine Obergrenze aus dem Arbeitsspeicher und einen Bedarf aus der Last. Der richtige Wert ist der kleinere von beiden.

Obergrenze aus dem Arbeitsspeicher

pm.max_children = Budget für PHP  /  PSS je Arbeitsprozess

Das Budget ist nicht der gesamte Arbeitsspeicher:

free -m

Die Spalte available berücksichtigt bereits den freiräumbaren Zwischenspeicher, enthält aber nicht den Speicher, den Ihre Arbeitsprozesse gerade halten. Das Budget ist also available plus die gemessene PSS-Summe, abzüglich einer Reserve. Für die Reserve haben sich rund 20 Prozent des Gesamtspeichers bewährt, mindestens aber 512 MB. Sie fängt ab, was in keiner Messung auftaucht: einen wachsenden Pufferpool der Datenbank, ein Backup, ein Paketupgrade, eine Importmaske zur Unzeit.

Gesamt-RAMDauerhaft belegtReserveBudget für PHPPSS je Prozesspm.max_children
4 GB1,5 GB0,8 GB1,7 GB60 MB29
8 GB3,0 GB1,6 GB3,4 GB80 MB43
16 GB6,0 GB3,2 GB6,8 GB110 MB63

Runden Sie immer ab. Ein Prozess mehr bringt nichts, ein Prozess zu viel kann alles kosten.

Bedarf aus der Last

benötigte Prozesse = Anfragen pro Sekunde  ×  mittlere Laufzeit in Sekunden

Die Anfragen pro Sekunde liefert die Statusseite von FPM: accepted conn geteilt durch start since ergibt den Durchschnitt seit dem letzten Start. Die Laufzeit kommt aus der Auswertung oben, und zwar der p95-Wert und nicht der Median, sonst planen Sie für den ruhigen Nachmittag.

Beispiel: 40 Anfragen pro Sekunde in der Spitze, p95-Laufzeit 300 Millisekunden, macht 12 gleichzeitige Anfragen, mit Zuschlag für kurze Ausschläge etwa 20 bis 25. Liegt die Obergrenze bei 43, tragen Sie den Bedarfswert ein und lassen den übrigen Speicher dort, wo er mehr nützt, nämlich im Zwischenspeicher der Datenbank.

Liegt der Bedarf über der Obergrenze, tragen Sie ihn trotzdem nicht ein. Dann ist entweder der Arbeitsspeicher zu klein, oder die Skripte sind zu langsam, oder es laufen Anfragen durch PHP, die als statische Datei oder aus einem Zwischenspeicher kommen könnten. Alle drei Ursachen lassen sich beheben, ein zu hoher Wert nicht: Er verschiebt nur den Zeitpunkt des Ausfalls.

dynamic, ondemand oder static

Die Betriebsart legt fest, wann Prozesse entstehen und verschwinden. Die Obergrenze pm.max_children gilt in allen drei Fällen.

BetriebsartProzesse beim StartVerhaltenSpeicherPasst zu
dynamicpm.start_servershält zwischen min_spare und max_spare Prozesse im Leerlauf bereit, spaltet bei Bedarf bis max_children abschwankt mit der Lastdem Normalfall: ein bis wenige Pools, wechselnde Last
ondemandkeinestartet einen Prozess erst bei einer Anfrage, beendet ihn nach pm.process_idle_timeout wiederim Leerlauf am niedrigstenvielen Pools auf einem Server, Auftritten mit wenig Verkehr
staticpm.max_childrengenau diese Anzahl, dauerhaft, ohne Abspalten und Beenden im Betriebkonstant auf dem Maximumeinem Pool auf dafür vorgesehener Hardware, gleichmäßiger Last

dynamic ist die Voreinstellung und für den Normalfall richtig. Sie kostet etwas Rechenzeit fürs Abspalten und verlangt, dass die vier Werte zueinander passen.

ondemand spart im Leerlauf spürbar Speicher, wenn ein Dutzend Pools für selten aufgerufene Auftritte auf einer Maschine liegen. Der Preis ist die erste Anfrage nach einer Ruhephase, die auf das Abspalten warten muss. pm.process_idle_timeout steuert, wie lange ein untätiger Prozess überlebt (ab Werk zehn Sekunden), und wirkt ausschließlich in dieser Betriebsart. Beachten Sie außerdem: Die Sättigungswarnung nennt hier max_children ohne das Präfix pm.. Wer nach dem gewohnten Wortlaut sucht, findet nichts.

static ist ehrlich: Was Sie eintragen, wird sofort und dauerhaft belegt, dafür gibt es unter Last keine Überraschung mehr. Sinnvoll ist das, wenn PHP der Hauptverbraucher der Maschine ist. Teilt sich PHP den Arbeitsspeicher mit einer Datenbank, nimmt static dieser die Möglichkeit, zeitweise mehr Zwischenspeicher zu halten.

Unabhängig von der Betriebsart lohnt ein Blick auf pm.max_requests, ab Werk 0 und damit unbegrenzt. Ein Wert wie 500 ersetzt jeden Arbeitsprozess nach 500 Anfragen. Gegen langsam wachsenden Speicherverbrauch durch eine unsaubere Erweiterung ist das wirksam und billig, denn der Opcache bleibt gemeinsam genutzt und wird nicht neu aufgebaut. Setzen Sie den Wert aber nicht auf 20, sonst verbringt FPM seine Zeit mit Abspalten.

start_servers und die Spare-Werte

Diese drei Werte wirken nur bei pm = dynamic und entscheiden darüber, wie schnell FPM auf eine Lastspitze reagiert:

  • pm.min_spare_servers: so viele untätige Prozesse hält FPM mindestens bereit, der Puffer für Ausschläge. Zu niedrig heißt, jede Spitze wartet erst auf das Abspalten.
  • pm.max_spare_servers: so viele untätige Prozesse duldet FPM höchstens. Ohne diese Grenze behielte FPM nach einer Spitze alle Prozesse und damit deren Speicher.
  • pm.start_servers: so viele Prozesse existieren unmittelbar nach dem Start.

FPM prüft beim Start vier Bedingungen und verweigert den Dienst, wenn eine davon verletzt ist: Beide Spare-Werte müssen größer als null sein, keiner von beiden darf größer als pm.max_children sein, max_spare darf nicht kleiner als min_spare sein, und start_servers muss dazwischen liegen. Fehlt pm.start_servers ganz, rechnet FPM selbst und protokolliert:

NOTICE: [pool www] pm.start_servers is not set. It's been set to 3.

Die Formel dafür ist min_spare + (max_spare - min_spare) / 2, also die Mitte zwischen beiden Spare-Werten. Als Ausgangspunkt funktioniert in der Praxis:

pm.max_children       = 40
pm.start_servers      = 10
pm.min_spare_servers  = 6
pm.max_spare_servers  = 16

Leicht übersehen: Auch untätige Prozesse belegen Speicher. Das Budget muss pm.max_children tragen, der Alltagsverbrauch entspricht dagegen ungefähr max_spare zuzüglich der aktiven Prozesse. Ein hoher max_spare hält den Speicher auch nachts um drei belegt.

Der Zusammenhang mit dem Swap

Es ist verlockend, den Auslagerungsspeicher in die Rechnung aufzunehmen. Tun Sie es nicht. Ein Arbeitsprozess, dessen Daten auf der Platte liegen, antwortet um Größenordnungen langsamer und bleibt dadurch länger belegt. Die Zahl gleichzeitiger Anfragen steigt, FPM spaltet weitere Prozesse ab, und diese verdrängen weiteren Speicher. Diese Rückkopplung ist der Grund, warum ein überbuchter Server nicht allmählich schlechter wird, sondern innerhalb weniger Minuten kippt.

Was Swap trotzdem leistet, ist ein Puffer für den Fehlerfall. Ohne ihn endet eine Überbuchung abrupt damit, dass der Kernel einen Prozess beendet. Mit ihm bekommen Sie zuerst einen langsamen Server und damit ein Zeitfenster zum Eingreifen. Die Regel lautet: Swap ja, aber pm.max_children ausschließlich gegen den echten Arbeitsspeicher rechnen, niemals gegen die Summe aus Arbeitsspeicher und Swap.

Ob Sie bereits auslagern, sehen Sie an zwei Stellen. free -m zeigt die Belegung, sagt aber nichts über die Aktivität, denn eine einmal ausgelagerte und nie wieder benötigte Seite ist harmlos. Aussagekräftig sind die Spalten si und so, also Ein- und Auslagerung pro Sekunde:

free -m
vmstat 1 5

Dauerhaft Werte über null heißen, der Server arbeitet gegen die Platte. Falls Ihr Kernel die Druckanzeige mitliefert, ist sie noch direkter, denn sie sagt nicht, wie viel ausgelagert wurde, sondern wie lange Prozesse deswegen warten mussten:

cat /proc/pressure/memory

Existiert die Datei nicht, ist die Funktion im Kernel abgeschaltet und Sie bleiben bei vmstat. Wie Sie Swap anlegen, dauerhaft eintragen und vm.swappiness passend setzen, steht im Beitrag Swap einrichten und Out-of-Memory beheben.

Zu hoch gesetzt: der OOM-Killer statt einer Warteschlange

Angenommen, Sie tragen 200 ein, damit die Warnung endlich verschwindet. Im Leerlauf passiert nichts, die Seite lädt, alles wirkt gelöst. Beim nächsten Ansturm spaltet FPM tatsächlich bis zu 200 Prozesse ab, jeder wächst mit den ersten Anfragen auf seine echte Größe, der freie Speicher fällt, der Kernel wirft zuerst den Datei-Zwischenspeicher weg (wodurch die Datenbank langsamer wird und ihre Abfragen länger dauern), dann lagert er aus, und dann greift der OOM-Killer.

Der wählt sein Opfer nach Speicherverbrauch, und der größte Einzelprozess auf einem Webserver ist nicht ein PHP-Arbeitsprozess mit 80 MB, sondern die Datenbank mit ihrem Pufferpool:

dmesg -T | grep -iE "out of memory|oom-kill"
journalctl -k --since "24 hours ago" | grep -i "out of memory"

Die beiden Zeilen, auf die es ankommt, sehen so aus:

php-fpm8.2 invoked oom-killer: gfp_mask=0x1100cca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0
Out of memory: Killed process 1234 (mariadbd) total-vm:2891234kB, anon-rss:1783456kB,
file-rss:0kB, shmem-rss:0kB, UID:107 pgtables:4096kB oom_score_adj:0

Auslöser und Opfer sind hier zwei verschiedene Prozesse, und genau das macht die Fehlersuche unangenehm: Im Postfach liegt eine Meldung über eine abgestürzte Datenbank, und niemand denkt an die PHP-Konfiguration von vorgestern. Je nach Datenbank steht in Klammern mariadbd oder mysqld.

Trifft es doch einen Arbeitsprozess, meldet FPM das selbst, und bei gesetztem Speicherlimit steht es zusätzlich im Journal:

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start
php8.2-fpm.service: A process of this unit has been killed by the OOM killer.

Der Unterschied zwischen beiden Fehlerbildern ist der eigentliche Punkt dieses Beitrags. Ein zu niedriges pm.max_children erzeugt eine Warteschlange: messbar, im Protokoll dokumentiert, auf einen Wert zurückführbar, und der Server bleibt ansprechbar. Ein zu hohes erzeugt einen beendeten Prozess an einer Stelle, die Sie sich nicht ausgesucht haben, bei einem Dienst, der unter Umständen nicht von selbst zurückkommt. Wartezeit ist ein Betriebszustand, ein OOM-Ereignis ist ein Zwischenfall.

Häufige Fehler und Lösungen

WARNING: [pool www] server reached pm.max_children setting (5), consider raising it: Zu einem Zeitpunkt waren alle Arbeitsprozesse belegt. Die Zeile erscheint einmal je Sättigungsphase, eine einzelne Zeile kann also eine Stunde Vollauslastung bedeuten. Erhöhen Sie den Wert erst, nachdem Sie PSS gemessen und das Budget bestimmt haben.

WARNING: [pool www] server reached max_children setting (5), consider raising it: Dieselbe Lage unter pm = ondemand, im Wortlaut ohne das Präfix pm..

ALERT: [pool www] pm.min_spare_servers and pm.max_spare_servers cannot be greater than pm.max_children, gefolgt von ERROR: failed to post process the configuration und ERROR: FPM initialization failed: Der häufigste Fall beim Senken von pm.max_children. Wer von 50 auf 8 geht und pm.max_spare_servers = 20 stehen lässt, hat anschließend einen Dienst, der nicht mehr startet. Alle vier Werte gehören zusammen geändert.

ALERT: [pool www] pm.start_servers must not be less than pm.min_spare_servers and not greater than pm.max_spare_servers: pm.start_servers liegt außerhalb der Spanne. Korrigieren Sie den Wert oder entfernen Sie die Zeile, dann rechnet FPM selbst.

PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes): Das ist memory_limit und hat mit pm.max_children nichts zu tun. Ein einzelnes Skript hat mehr angefordert, als PHP je Anfrage zulässt. Ein höheres pm.max_children ändert daran nichts, und ein niedrigeres memory_limit verkleinert umgekehrt nicht den Speicherbedarf eines Arbeitsprozesses, es bricht Skripte nur früher ab. Für die Planung gilt trotzdem: memory_limit ist die Obergrenze dessen, was ein Prozess anfordern darf.

connect() to unix:/run/php/php8.2-fpm.sock failed (11: Resource temporarily unavailable) while connecting to upstream im Fehlerprotokoll von nginx: Alle Prozesse belegt und zusätzlich die Annahmewarteschlange voll. Ein höheres listen.backlog verschiebt das nur, die Ursache liegt bei der Anzahl der Prozesse oder bei der Laufzeit der Skripte.

WARNING: [pool www] child 1234 exited on signal 9 (SIGKILL) after 3612.472183 seconds from start: Der Prozess wurde von außen hart beendet, in aller Regel durch den OOM-Killer. Hier ist der Wert zu hoch, nicht zu niedrig. Gegenprobe im Kernel-Protokoll.

"Ich habe den Wert erhöht, es ändert sich nichts." Fast immer wurde die falsche Datei bearbeitet: eine zweite PHP-Version unter /etc/php/, ein zweiter Pool oder eine Kopie in pool.d/, die gar nicht eingelesen wird. Maßgeblich ist, welchen Socket nginx anspricht und welcher Pool darauf lauscht:

grep -Rn "fastcgi_pass" /etc/nginx/
grep -n "^listen *=" /etc/php/*/fpm/pool.d/*.conf
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

"Nach dem Reload ist die Seite weg." Ein Reload startet FPM mit der neuen Konfiguration neu, und ist diese ungültig, beendet sich der Hauptprozess. Spielen Sie die Kopie aus /root/backups zurück und gewöhnen Sie sich php-fpm8.2 -t vor jedem Reload an.

Woran Sie erkennen, dass der Wert stimmt

"Die Warnung ist weg" ist kein Nachweis. Mit dem Wert 500 ist sie auch weg, bis zum ersten OOM-Ereignis. Belastbar sind fünf Prüfungen.

Erstens die Statusseite von FPM. Tragen Sie pm.status_path = /status in die Pool-Datei ein, prüfen und laden Sie, und fragen Sie den Socket direkt ab, vollständig an nginx vorbei. Damit ist die Seite nicht aus dem Netz erreichbar:

apt-get install -y libfcgi-bin
SCRIPT_NAME=/status SCRIPT_FILENAME=/status REQUEST_METHOD=GET \
  cgi-fcgi -bind -connect /run/php/php8.2-fpm.sock

Vier Zeilen der Ausgabe tragen die Antwort:

FeldBedeutungZielwert
max children reachedwie oft die Obergrenze seit dem Start erreicht wurde0
max listen queuelängste beobachtete Warteschlange am Socket0
max active processeshöchste gleichzeitig aktive Prozesszahldeutlich unter pm.max_children
slow requestsAnfragen über request_slowlog_timeoutmöglichst 0

Aussagekräftig wird das erst nach einer vollen Woche mit allen Stoßzeiten. Steht max active processes danach bei 12, während pm.max_children auf 43 steht, haben Sie Luft und können die Reserve anderswo verwenden.

Zweitens der Speicher unter Last, nicht nachts, sondern in der Spitze. available muss deutlich über null bleiben, die Spalten si und so sollen bei null stehen:

free -m
vmstat 1 5

Drittens: kein OOM-Ereignis. Diese Prüfung ist die wichtigste, weil sie das schlimmste Fehlerbild ausschließt. Eine leere Ausgabe ist das gewünschte Ergebnis:

journalctl -k --since "7 days ago" | grep -i "out of memory"

Viertens die Summe über alle Pools. Auf einem Server mit mehreren Auftritten zählt nicht der einzelne Wert, sondern die Summe, multipliziert mit dem PSS je Prozess. Sie muss ins Budget passen, auch wenn alle Pools gleichzeitig unter Last stehen:

php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Fünftens: Es übersteht einen Neustart. Der am häufigsten übersprungene Punkt. Ein Wert in einer Datei, die gar nicht eingelesen wird, fällt erst beim nächsten Neustart auf, und der passiert selten dann, wenn Sie gerade hinsehen:

systemctl is-enabled php8.2-fpm
systemctl restart php8.2-fpm
php-fpm8.2 -tt 2>&1 | grep "pm.max_children"

Starten Sie den Server danach einmal kontrolliert neu, solange Sie noch zusehen. Bei den KVM-Rootservern und Dedicated Servern von KernelHost lösen Sie diesen Neustart im Kundenbereich aus und verfolgen das Hochfahren über die VNC-Konsole, auch wenn der Webdienst noch nicht antwortet. Die Server stehen im Rechenzentrum maincubes in Frankfurt am Main.

Kurzcheckliste

  1. Kopie der Pool-Datei nach /root/backups, Zugang über die VNC-Konsole einmal ausprobiert.
  2. PSS je Arbeitsprozess unter echter Last messen, nicht nach einem Neustart, und nicht mit RSS rechnen.
  3. Budget bestimmen: available plus laufende PSS-Summe, abzüglich einer bewusst gesetzten Reserve.
  4. Obergrenze ausrechnen, Bedarf aus Anfragen pro Sekunde mal p95-Laufzeit gegenrechnen, den kleineren Wert nehmen, abrunden.
  5. Betriebsart wählen und die Spare-Werte gemeinsam mit pm.max_children anpassen.
  6. php-fpm8.2 -t, dann systemctl reload, dann php-fpm8.2 -tt als Nachweis, dass der Wert geladen ist.
  7. Eine Woche später max children reached, max listen queue und das Kernel-Protokoll prüfen.

Häufige Fragen

Wie berechne ich pm.max_children richtig?
Über zwei Zahlen, und der kleinere Wert gewinnt. Die Obergrenze ist das Budget für PHP geteilt durch den anteiligen Speicher (PSS) eines Arbeitsprozesses. Das Budget ist die Spalte available aus free -m, zuzüglich des Speichers, den die laufenden Arbeitsprozesse gerade halten, abzüglich einer Reserve von rund 20 Prozent des Gesamtspeichers. Die zweite Zahl ist der Bedarf: Anfragen pro Sekunde in der Spitze multipliziert mit der p95-Laufzeit in Sekunden. Ergibt die Obergrenze 43 und der Bedarf 22, tragen Sie 22 ein und lassen den Rest des Speichers der Datenbank. Immer abrunden.
Warum ist ein zu hoher Wert gefährlicher als ein zu niedriger?
Ein zu niedriger Wert erzeugt eine Warteschlange. Anfragen warten in der Annahmewarteschlange des Sockets, die Seite wird langsamer, der Server bleibt ansprechbar und das Protokoll sagt Ihnen genau, was los ist. Ein zu hoher Wert erzeugt unter Last einen Speichermangel, und dann sucht sich der Kernel selbst ein Opfer, und zwar nach Speicherverbrauch. Der größte Prozess auf einem Webserver ist meist die Datenbank mit ihrem Pufferpool, nicht ein PHP-Arbeitsprozess. Sie tauschen also messbare Wartezeit gegen einen unangekündigten Ausfall eines anderen Dienstes.
Warum soll ich mit PSS rechnen und nicht mit RSS?
RSS enthält auch die gemeinsam genutzten Speicherbereiche, allen voran den Opcache und die geteilten Bibliotheken. Wer RSS über 20 Arbeitsprozesse aufsummiert, zählt denselben Opcache zwanzigmal und kommt auf einen viel zu großen Speicherbedarf, also auf ein unnötig kleines pm.max_children. PSS teilt gemeinsam genutzte Seiten durch die Zahl ihrer Nutzer und lässt sich deshalb korrekt aufsummieren. Der Wert steht als root in /proc/PID/smaps_rollup in der Zeile Pss.
Wann nehme ich dynamic, wann ondemand und wann static?
dynamic ist die Voreinstellung und für den Normalfall richtig: ein bis wenige Pools mit wechselnder Last. ondemand lohnt sich, wenn viele Pools für selten aufgerufene Auftritte auf einer Maschine liegen, denn im Leerlauf existieren dort keine Prozesse. Der Preis ist die erste Anfrage nach einer Ruhephase, die auf das Abspalten eines Prozesses wartet. static passt, wenn PHP der Hauptverbraucher der Maschine ist und die Last gleichmäßig ist: Der Speicher wird sofort und dauerhaft belegt, dafür gibt es unter Last keine Überraschung mehr. Teilt sich PHP den Server mit einer Datenbank, nimmt static dieser die Möglichkeit, zeitweise mehr Zwischenspeicher zu halten.
Darf ich den Swap in die Rechnung einbeziehen?
Nein. Ein Arbeitsprozess, dessen Daten auf der Platte liegen, antwortet um Größenordnungen langsamer und bleibt dadurch länger belegt. Die Zahl gleichzeitiger Anfragen steigt, FPM spaltet weitere Prozesse ab, und diese verdrängen weiteren Speicher. Deshalb kippt ein überbuchter Server innerhalb weniger Minuten, statt allmählich langsamer zu werden. Swap ist trotzdem sinnvoll, aber als Puffer für den Fehlerfall: Er verschafft Ihnen ein Zeitfenster zum Eingreifen, bevor der Kernel einen Prozess beendet. Gerechnet wird ausschließlich gegen den echten Arbeitsspeicher.
Ich habe pm.max_children erhöht, es ändert sich nichts. Woran liegt das?
Fast immer wurde die falsche Datei bearbeitet. Auf Servern mit mehreren PHP-Versionen unter /etc/php/ oder mehreren Pools zählt nur der Pool, dessen Socket nginx wirklich anspricht. Vergleichen Sie fastcgi_pass aus der nginx-Konfiguration mit den listen-Zeilen der Pool-Dateien. Was FPM tatsächlich geladen hat, zeigt php-fpm8.2 -tt, dessen Ausgabe die vollständig aufgelöste Konfiguration enthält, inklusive aller Vorgabewerte.
Nach dem Senken von pm.max_children startet PHP-FPM nicht mehr. Was ist passiert?
Wahrscheinlich stehen die Spare-Werte noch auf den alten, höheren Zahlen. FPM verweigert den Start mit der Meldung, dass pm.min_spare_servers und pm.max_spare_servers nicht größer als pm.max_children sein dürfen, gefolgt von FPM initialization failed. Passen Sie alle vier Werte gemeinsam an: Beide Spare-Werte müssen größer als null und höchstens so groß wie pm.max_children sein, max_spare darf nicht kleiner als min_spare sein, und pm.start_servers muss zwischen beiden liegen.
Hat memory_limit etwas mit pm.max_children zu tun?
Nur indirekt. memory_limit ist die Obergrenze, die PHP je Anfrage erzwingt, ab Werk 128M im FPM-Betrieb. Die Meldung über erschöpften Speicher (Allowed memory size exhausted) betrifft ein einzelnes Skript und wird durch ein höheres pm.max_children nicht besser. Umgekehrt verkleinert ein niedrigeres memory_limit nicht den Speicherbedarf eines Arbeitsprozesses, es bricht Skripte nur früher ab. Der Zusammenhang besteht in der Planung: Wer memory_limit auf 1024M setzt, muss im ungünstigsten Fall auch mit dieser Größe je Prozess rechnen.

PHP-FPM pm.max_children Arbeitsspeicher Debian Ubuntu nginx OOM-Killer Serveroptimierung