Zum Inhalt springen
Core Web Vitals Spezialisten
Server & Hosting

PHP-Laufzeit optimieren: OPcache, JIT und FPM-Worker

Zwischen Webserver und Datenbank liegt die PHP-Laufzeit. Wie OPcache, Preloading, JIT und die FPM-Worker die Antwortzeit unter Last bestimmen.

12 Min. Lesezeit Server-OptimierungTTFBPHPOPcacheBackend

Wenn eine Seite zu langsam antwortet, beginnt die Ursachensuche fast überall an denselben zwei Enden: Man wechselt das Hosting-Paket, oder man nimmt sich die Datenbank vor. Dazwischen liegt eine dritte Schicht, die deutlich seltener geprüft wird und bei WordPress- und Shopware-Installationen häufig den größten Anteil an der Server-Antwortzeit trägt -- die Laufzeit der Anwendung selbst. Hier werden Quelldateien gelesen, zu Opcodes kompiliert und ausgeführt, bevor die Datenbank überhaupt eine Abfrage sieht. Wie schnell dieser Teil arbeitet, hängt an drei Stellschrauben: am OPcache, an der Frage, ob der JIT-Compiler in einer kurzen Webanfrage überhaupt etwas beiträgt, und an der Konfiguration der FPM-Worker, die die Anfragen abarbeiten. Als gut gilt eine Server-Antwortzeit von höchstens 800 Millisekunden (web.dev); viele Installationen halten diesen Wert im Leerlauf und verlieren ihn genau dann, wenn Betrieb herrscht. Dieser Beitrag ordnet die drei Stellschrauben ein, zeigt, welche Werte sich tatsächlich auslesen lassen, und erklärt, warum ein einzelner Abruf im Browser als Beleg nicht taugt. In unserer Server-Optimierung ist diese Zwischenschicht ein fester Prüfpunkt.

Das Wichtigste in Kürze

  • Der OPcache spart die beiden teuersten Schritte jeder Anfrage: Datei lesen und zu Opcodes kompilieren. Ab Werk stehen dafür 128 MB (PHP-Handbuch) gemeinsamer Speicher und 10.000 (PHP-Handbuch) Dateiplätze bereit -- für größere Shops reicht beides häufig nicht.
  • Die Trefferquote ist die Kennzahl, die zählt. Sie lässt sich über opcache_get_status() auslesen (PHP-Handbuch); Neustarts wegen erschöpftem Speicher oder vollem Dateitisch sind das deutlichste Warnsignal.
  • JIT ist ab Werk abgeschaltet: Seit PHP 8.4 steht opcache.jit auf disable, der Puffer opcache.jit_buffer_size dagegen auf 64M; bis PHP 8.3 stand der Puffer auf 0 und JIT scheiterte schon daran (PHP-Handbuch). Für rechenintensive Schleifen bringt er viel, für typische Webanfragen wenig: Im JIT-RFC stieg der Durchsatz bei WordPress von 315 auf 326 Anfragen pro Sekunde (PHP-RFC JIT).
  • Das FPM-Prozessmodell entscheidet über das Verhalten unter Last. pm.max_children ist keine Wunschzahl, sondern das Ergebnis einer Rechnung aus nutzbarem Arbeitsspeicher und tatsächlichem Speicherbedarf eines Worker-Prozesses.
  • Worker-Sättigung sieht in der Messung aus wie ein langsames Backend, ist aber ein Warteproblem: Die Anfrage steht in der Listen-Queue, bevor eine Zeile PHP läuft. Sichtbar wird das über die FPM-Statusseite (PHP-Handbuch), nicht im Browser.

Die Schicht zwischen Webserver und Datenbank

Der Weg einer dynamischen Seite ist schnell beschrieben, aber an mehreren Stellen teuer. Der Webserver nimmt die Anfrage an und reicht sie über einen Socket an PHP-FPM weiter. Dort greift ein freier Worker-Prozess zu, lädt die Einstiegsdatei, löst Autoloader-Pfade auf, liest jede benötigte Klassendatei vom Dateisystem, parst sie, kompiliert sie zu Opcodes und führt sie aus. Erst danach entstehen Datenbankabfragen, Cache-Zugriffe und Aufrufe an externe Schnittstellen. Was der Besucher als Time to First Byte erlebt, ist die Summe all dieser Schritte -- die Metrik selbst und ihre Bestandteile behandelt der Beitrag zur Server-Antwortzeit ausführlich.

In vielen Projekten fällt die Diagnose zu früh. Ist die Antwortzeit hoch, gerät die Datenbank in Verdacht -- und tatsächlich lassen sich dort mit Indexen und dem Auflösen von N+1-Mustern große Sprünge erzielen, wie der Beitrag zur Datenbank-Query-Optimierung zeigt. Bindet ein Shop aber mehrere tausend Klassendateien pro Anfrage ein und ist der Opcode-Cache zu klein dimensioniert, entsteht ein erheblicher Teil der Wartezeit, bevor die erste Abfrage überhaupt formuliert ist. Diese Zeit taucht in keinem Slow-Query-Log auf. Sie steckt in der Laufzeit -- und sie ist meist über Konfiguration erreichbar, nicht über einen Code-Umbau.

Der praktische Nutzen dieser Unterscheidung liegt in der Reihenfolge. Wer weiß, welcher Anteil der Antwortzeit auf Warten, Kompilieren, Ausführen und I/O entfällt, muss nicht raten, welche Maßnahme sich lohnt. Und er erkennt früh, wenn eine Maßnahme gar nicht greifen kann: Ein perfekt dimensionierter Opcode-Cache verkürzt keine einzige Millisekunde Wartezeit vor einem belegten Worker, und zusätzliche Worker beschleunigen keine langsame Abfrage.

Wo die Zeit in einer PHP-Anfrage liegt

Eine PHP-Anfrage zerfällt in vier grobe Abschnitte: Warten auf einen freien Worker, Laden und Kompilieren des Codes, Ausführen der Opcodes und schließlich I/O gegen Datenbank, Cache und Fremdsysteme. Der OPcache adressiert ausschließlich den zweiten Abschnitt, JIT ausschließlich den dritten, die FPM-Konfiguration ausschließlich den ersten. Wer die Abschnitte nicht trennt, optimiert leicht an der falschen Stelle -- und wundert sich, dass eine korrekt umgesetzte Maßnahme in der Messung nichts bewegt.

OPcache: einmal kompilieren statt bei jeder Anfrage

Ohne Opcode-Cache wiederholt PHP bei jeder einzelnen Anfrage dieselbe Arbeit: Dateien vom Dateisystem lesen, den Quelltext parsen, einen Syntaxbaum aufbauen und daraus Opcodes erzeugen. Erst dann beginnt die eigentliche Ausführung. Der OPcache legt das Ergebnis dieser Kompilierung im gemeinsamen Speicher ab, sodass sämtliche Worker-Prozesse darauf zugreifen. Ab der zweiten Anfrage entfallen Lesen, Parsen und Kompilieren -- übrig bleibt die Ausführung. Bei Anwendungen mit vielen kleinen Klassendateien, also bei praktisch jedem modernen Shop und jedem Framework, ist das der wirksamste Einzelschritt in der Laufzeitschicht.

Entscheidend ist die Dimensionierung, und dabei zählen drei Werte. opcache.memory_consumption legt fest, wie viel gemeinsamer Speicher zur Verfügung steht -- ab Werk 128 MB (PHP-Handbuch). opcache.max_accelerated_files begrenzt die Zahl der Dateien im Cache, ab Werk 10.000 (PHP-Handbuch); der gesetzte Wert wird intern auf die nächstgrößere Zahl einer festen Reihe aufgerundet (PHP-Handbuch), sodass 32.531 eine sinnvollere Angabe ist als eine krumme Zahl. opcache.interned_strings_buffer reserviert Platz für mehrfach verwendete Zeichenketten, ab Werk 8 MB (PHP-Handbuch); bei vielen Klassen, Übersetzungen und Konfigurationsschlüsseln wird das schnell knapp. Reicht einer der drei Werte nicht, verwirft der Cache Einträge und kompiliert nach -- und genau dann entsteht ein Effekt, der im Betrieb wie ein sprunghaft langsames Backend aussieht.

opcache.ini
; Opcode-Cache dimensionieren statt raten
opcache.enable = 1
opcache.memory_consumption = 512
opcache.interned_strings_buffer = 32
opcache.max_accelerated_files = 32531

; Produktion: Zeitstempel nicht bei jeder Anfrage prüfen,
; dafür den Cache im Deployment gezielt zurücksetzen
opcache.validate_timestamps = 0

; JIT bleibt ohne Puffer wirkungslos - bewusst zuschalten und
; getrennt von allen anderen Änderungen messen
opcache.jit_buffer_size = 0

Ob die Dimensionierung trägt, lässt sich messen statt schätzen. Die Funktion opcache_get_status() liefert Trefferquote, belegten und freien Speicher, die Zahl der zwischengespeicherten Skripte sowie Zähler für Neustarts (PHP-Handbuch). Erfahrungsgemäß sollte die Trefferquote im laufenden Betrieb dicht an hundert Prozent liegen; deutlich niedrigere Werte bedeuten, dass PHP regelmäßig nachkompiliert. Noch aussagekräftiger sind die Neustart-Zähler: oom_restarts steht für Neustarts wegen erschöpftem Speicher, hash_restarts für einen vollen Dateitisch, manual_restarts für ausgelöste Leerungen. Steigen die ersten beiden im Betrieb, ist der Cache zu klein -- unabhängig davon, wie gut die Trefferquote im Mittel aussieht.

Wert aus opcache_get_status()Was er aussagtReaktion
opcache_hit_rateAnteil der Anfragen, die kompilierten Code vorfindenDeutlich unter hundert Prozent: Speicher und Dateiplätze prüfen
num_cached_scriptsZahl der zwischengespeicherten DateienNahe an max_accelerated_files: Grenze anheben
free_memoryFreier Anteil des gemeinsamen SpeichersDauerhaft knapp: memory_consumption erhöhen
oom_restartsNeustarts wegen erschöpftem SpeicherÜber null: Cache ist zu klein dimensioniert
hash_restartsNeustarts wegen vollem DateitischÜber null: max_accelerated_files anheben

Der Opcode-Cache ist dabei nur die unterste von mehreren Cache-Ebenen. Darüber liegen Objekt-Cache, Seiten-Cache und die Auslieferung über vorgelagerte Schichten; wie diese Ebenen ineinandergreifen und welche Anfrage überhaupt bis zu PHP durchdringt, ordnet der Beitrag zu Caching-Strategien ein. Für eine Seite, die vollständig aus einem Seiten-Cache beantwortet wird, spielt die Laufzeitschicht keine Rolle -- für jede personalisierte Seite, jeden Warenkorb und jeden Login dagegen sehr wohl.

Invalidierung nach dem Deployment

Ein Cache, der kompilierten Code hält, muss wissen, wann dieser Code veraltet ist. PHP löst das über Zeitstempel: opcache.validate_timestamps ist ab Werk aktiv, und opcache.revalidate_freq legt fest, nach wie vielen Sekunden eine Datei erneut geprüft wird -- ab Werk sind das 2 Sekunden (PHP-Handbuch). Auf Produktionssystemen wird die Prüfung häufig abgeschaltet, weil jeder Abgleich Dateisystemzugriffe kostet. Das ist eine vertretbare Entscheidung, sie verlagert aber die Verantwortung: Wer die Prüfung ausschaltet, muss den Cache nach jedem Deployment aktiv zurücksetzen, sonst läuft der alte Stand weiter.

Der Klassiker: halb alter, halb neuer Code

Wird ein Verzeichnis im laufenden Betrieb überschrieben, kann eine einzelne Anfrage Klassen aus dem alten und dem neuen Stand mischen -- ein Teil liegt noch im Cache, ein anderer wird frisch gelesen. Das Ergebnis sind sporadische Fehler, die sich nach dem nächsten Neustart in Luft auflösen und deshalb schwer zu reproduzieren sind. Robuster ist die Auslieferung in ein neues Verzeichnis, gefolgt vom Umhängen eines Symlinks und einem gezielten Zurücksetzen von Opcode- und Realpath-Cache. Wichtig dabei: Der Realpath-Cache löst Pfade auf und muss beim Symlink-Wechsel mitgedacht werden, sonst zeigen die Worker weiter auf den alten Pfad.

Bei WordPress-Installationen mit vielen Erweiterungen kommt ein zweiter Effekt hinzu. Jede Aktualisierung ändert Dateien, und bei abgeschalteter Zeitstempelprüfung wirkt die Änderung erst nach dem Zurücksetzen des Caches. Wer im Redaktionsalltag Erweiterungen aktualisiert, braucht dafür einen definierten Ablauf statt eines beherzten Neustarts. Welche weiteren Bremsen in solchen Installationen typischerweise zusammenkommen, beschreibt der Beitrag zu den häufigen WordPress-Bremsen.

Preloading: den Kern einmal beim Start laden

Seit PHP 7.4 gibt es eine Stufe über dem reinen Opcode-Cache: das Preloading (PHP-Handbuch). Über opcache.preload wird beim Start des Dienstes ein Skript ausgeführt, das eine Liste von Klassen einliest. Diese Klassen bleiben dauerhaft im gemeinsamen Speicher und stehen jeder Anfrage sofort zur Verfügung -- ohne Autoloader, ohne Dateisystemzugriff und mit bereits aufgelösten Vererbungsbeziehungen. Für Frameworks mit stabilem Kern ist das ein zusätzlicher Gewinn über den Opcode-Cache hinaus, weil selbst der Nachschlagepfad entfällt.

Der Preis ist Betriebsdisziplin. Vorgeladene Klassen sind an den Prozess gebunden: Eine Änderung an einer vorgeladenen Datei wirkt erst nach einem Neustart des FPM-Dienstes, unabhängig von der Zeitstempelprüfung. Außerdem gehört opcache.preload_user gesetzt, wenn der Dienst als root startet (PHP-Handbuch), und die Liste sollte den stabilen Kern umfassen, nicht den Anwendungscode, der sich wöchentlich ändert. In Shop-Projekten lohnt sich Preloading vor allem dort, wo der Framework-Kern groß und der Anteil kundenspezifischer Klassen überschaubar ist; für Shopware-Installationen ist das ein eigener Schritt hinter der Cache-Dimensionierung, kein Ersatz dafür.

Was JIT bringt und was nicht

Mit PHP 8.0 kam der JIT-Compiler hinzu (PHP-Handbuch). Er übersetzt häufig durchlaufene Opcode-Folgen zur Laufzeit in Maschinencode und umgeht damit die virtuelle Maschine. Das klingt nach einem großen Hebel, und in bestimmten Lastprofilen ist es einer -- allerdings nicht dort, wo die meisten Webanwendungen ihre Zeit verbringen. Wichtig zu wissen: JIT ist ab Werk nicht aktiv. Seit PHP 8.4 steht opcache.jit auf disable und opcache.jit_buffer_size auf 64M; bis PHP 8.3 war es umgekehrt, dort stand der Puffer auf 0 (PHP-Handbuch). Ohne Puffer passiert nichts, gleich wie opcache.jit gesetzt ist, und ein gesetzter Puffer allein genügt ebenso wenig, solange opcache.jit abgeschaltet bleibt. Wer JIT für eingeschaltet hält, ohne beides geprüft zu haben, misst eine Wirkung, die es nicht gibt.

Die Zahlen aus dem zugehörigen RFC zeigen den Unterschied deutlich. Auf einem synthetischen Rechen-Benchmark sank die Laufzeit von 0,320 auf 0,140 Sekunden (PHP-RFC JIT), bei einer Mandelbrot-Berechnung lag der Faktor sogar über vier (PHP-RFC JIT). Bei WordPress dagegen stieg der Durchsatz lediglich von 315 auf 326 Anfragen pro Sekunde (PHP-RFC JIT) -- rund 3,5 Prozent. Die offizielle Einordnung zum Tracing JIT in PHP 8.0 klingt ähnlich: etwa dreifache Leistung auf synthetischen Benchmarks, das Anderthalb- bis Zweifache bei bestimmten lang laufenden Anwendungen und für typische Anwendungen ein Niveau vergleichbar mit PHP 7.4 (PHP 8.0 Release Notes).

Der Grund liegt im Profil einer Webanfrage. Sie ist kurz, springt durch viele verschiedene Codepfade und wartet einen erheblichen Teil ihrer Zeit auf Datenbank, Cache und Dateisystem. JIT braucht dagegen heiße Schleifen, um seine Übersetzungskosten wieder einzuspielen. Wo solche Schleifen tatsächlich vorkommen -- Bildverarbeitung, Preis- und Rabattberechnungen über große Mengen, Import- und Exportläufe, statistische Auswertungen --, ist der Gewinn real und messbar. Im gewöhnlichen Seitenaufbau eines Shops sind sie selten.

Wo JIT trägt

Lang laufende, rechenintensive Schleifen: Bildverarbeitung, Massenberechnungen, Import- und Exportläufe, statistische Auswertungen. Der RFC misst hier Faktoren von zwei bis über vier (PHP-RFC JIT).

Wo JIT kaum wirkt

Die kurze Webanfrage mit vielen Codepfaden und viel Warten auf I/O. Bei WordPress lag der Zuwachs im RFC bei 315 gegenüber 326 Anfragen pro Sekunde (PHP-RFC JIT), also rund 3,5 Prozent.

Was JIT kostet

Der Puffer belegt Speicher, der sonst dem Opcode-Cache zugutekäme, und die Übersetzung braucht eine Aufwärmphase. Deshalb gehört JIT einzeln zugeschaltet und getrennt gemessen, nicht gemeinsam mit anderen Änderungen.

Der Kern in einem Satz

Der OPcache verkürzt den Weg zum Code, JIT beschleunigt reine Rechenarbeit, und die FPM-Konfiguration entscheidet, wie viele Anfragen gleichzeitig davon profitieren -- die drei ersetzen einander nicht, sie greifen an drei verschiedenen Abschnitten derselben Anfrage an.

Ob sich eingesparte Rechenzeit am Ende überhaupt zeigt, hängt außerdem an der Maschine darunter. Auf einem geteilten Paket mit schwankender Rechenleistung verschwindet ein Gewinn von wenigen Prozent im Rauschen der Nachbarn; auf dedizierter Kapazität wird er sichtbar. Welche Rolle das Hosting-Modell für die Antwortzeit spielt, ordnet der Beitrag zur Hosting-Wahl ein.

Das FPM-Prozessmodell: static, dynamic, ondemand

PHP-FPM verwaltet einen Vorrat an Worker-Prozessen. Wie dieser Vorrat entsteht, legt die Direktive pm fest, und sie kennt drei Werte: static, dynamic und ondemand (PHP-Handbuch). Bei static läuft eine feste Anzahl Prozesse dauerhaft. Bei dynamic startet FPM eine Grundmenge und regelt zwischen einer Unter- und einer Obergrenze freier Prozesse nach. Bei ondemand existiert zunächst kein Prozess; die Prozesse entstehen bei Bedarf und werden nach einer Leerlaufzeit wieder beendet. Alle drei Modi lösen dieselbe Aufgabe, aber sie verteilen die Kosten unterschiedlich zwischen Speicherverbrauch und Startzeit.

pm-ModusVerhaltenPasst zuPreis
staticFeste Anzahl Prozesse, dauerhaft vorgehaltenGleichmäßige, planbare Last auf einer eigenen MaschineSpeicher ist dauerhaft gebunden
dynamicGrundmenge plus Nachregelung zwischen zwei GrenzenSchwankende Last mit erkennbarem GrundrauschenStartkosten beim Nachregeln nach oben
ondemandProzesse entstehen bei Bedarf, enden nach LeerlaufViele kleine Auftritte auf einer gemeinsamen MaschineDie erste Anfrage nach einer Pause zahlt den Start

Über allen Modi steht pm.max_children -- die Obergrenze gleichzeitig arbeitender Prozesse und damit die eigentliche Kapazitätsgrenze der Anwendung. Dieser Wert ist keine Wunschzahl, sondern das Ergebnis einer Rechnung: nutzbarer Arbeitsspeicher abzüglich dessen, was Datenbank, Cache-Dienst und Betriebssystem brauchen, geteilt durch den tatsächlichen Speicherbedarf eines Worker-Prozesses unter Last. Wird der Bedarf zu niedrig angesetzt, beginnt das Betriebssystem unter Last auszulagern -- und ein auslagernder Server antwortet langsamer als einer mit einer knapp bemessenen Zahl an Workern.

Ergänzend begrenzt pm.max_requests, wie viele Anfragen ein Worker bearbeitet, bevor er sich beendet und neu startet; ab Werk steht der Wert auf 0, also unbegrenzt (PHP-Handbuch). Ein endlicher Wert ist eine pragmatische Absicherung gegen langsam wachsenden Speicherverbrauch in Erweiterungen. Er behebt die Ursache nicht, hält den Prozessvorrat aber gesund. Vergleichbares Wachstum gibt es auf der Browserseite, wo es sich in langen Sitzungen als zunehmende Trägheit äußert -- beschrieben im Beitrag zu Speicherlecks im Frontend.

www.conf
[www]
; (nutzbarer Arbeitsspeicher - Reserve) / Speicherbedarf je Prozess
pm = dynamic
pm.max_children = 40
pm.start_servers = 10
pm.min_spare_servers = 6
pm.max_spare_servers = 14

; Prozess nach 500 Anfragen erneuern - Absicherung, keine Ursachenbehebung
pm.max_requests = 500

; Sättigung sichtbar machen
pm.status_path = /fpm-status
slowlog = /var/log/php-fpm/slow.log
request_slowlog_timeout = 5s

Worker-Sättigung: die versteckte Ursache

Der unangenehmste Zustand ist nicht der langsame Code, sondern der ausgebuchte Prozessvorrat. Sind alle Worker belegt, wandern neue Anfragen in die Listen-Queue des Sockets und warten dort, ohne dass eine Zeile PHP läuft. Für den Besucher sieht das aus wie ein langsamer Server; im Anwendungs-Profiling sieht es nach nichts aus, weil die Wartezeit vor dem gemessenen Bereich liegt. Die Queue hat ihrerseits eine Obergrenze: listen.backlog steht ab Werk auf 511 (PHP-Handbuch). Ist auch sie voll, werden Verbindungen abgewiesen -- aus einer langsamen Seite wird eine fehlerhafte.

Sichtbar wird der Zustand über die Statusseite von FPM. Mit pm.status_path liefert der Dienst unter anderem die Zahl aktiver und wartender Prozesse, die aktuelle sowie die höchste je erreichte Länge der Listen-Queue und den Zähler max children reached (PHP-Handbuch). Steht dieser Zähler über null, war der Prozessvorrat mindestens einmal ausgeschöpft -- der eindeutigste Hinweis darauf, dass nicht der Code, sondern die Kapazität das Problem ist. Ergänzend zeigt das Slowlog mit request_slowlog_timeout, welche Skripte eine gesetzte Schwelle überschreiten, samt Aufrufkette (PHP-Handbuch); ab Werk ist es abgeschaltet und damit in den meisten Installationen ungenutzt.

  • Der Zähler max children reached steht über null -- der Vorrat war bereits ausgeschöpft
  • Listen-Queue und ihr Höchstwert sind dauerhaft von null verschieden
  • Die Antwortzeit steigt ab einer bestimmten Parallelität sprunghaft, während einzelne Datenbankabfragen gleich schnell bleiben
  • Die Auslastung der Rechenkerne ist niedrig, obwohl Anfragen warten -- ein Zeichen für zu wenige Worker, nicht für zu wenig Rechenleistung
  • Ein Neustart des Dienstes bringt kurzfristig Ruhe, und der Zustand kehrt unter Last zurück

Diese Grenze lässt sich vor der Hochsaison gezielt ausloten, statt sie im Echtbetrieb zu finden; wie ein solcher Versuchsaufbau aussieht, beschreibt der Beitrag zum Lasttest vor Traffic-Spitzen. Dabei lohnt der Blick auf die gesamte Anfragemenge: Ein wachsender Teil der Zugriffe stammt nicht von Kunden, sondern von automatisierten Abrufen, die dieselben Worker belegen -- eine Entwicklung, die der Beitrag zur Crawler-Last auf dem Server einordnet.

Vorher und nachher realistisch messen

Die häufigste Fehlmessung in dieser Schicht ist der Einzelabruf. Man ändert einen Wert in der Konfiguration, lädt die Seite im Browser neu, sieht eine bessere Zahl und schreibt sie in den Bericht. Das Problem: Der erste Abruf nach einem Neustart trifft einen leeren Opcode-Cache, der zweite einen warmen, und keiner von beiden sagt etwas über das Verhalten bei zwanzig gleichzeitigen Nutzern. Belastbar wird die Messung erst mit drei Zutaten -- einem definierten Aufwärmzustand, wiederholter Last mit realistischer Parallelität und einer Auswertung über Perzentile statt über Mittelwerte.

Der Mittelwert verdeckt genau den Effekt, um den es hier geht. Wenn neun von zehn Anfragen einen freien Worker finden und die zehnte wartet, bleibt der Durchschnitt ruhig, während das 95. Perzentil deutlich ausschlägt. Deshalb gehören der Wert bei 75 und bei 95 Prozent in jeden Vorher-Nachher-Vergleich, zusammen mit der Angabe, unter wie vielen gleichzeitigen Anfragen gemessen wurde. Wie sich Labor- und Feldmessung dabei ergänzen und warum beide gebraucht werden, ordnet der Beitrag zu Felddaten und Labordaten ein.

  1. Ausgangszustand festhalten: Trefferquote, belegter und freier Speicher, Zahl der Skripte, Neustart-Zähler und die Werte der FPM-Statusseite protokollieren
  2. Speicherbedarf eines Workers unter echter Last messen, nicht im Leerlauf schätzen
  3. pm.max_children aus nutzbarem Arbeitsspeicher und diesem Bedarf berechnen und den pm-Modus zum Lastprofil passend wählen
  4. Den OPcache so dimensionieren, dass oom_restarts und hash_restarts im Betrieb nicht mehr steigen
  5. Deployment-Ablauf absichern: Cache zurücksetzen, Symlink umhängen, Realpath-Cache mitdenken
  6. Preloading und JIT einzeln zuschalten und jeweils getrennt messen -- zwei Änderungen gleichzeitig sind nicht auswertbar
  7. Unter realistischer Parallelität wiederholt messen und die Werte bei 75 und 95 Prozent vergleichen, nicht den Mittelwert

Damit die Verbesserung bleibt, gehört sie in die laufende Kontrolle. Ein Schwellwert für die Antwortzeit im Auslieferungsprozess meldet Rückschritte, bevor sie im Feld ankommen -- eine Erweiterung, die sich in bestehende Abläufe einfügen lässt und im Beitrag zum Performance-Budget in der CI beschrieben ist. Ohne diese Absicherung wandert die Konfiguration erfahrungsgemäß mit dem nächsten Serverumzug oder der nächsten Version wieder auf Standardwerte zurück.

Genau diese Reihenfolge arbeiten wir in einem Projekt ab. In der Performance-Analyse lesen wir zuerst die Laufzeitwerte aus, trennen Wartezeit, Kompilierzeit und Ausführungszeit voneinander und benennen, welcher Anteil überhaupt zu holen ist. Danach folgen die Eingriffe in der Reihenfolge ihres Nutzens: Cache-Dimensionierung, Deployment-Invalidierung, Prozessmodell, erst danach Preloading und -- sofern das Lastprofil es hergibt -- JIT. Für WordPress-Installationen und Shopware-Shops ist das ein eingespielter Ablauf, weil sich beide Systeme in der Laufzeitschicht ähnlich verhalten.

Wirtschaftlich lohnt sich der Aufwand dort, wo Wartezeit direkt auf Abschlüsse durchschlägt. Eine Verbesserung der mobilen Ladezeit um 0,1 Sekunden hob die Conversion-Rate im Einzelhandel um durchschnittlich 8,4 Prozent (Deloitte Ireland 2020), und über alle Branchen hinweg brechen im Mittel 70,22 Prozent (Baymard Institute) der Warenkörbe ab -- Wartezeit ist einer der Gründe, die sich technisch beeinflussen lassen. Wie sich Ladezeit und Umsatz insgesamt zueinander verhalten, führt der Beitrag zu Ladezeit, Conversion-Rate und Umsatz aus.

Ein Server wirkt selten gleichmäßig langsam. Er ist meist ausreichend schnell, bis der Prozessvorrat ausgeht -- und ab diesem Punkt wird jede weitere Anfrage teurer als die vorige. Wer nur den Einzelabruf misst, sieht diesen Punkt erfahrungsgemäß erst dann, wenn die Kunden ihn bereits erlebt haben.

Projekterfahrung aus Server-Optimierungen

Der Aufwand für diese Schicht bleibt überschaubar, weil fast alles Konfiguration ist und kein Code-Umbau. Ein sauber dimensionierter Opcode-Cache, ein Deployment, das ihn zuverlässig zurücksetzt, ein Prozessmodell, das zum Lastprofil passt, und eine Messung, die Parallelität berücksichtigt: Damit ist der größte Teil gehoben, bevor exotischere Mittel überhaupt in Frage kommen. Bleibt danach noch Antwortzeit übrig, greifen die Ebenen darüber und darunter -- die TTFB-Optimierung an der Auslieferung und die Datenbank an der Abfrage. Welche Schritte dazugehören und wie ein solches Projekt abläuft, zeigt die Übersicht der Performance-Leistungen.

Quellen und Studien

Dieser Artikel basiert auf Daten von PHP-Handbuch, PHP-RFC JIT, web.dev und Deloitte Ireland sowie ergänzend von PHP 8.0 Release Notes und Baymard Institute. Die genannten Zahlen beziehen sich auf den Stand der jeweiligen Veröffentlichung.

Verwandte Artikel