PHP schneller konfigurieren: OPcache, Preloading, FPM

TL;DR — Eine gut eingestellte PHP-Umgebung bringt bei jedem Request etwas, und die meisten Einstellungen kosten nur einmal Nachdenken. Zwei davon greifen allerdings in den Deployment-Ablauf ein und wollen vorher verstanden sein. Dieser Artikel zeigt, welche Einstellungen wirklich wirken, welche nur Speicher belegen und wie du den Unterschied nachmisst — statt Werte aus fremden Blogartikeln abzuschreiben.

Was bei einem PHP-Request ohne und mit OPcache passiert Ohne OPcache — bei jedem Request Datei lesen Parsen Kompilieren Ausführen Mit OPcache — nur beim ersten Mal Lesen, Parsen, Kompilieren — entfällt ab dem zweiten Request Ausführen
Abbildung 1: OPcache legt den kompilierten Bytecode im Arbeitsspeicher ab. Die ersten drei Schritte entfallen ab dem zweiten Request — bei jeder Datei, bei jedem Benutzer.

Alle Einstellungen in diesem Artikel sind Bordmittel. Du brauchst keine Erweiterung außerhalb von OPcache, das seit PHP 5.5 mitgeliefert wird.

Vorab. Alle Werte hier sind Ausgangspunkte, keine Vorgaben. Was zu deinem Server passt, hängt von RAM, Anwendungsgröße und Deployment-Ablauf ab. Probiere Änderungen zuerst auf einem Testsystem, ändere eine Sache pro Schritt und sorge dafür, dass du den vorherigen Stand zurückholen kannst. Für den Betrieb deiner Systeme bist du verantwortlich.


Erst prüfen, was tatsächlich eingestellt ist

Der häufigste Fehler bei PHP-Konfiguration ist, Werte aus einem Blogartikel zu übernehmen, ohne den Ist-Zustand zu kennen. Die Voreinstellungen unterscheiden sich zwischen PHP-Versionen, Distributionen und Hosting-Panels erheblich — was bei dir steht, weißt du nur, wenn du nachsiehst.

php -i | grep -E 'opcache|realpath_cache|memory_limit'

Auf einem Webserver ist zusätzlich wichtig: die CLI hat eine andere php.ini als FPM. Was php -i zeigt, gilt nicht automatisch für deine Website. Meistens kommst du trotzdem ohne den Browser aus — die Kommandozeile lässt sich mit der FPM-Konfiguration starten und deckt alles ab, was in der php.ini steht:

php -c /etc/php/8.3/fpm/php.ini -i | grep -E 'opcache|realpath_cache'

Was dieser Weg nicht abdeckt, sind php_admin_value-Überschreibungen aus der Pool-Konfiguration. Nur dafür brauchst du wirklich den Blick aus dem Webserver heraus:

<?php phpinfo();

Diese Datei ist ein vollständiger Lageplan deines Servers — und deshalb der letzte, nicht der erste Griff. Sie zeigt nicht nur Pfade, Erweiterungen und Versionen, sondern den kompletten $_SERVER-Block samt Session-Cookie des Aufrufenden, und je nach variables_order alle Umgebungsvariablen. In Container- und Panel-Setups stehen dort regelmäßig Datenbank-Passwörter und API-Schlüssel im Klartext. Wenn es nicht anders geht: zuerst den Zugriff auf deine IP beschränken (Require ip in Apache, allow/deny in nginx), dann die Datei anlegen, aufrufen und sofort wieder löschen. Ein geratener Dateiname ersetzt die Zugriffsbeschränkung nicht — er verzögert nur den Fund. Eine vergessene phpinfo.php ist einer der häufigsten Wege, auf denen Zugangsdaten abfliessen.

Die Reihenfolge beim Einstellen einer PHP-Umgebung 1 Ist-Zustand phpinfo, nicht CLI 2 OPcache-Status oom_restarts, Trefferquote 3 Speicher messen je Prozess, im Betrieb 4 Pool rechnen nicht raten
Abbildung 2: Jeder Schritt liefert die Zahl, die der nächste braucht. Wer bei Schritt 4 anfängt, rät.

OPcache: die eine Einstellung, die immer wirkt

OPcache speichert den kompilierten Bytecode deiner PHP-Dateien im Arbeitsspeicher. Ohne ihn liest, parst und kompiliert PHP bei jedem Request jede beteiligte Datei neu.

Die Werte, die zählen

opcache.enable = 1
opcache.enable_cli = 0
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0

memory_consumption in Megabyte. Wichtig zu wissen: OPcache verdrängt nicht. Ist der Speicher voll, nimmt er neue Skripte gar nicht mehr auf — die werden dann bei jedem Request neu kompiliert, ohne dass etwas Alarm schlägt. Erst wenn zu viel Speicher als wasted gilt, wirft OPcache den gesamten Cache weg und fängt von vorn an (Restart). Ob dein Wert reicht, sagt dir die Laufzeit selbst:

$s = opcache_get_status(false);
if ($s === false) {
    exit("OPcache ist in diesem SAPI nicht aktiv.\n");
}

printf("voll: %s | Skripte: %d von %d | frei: %.1f MB | OOM-Neustarts: %d | Treffer: %.1f %%\n",
    $s['cache_full'] ? 'ja' : 'nein',
    $s['opcache_statistics']['num_cached_scripts'],
    $s['opcache_statistics']['max_cached_keys'],
    $s['memory_usage']['free_memory'] / 1048576,
    $s['opcache_statistics']['oom_restarts'],
    $s['opcache_statistics']['opcache_hit_rate']
);

cache_full ist die direkte Antwort auf „reicht mein Cache?" — und der Wert, der am häufigsten übersehen wird. Er steht auf ja, sobald entweder der Speicher oder die Datei-Obergrenze erreicht ist, ohne dass oom_restarts hochzählt. Wer nur auf die Neustarts schaut, übersieht damit den häufigeren Fall: ein zu kleines max_accelerated_files. Steht oom_restarts zusätzlich über null, ist auch der Speicher zu knapp. Die Trefferquote sollte im laufenden Betrieb nahe 100 % liegen.

⚠ Das Skript gehört über den Webserver aufgerufen, nicht über die Kommandozeile: FPM und CLI führen getrennte Caches, und mit dem oben empfohlenen opcache.enable_cli = 0 liefert opcache_get_status() auf der CLI ohnehin false.

max_accelerated_files begrenzt die Anzahl gecachter Dateien. PHP rundet den Wert intern auf die nächste Zahl aus einer festen Reihe auf — 223, 463, 983, 1979, 3907, 7963, 16229, 32531, 65407, 130987. Ein Wert von 20000 ergibt also effektiv 32531. Wie viele du wirklich brauchst, sagt num_cached_scripts aus dem Snippet oben — belastbarer als ein Dateizähler, der vendor/ und tests/ mitnimmt. Als grober erster Anhalt:

find . -name '*.php' | wc -l

validate_timestamps = 0 ist der größte Einzelgewinn — und die Einstellung mit dem Fallstrick. Sie schaltet die Prüfung ab, ob eine Datei sich geändert hat. Damit spart PHP bei jedem Request einen Dateisystem-Zugriff pro eingebundener Datei.

Danach übernimmt der Server geänderten Code nicht mehr von selbst. Nach jedem Deployment muss der Cache geleert werden — in der Regel durch einen Reload von PHP-FPM. Wer das vergisst, sucht stundenlang einen Fehler in Code, der längst korrigiert ist. Setze diesen Wert deshalb erst, wenn dein Deployment den Reload zuverlässig mitmacht — und in der Entwicklung nie.


Preloading: für große Anwendungen, nicht für jede

Seit PHP 7.4 lassen sich Klassen beim Start des Prozesses laden und dauerhaft im Speicher halten. Sie stehen dann jedem Request ohne Autoloader und ohne Dateizugriff zur Verfügung.

opcache.preload = /var/www/preload.php
opcache.preload_user = www-data

Die Preload-Datei bindet ein, was geladen werden soll — üblicherweise über die vom Framework mitgelieferte Liste oder über die Composer-Klassenkarte.

Wann sich das lohnt und wann nicht:

Situation Lohnt sich Preloading?
Großes Framework, hunderte Klassen pro Request ja, deutlich
Kleine Anwendung mit wenigen Dateien kaum messbar
Häufige Deployments nur mit automatisiertem Reload
Shared Hosting ohne Neustart-Rechte nein, nicht einsetzbar

⚠ Preloading hat dieselbe Konsequenz wie validate_timestamps = 0, nur schärfer: geänderte Klassen werden erst nach einem Neustart von PHP-FPM aktiv. Ohne Kontrolle über den Dienst ist Preloading nicht praktikabel.


JIT: ehrlich betrachtet

Der Just-in-Time-Compiler aus PHP 8.0 wird oft als der große Performance-Sprung beschrieben. Für typische Webanwendungen ist er das nicht.

JIT übersetzt heißen Bytecode in Maschinencode. Das hilft, wenn die CPU der Engpass ist — bei Bildverarbeitung, Mandelbrot-Mengen, numerischen Schleifen. Eine typische Webanwendung wartet aber die meiste Zeit: auf die Datenbank, auf eine API, auf das Dateisystem. Dort beschleunigt JIT das, was ohnehin nicht das Problem war.

; Nur sinnvoll bei nachweislich CPU-gebundener Last:
opcache.jit_buffer_size = 64M
opcache.jit = tracing

Die ehrliche Empfehlung: JIT nicht pauschal aktivieren. Miss vorher, ob deine Anwendung überhaupt CPU-gebunden ist — wenn der Profiler zeigt, dass die Zeit im Warten auf externe Systeme vergeht, ändert JIT daran nichts. Wo die Zeit eines Requests wirklich hingeht und wie du das feststellst, steht in unserem Artikel zu den teuersten Fehlern im PHP-Code.


realpath_cache: klein, unauffällig, wirksam

Jedes include und jedes require mit relativem Pfad muss aufgelöst werden — aus ../lib/Foo.php wird ein absoluter Pfad. Diese Auflösung kostet Dateisystem-Zugriffe, und PHP merkt sich das Ergebnis im realpath_cache.

; 4096k ist bereits der Default — anheben lohnt nur bei grossen Codebasen
; mit tiefen Verzeichnisbäumen:
realpath_cache_size = 16384k
realpath_cache_ttl  = 600      ; Default ist 120

Der Effekt ist auf Systemen mit vielen Dateien und tiefen Verzeichnisbäumen spürbar, besonders bei netzgebundenem Speicher. Ob dein Cache überläuft, zeigt:

printf("realpath-Cache: %d Bytes belegt von %s, %d Einträge\n",
    realpath_cache_size(), ini_get('realpath_cache_size'), count(realpath_cache_get()));

Läuft der Wert an die konfigurierte Grenze, verwirft PHP Einträge und löst dieselben Pfade wieder und wieder auf.


FPM-Pools: mehr Prozesse sind nicht mehr Leistung

Zu viele FPM-Prozesse gegen passend dimensionierte Zu viele Prozesse Summe der Speicherlimits größer als der RAM System beginnt zu swappen Passend dimensioniert jeder Prozess hat Speicher Anfragen warten kurz statt lange gleichmäßige Antwortzeiten
Abbildung 3: Sobald die Summe der Prozess-Speicherbedarfe den RAM übersteigt, wird das System langsamer, nicht schneller. Eine kurze Warteschlange ist besser als Swapping.

Der verbreitetste Fehler in der FPM-Konfiguration ist, pm.max_children zu erhöhen, weil die Seite langsam ist. Das hilft nur, wenn Anfragen tatsächlich in der Warteschlange stehen — und es schadet, sobald der Speicher nicht mehr reicht.

⚠ Reicht er deutlich nicht, bleibt es nicht bei Swapping: dann beendet der Kernel Prozesse, im ungünstigen Fall die Datenbank. Rechne mit Reserve und beobachte den Server nach der Änderung.

Die Rechnung ist einfach:

pm.max_children  ≈  (verfügbarer RAM − Reserve für DB und System)  ÷  Speicher je Prozess

Zur Reserve gehört mehr als Datenbank und Betriebssystem. Der OPcache selbst belegt memory_consumption als gemeinsamen Speicher — einmal, aber er fällt an. Der realpath_cache dagegen existiert je Prozess: bei 4 MB und 100 Workern sind das bis zu 400 MB, die in der Formel oben nicht auftauchen. Miss deshalb erst, nachdem beide Werte gesetzt sind.

Den Speicherbedarf je Prozess misst du im laufenden Betrieb, nicht durch Schätzen:

pgrep -f php-fpm | xargs -r ps -o rss= -p | awk '{s+=$1; n++}
  END {if (n) printf "%d Prozesse, im Schnitt %.0f MB\n", n, s/n/1024
       else print "keine php-fpm-Prozesse gefunden"}'

Auf Debian und Ubuntu heisst der Prozess php-fpm8.3 statt php-fpmpgrep -f fängt beide Schreibweisen, und das if (n) verhindert eine Division durch null, wenn nichts gefunden wird.

Zwei Werte, die dabei oft verwechselt werden:

⚠ Dieser Durchschnitt ist eher zu hoch als zu niedrig: RSS zählt gemeinsam genutzten Speicher bei jedem Prozess voll mit — das OPcache-Segment, die Bibliotheken, die Copy-on-Write-Seiten nach dem Fork. Die Formel liefert damit eher zu wenige Worker als zu viele, was die sichere Richtung ist. Wer es genau will, nimmt PSS (smem -P php-fpm oder das Feld Pss aus /proc/<pid>/smaps_rollup).

memory_limit ist die Obergrenze für ein einzelnes Skript, nicht der übliche Verbrauch. Ein Limit von 512 MB heißt nicht, dass jeder Prozess 512 MB belegt — es heißt, dass er im schlimmsten Fall so weit wachsen darf. Für die Dimensionierung zählt der tatsächliche Durchschnitt, für die Sicherheit die Obergrenze.

pm = dynamic, static oder ondemand. static hält eine feste Zahl Prozesse vor und ist auf dedizierten Servern mit gleichmäßiger Last die ruhigere Wahl. dynamic fährt hoch und runter und passt, wenn sich der Server die Maschine mit anderen Diensten teilt. ondemand startet Prozesse erst bei Bedarf und beendet sie wieder — die richtige Wahl, wenn viele kleine Sites mit wenig Verkehr auf einer Maschine liegen, wie es bei Hosting-Panels der Regelfall ist.


Was die Konfiguration nicht reparieren kann

Eine gut eingestellte Umgebung beschleunigt guten und schlechten Code gleichermassen — sie ändert nichts an dessen Struktur. Eine Schleife, die pro Durchlauf die Datenbank fragt, bleibt mit OPcache genauso oft in der Warteschlange wie ohne.

Die Reihenfolge ist deshalb: erst die Muster im Code ausräumen, dann die Umgebung einstellen. Wer es umgekehrt macht, verschiebt die Grenze ein Stück nach oben und trifft sie bei der nächsten Datenmenge wieder. Die acht häufigsten dieser Muster — von N+1-Abfragen bis zur blockierenden Session — stehen in unserem Artikel zu PHP-Performance-Anti-Pattern.

Und wenn die Datenbank der Engpass ist, hilft keine PHP-Einstellung: dann geht es um Indexe, Speicherparameter und Wartung. Die PostgreSQL-Seite davon behandeln wir in den PostgreSQL-Performance-Einstellungen und, für große Instanzen, in Huge Pages für PostgreSQL.


Häufige Fragen (FAQ)

Welche OPcache-Einstellung bringt am meisten?

opcache.validate_timestamps = 0 in der Produktion, zusammen mit ausreichend memory_consumption. Damit entfällt pro eingebundener Datei ein Dateisystem-Zugriff je Request. Voraussetzung ist, dass dein Deployment PHP-FPM zuverlässig neu lädt — sonst läuft der Server nach dem nächsten Deployment mit altem Code weiter.

Woran erkenne ich, dass der OPcache zu klein ist?

An cache_full in opcache_get_status() — das ist der direkte Wert und steht auch dann auf ja, wenn nur die Datei-Obergrenze erreicht ist. oom_restarts über null heisst zusätzlich, dass OPcache mangels Speicher neu starten und den gesamten Cache verwerfen musste. Verdrängt wird dabei nichts: ist der Cache voll, nimmt OPcache neue Skripte einfach nicht mehr auf, und die werden bei jedem Request neu kompiliert.

Soll ich JIT aktivieren?

Nur wenn deine Anwendung nachweislich CPU-gebunden ist. Bei typischen Webanwendungen vergeht die meiste Zeit mit Warten auf Datenbank oder externe Dienste, und dort ändert JIT nichts. Miss zuerst, wo die Zeit hingeht — aktiviere JIT erst, wenn die Antwort „im Rechnen" lautet.

Wie viele FPM-Prozesse sind richtig?

So viele, wie der Arbeitsspeicher zulässt, abzüglich einer Reserve für Datenbank und Betriebssystem. Miss den tatsächlichen Speicherbedarf eines Prozesses im Betrieb und teile den verfügbaren RAM dadurch. Mehr Prozesse als der Speicher trägt machen das System langsamer, weil es zu swappen beginnt — eine kurze Warteschlange ist die bessere Alternative.

Warum wirkt meine php.ini-Änderung nicht?

Meistens, weil du die falsche Datei bearbeitet hast: die Kommandozeile und PHP-FPM nutzen getrennte Konfigurationen. Prüfe über phpinfo() im Browser, welche Datei tatsächlich geladen wird. Der zweite häufige Grund ist ein fehlender Reload von PHP-FPM — Änderungen an der php.ini greifen erst danach.

Bringt eine neuere PHP-Version mehr als gute Konfiguration?

Beides zusammen. Die Versionssprünge haben erhebliche Gewinne gebracht, aber eine aktuelle Version mit abgeschaltetem OPcache ist langsamer als eine ältere mit eingeschaltetem. Aktualisiere die Version aus Sicherheitsgründen ohnehin regelmäßig und stelle die Umgebung unabhängig davon richtig ein.


Wo du weitermachst

Konfiguration ist die eine Hälfte, Code die andere. Wer beide Seiten systematisch angehen will: das Seminar PHP OOP und fortgeschrittene Technologien behandelt die Strukturen, die überhaupt erst cachefähigen und messbaren Code ergeben, und PHP-Programmierung Grundlagen legt die Basis dafür. Geht es um das Zusammenspiel mit der Datenbank über Systemgrenzen hinweg, ist Datenbankübergreifendes Performance-Tuning das passende Format.

Alle Einstellungen in diesem Artikel sind Bordmittel und kosten keine Lizenz. Was sie kosten, ist die Disziplin, den Ist-Zustand zu messen, bevor man ihn ändert.