OPcache Preloading in PHP 8 richtig konfigurieren
AI generated
<?php
8.4
PHP · OPcache · Performance
OPcache Preloading in PHP 8 richtig konfigurieren
Klassen dauerhaft im Speicher halten statt bei jedem Request neu zu laden

OPcache Preloading lädt ausgewählte Klassen und Funktionen einmalig beim Start eines PHP-FPM-Workers in einen dauerhaften Speicherbereich, sodass sie für jeden folgenden Request sofort verfügbar sind. Richtig konfiguriert spart das spürbar Zeit für Autoloading, Vererbungsauflösung und Interface-Checks, falsch konfiguriert bringt es nichts oder erzeugt schwer nachvollziehbare Fehler nach jedem Deploy.

15 Min. Lesezeit opcache.preload · Preload-Skript · PHP-FPM PHP 8.x

1. Was OPcache Preloading tatsächlich löst

OPcache selbst löst bereits das Problem, dass PHP-Dateien bei jedem Request neu geparst und in Opcodes übersetzt werden müssen: Der kompilierte Bytecode landet im Shared Memory und wird beim nächsten Request wiederverwendet. OPcache Preloading geht einen Schritt weiter und löst ein zweites, subtileres Problem, das reines Opcode-Caching nicht adressiert: Auch wenn der Bytecode einer Klasse bereits im Cache liegt, muss PHP bei jedem Request trotzdem prüfen, ob die Klasse instanziierbar ist, ihre Elternklassen und Interfaces auflösen und die Vererbungshierarchie neu verlinken.

Genau diese Auflösung entfällt mit aktiviertem OPcache Preloading vollständig. Beim Start eines PHP-FPM-Workers, also genau einmal pro Prozess und nicht pro Request, lädt ein Preload-Skript ausgewählte Klassen, bindet ihre Vererbungshierarchie fertig auf und legt das Ergebnis in einem permanenten, worker-übergreifenden Speicherbereich ab. Jeder nachfolgende Request in diesem Worker sieht die Klasse bereits vollständig aufgelöst vor sich, ohne Autoloading, ohne Interface-Prüfung, ohne erneute Verlinkung der Elternklasse.

Der Effekt ist am größten bei Anwendungen mit vielen Klassen und tiefen Vererbungshierarchien, etwa Frameworks wie Symfony oder Systeme wie Magento, die hunderte bis tausende Klassen pro Request potenziell laden könnten. Bei kleinen Skripten mit wenigen Dutzend Klassen ist der Effekt von OPcache Preloading dagegen kaum messbar, weil die Auflösungskosten pro Request ohnehin gering sind. Wer OPcache Preloading einführt, sollte deshalb zuerst prüfen, ob die eigene Anwendung überhaupt in der Größenordnung liegt, in der sich der zusätzliche Konfigurationsaufwand lohnt.

2. Grundkonfiguration: opcache.preload aktivieren

Die zentrale Konfigurationsdirektive heißt opcache.preload und zeigt auf den absoluten Pfad eines PHP-Skripts, das beim Start jedes PHP-FPM- oder CLI-Server-Prozesses einmalig ausgeführt wird. Wichtig: Diese Direktive funktioniert ausschließlich, wenn PHP im FPM- oder CLI-Server-Modus läuft, nicht im klassischen CGI-Modus, und sie muss in der Haupt-php.ini oder einer geladenen .ini-Datei stehen, nicht in einer .htaccess oder Runtime-Konfiguration, weil Preloading vor jedem Request bereits abgeschlossen sein muss.

Eine zweite, oft übersehene Direktive ist opcache.preload_user. Wenn PHP-FPM als root gestartet wird, um anschließend die Worker-Prozesse unter einem unprivilegierten Benutzer laufen zu lassen, muss das Preload-Skript unter genau diesem Benutzer ausgeführt werden, sonst schlägt der Start mit einer Fehlermeldung fehl oder das Preloading wird stillschweigend unter falschen Dateirechten ausgeführt. In den meisten Docker-basierten Setups, in denen PHP-FPM ohnehin direkt unter einem nicht-root-Benutzer läuft, kann opcache.preload_user entfallen.


; php.ini or /usr/local/etc/php/conf.d/opcache-preload.ini

; Path to the preload script, executed once per worker start
opcache.preload=/var/www/html/preload.php

; Only needed if php-fpm master process runs as root
; opcache.preload_user=www-data

; Preloading requires opcache to be enabled
opcache.enable=1
opcache.enable_cli=0

; Recommended baseline for production preloading setups
opcache.validate_timestamps=0
opcache.memory_consumption=256
opcache.max_accelerated_files=20000

Bemerkenswert ist die Kombination mit opcache.validate_timestamps=0. Diese Direktive deaktiviert die automatische Prüfung, ob eine Quelldatei seit dem letzten Caching geändert wurde. In Kombination mit OPcache Preloading ist das nahezu Pflicht, weil preloadete Klassen ohnehin nicht mehr aus einer Änderung der Quelldatei zur Laufzeit profitieren würden, ein PHP-FPM-Neustart nach jedem Deploy also ohnehin notwendig ist. Wer validate_timestamps aktiviert lässt, riskiert widersprüchliche Zustände zwischen preloadeten und regulär gecachten Klassen.

3. Ein Preload-Skript richtig aufbauen

Ein Preload-Skript ist zunächst nichts anderes als eine gewöhnliche PHP-Datei, die beim Start eines Workers ausgeführt wird. Der zentrale Befehl darin ist opcache_compile_file(), der eine Datei kompiliert und ihren Bytecode im Preload-Speicher ablegt, ohne die Datei tatsächlich auszuführen. Für Klassen mit Vererbungsketten reicht das reine Kompilieren jedoch nicht aus: Damit PHP die Elternklasse und Interfaces korrekt auflösen kann, muss die Klasse zusätzlich instanziiert oder zumindest über class_exists() geladen werden, damit der Autoloader greift und die komplette Kette bekannt ist.

In der Praxis besteht ein robustes Preload-Skript deshalb aus drei Teilen: Zuerst wird der Composer-Autoloader eingebunden, damit Klassennamen automatisch aufgelöst werden können. Danach werden gezielt die wichtigsten Klassen der Anwendung geladen, meist über eine Liste von Verzeichnissen, die per glob() oder RecursiveDirectoryIterator durchsucht werden. Am Ende protokolliert das Skript, wie viele Klassen erfolgreich geladen wurden, damit Fehler beim Worker-Start sichtbar werden, statt in den PHP-FPM-Logs unterzugehen.


declare(strict_types=1);

// preload.php — executed once per PHP-FPM worker start
require __DIR__ . '/vendor/autoload.php';

$preloadedCount = 0;
$failedClasses = [];

/**
 * @param iterable<string> $classNames
 */
function preloadClasses(iterable $classNames, array &$failed): int
{
    $count = 0;
    foreach ($classNames as $className) {
        try {
            // class_exists triggers the autoloader and resolves the full
            // inheritance chain, not just this one file
            if (class_exists($className) || interface_exists($className) || trait_exists($className)) {
                $count++;
            }
        } catch (\Throwable $e) {
            $failed[] = $className . ': ' . $e->getMessage();
        }
    }
    return $count;
}

// Only preload the hot path: value objects, core services, entities
$classMap = require __DIR__ . '/vendor/composer/autoload_classmap.php';
$hotPathPrefixes = ['App\\Domain\\', 'App\\Service\\', 'App\\Repository\\'];

$candidates = array_filter(
    array_keys($classMap),
    static fn (string $class): bool => array_any(
        $hotPathPrefixes,
        static fn (string $prefix) => str_starts_with($class, $prefix)
    )
);

$preloadedCount = preloadClasses($candidates, $failedClasses);

if ($failedClasses !== []) {
    error_log(sprintf('[preload] %d classes failed: %s', count($failedClasses), implode(', ', $failedClasses)));
}

error_log(sprintf('[preload] %d classes preloaded successfully', $preloadedCount));

Der entscheidende Designfehler, den viele erste Preload-Skripte machen: Sie versuchen, die gesamte Codebasis zu preloaden, statt sich auf den tatsächlichen Hot Path zu beschränken. Das kostet nicht nur zusätzlichen Speicher im Preload-Bereich, sondern erhöht auch das Risiko, dass selten benutzte Klassen mit fehlenden Abhängigkeiten den kompletten Worker-Start zum Absturz bringen. Eine gezielte Auswahl der am häufigsten instanziierten Klassen bringt in der Regel den größten Effekt bei überschaubarem Risiko.

4. Klassenreihenfolge und Vererbungsauflösung

Eine der am häufigsten unterschätzten Regeln von OPcache Preloading betrifft die Reihenfolge, in der Klassen geladen werden. Erbt eine Klasse von einer Elternklasse, die noch nicht bekannt ist, schlägt das Preloading dieser Klasse fehl, und PHP protokolliert eine Warnung, dass die Klasse nicht preloadet werden konnte. Verwendet man class_exists() statt direktem opcache_compile_file(), löst der Autoloader dieses Problem in den meisten Fällen automatisch, weil er bei Bedarf die Elternklasse selbst nachlädt, bevor die Kindklasse aufgelöst wird.

Kritischer wird es bei Interfaces und Traits, die von mehreren Klassen gemeinsam genutzt werden. Wird ein Interface bereits während des Preloadings einer ersten Klasse aufgelöst, ist es für alle nachfolgenden Klassen im selben Preload-Lauf bereits verfügbar und muss nicht erneut aufgelöst werden. Diese Wiederverwendung innerhalb des Preload-Laufs selbst ist ein Grund, warum die Reihenfolge in der Praxis meist weniger kritisch ist, als es zunächst scheint, solange konsequent class_exists() statt der direkten Kompilierungsfunktion verwendet wird.

Ein Sonderfall betrifft abstrakte Klassen und Klassen mit ungetypten oder generischen Konstruktor-Parametern, die intern auf noch nicht geladene Klassen verweisen. Hier hilft es, das Preload-Skript zunächst mit einer kleinen, bewusst gewählten Testmenge an Klassen laufen zu lassen und die Fehlerausgabe in den PHP-FPM-Logs zu beobachten, bevor die vollständige Liste aktiviert wird. Ein iteratives Vorgehen verhindert, dass ein einziger fehlerhafter Preload-Eintrag den gesamten Worker-Start blockiert.

5. Zusammenspiel mit Composer-Autoloading

OPcache Preloading und Composer-Autoloading ergänzen sich, lösen aber unterschiedliche Probleme zu unterschiedlichen Zeitpunkten. Composer-Autoloading entscheidet zur Laufzeit, welche Datei für welchen Klassennamen geladen werden muss, basierend auf der PSR-4-Zuordnung in der autoload_psr4.php. OPcache Preloading nutzt diese Zuordnung genau einmal beim Worker-Start, um die Klassen physisch zu laden, danach spielt der Composer-Autoloader für preloadete Klassen keine Rolle mehr, weil sie bereits vollständig im Speicher liegen.

Ein praktischer Vorteil dieser Kombination: Für preloadete Klassen entfällt bei jedem Request nicht nur die Kompilierung, sondern auch der komplette PSR-4-Auflösungspfad inklusive Dateisystemzugriff, den der Composer-Autoloader normalerweise durchläuft, um die richtige Datei zu finden. Bei Anwendungen mit tausenden Klassen und entsprechend großer autoload_classmap.php kann allein das Wegfallen dieses Lookups pro Request spürbar sein, insbesondere wenn der Realpath Cache knapp bemessen ist und häufig Dateisystemzugriffe für die Pfadauflösung nötig wären.

Wichtig zu verstehen: Preloadete Klassen sind für den gesamten Lebenszyklus des Workers fixiert. Ein Autoloader-Aufruf für eine bereits preloadete Klasse gibt sofort true zurück, ohne die Datei erneut zu laden, weil PHP intern erkennt, dass die Klasse bereits als Teil des Preload-Speichers existiert. Für neue Klassen, die nicht Teil des Preload-Skripts waren, funktioniert der Composer-Autoloader unverändert wie gewohnt und lädt sie bei Bedarf ganz normal nach.

6. Preloading im Deployment-Workflow

OPcache Preloading verändert den Deployment-Workflow an einer entscheidenden Stelle: Weil das Preload-Skript nur einmal beim Start eines Workers läuft, wird ein Code-Deploy erst nach einem vollständigen Neustart von PHP-FPM sichtbar, ein einfaches opcache_reset() oder ein Graceful Reload reicht nicht mehr aus. Wer bisher mit systemctl reload php-fpm gearbeitet hat, weil das für reines Opcode-Caching genügte, muss beim Einsatz von OPcache Preloading auf einen echten Neustart der Worker-Prozesse umstellen, etwa systemctl restart php-fpm.

Für Deployment-Pipelines mit mehreren Application-Servern bedeutet das: Ein Rolling Restart, bei dem Server nacheinander neugestartet werden, während der Load Balancer den Traffic auf die verbleibenden Server umleitet, wird nahezu Pflicht. Ein gleichzeitiger harter Neustart aller Worker würde für die Dauer des Preloadings zu Verbindungsfehlern führen, weil der neue Prozess erst nach abgeschlossenem Preload-Lauf Requests annehmen kann. Bei einem typischen Preload-Skript mit wenigen hundert Klassen dauert dieser Vorgang üblicherweise nur wenige hundert Millisekunden, ist bei tausenden Klassen aber durchaus im Sekundenbereich messbar.

Ein zweiter wichtiger Punkt für den Deployment-Workflow: Das Preload-Skript selbst muss Teil des Deployment-Artefakts sein und synchron mit dem restlichen Code aktualisiert werden. Ein veraltetes Preload-Skript, das auf inzwischen umbenannte oder gelöschte Klassen verweist, erzeugt beim nächsten Worker-Start Fehler, die je nach Konfiguration den Start komplett verhindern können. Ein automatisierter Smoke-Test direkt nach dem Neustart, der prüft, ob PHP-FPM tatsächlich wieder Requests annimmt, gehört deshalb in jede Pipeline, die OPcache Preloading produktiv einsetzt.

7. Typische Stolperfallen in Produktion

Die häufigste Stolperfalle ist das bereits erwähnte Vergessen von opcache.validate_timestamps=0. Bleibt diese Direktive aktiv, prüft PHP bei jedem Request weiterhin, ob sich reguläre, nicht preloadete Dateien geändert haben, während preloadete Klassen davon unberührt bleiben. Das führt zu inkonsistentem Verhalten: Änderungen an nicht preloadeten Dateien werden sofort wirksam, Änderungen an preloadeten Klassen erst nach einem Neustart, was in der Entwicklung zu verwirrenden Debugging-Sitzungen führt, wenn ein Fix scheinbar nicht ankommt.

Eine zweite häufige Stolperfalle betrifft Klassen, die Umgebungsvariablen oder Konfigurationswerte im Konstruktor oder in statischen Initialisierern auswerten. Wird eine solche Klasse während des Preloadings instanziiert, um ihre Vererbungshierarchie aufzulösen, kann sie versehentlich mit den Umgebungsvariablen des Preload-Kontexts initialisiert werden, statt mit denen des tatsächlichen Requests. Deshalb ist es sicherer, Klassen ausschließlich über class_exists() zu laden statt sie tatsächlich zu instanziieren, weil class_exists() lediglich die Deklaration auflöst, ohne den Konstruktor auszuführen.


declare(strict_types=1);

// WRONG: instantiating during preload can capture stale environment state
// $service = new PaymentGatewayClient($_ENV['GATEWAY_URL']);

// RIGHT: class_exists resolves the declaration and inheritance chain
// without running the constructor or any static initializer
if (!class_exists(PaymentGatewayClient::class)) {
    error_log('[preload] PaymentGatewayClient could not be resolved');
}

// Verify how many classes actually made it into the preload memory
$preloadedClasses = array_filter(
    get_declared_classes(),
    static fn (string $class): bool => str_starts_with($class, 'App\\')
);
error_log(sprintf('[preload] verification: %d App\\ classes visible', count($preloadedClasses)));

Eine dritte, seltenere aber folgenschwere Stolperfalle: Preloadete Klassen belegen den kompletten konfigurierten Speicherbereich über opcache.memory_consumption dauerhaft, unabhängig davon, wie viele Requests tatsächlich diese Klassen benutzen. Ist der konfigurierte Speicher zu knapp bemessen, kann OPcache in einen Zustand geraten, in dem regulär gecachte Dateien laufend aus dem Cache verdrängt werden, weil der Preload-Anteil bereits einen Großteil des verfügbaren Speichers belegt. Ein Blick in opcache_get_status() zeigt, wie viel Speicher tatsächlich vom Preloading beansprucht wird.

8. Preloading überwachen und verifizieren

Ob OPcache Preloading tatsächlich greift, lässt sich mit der Funktion opcache_get_status() prüfen, die unter dem Schlüssel preload_statistics Informationen darüber liefert, wie viele Skripte, Funktionen und Klassen erfolgreich preloadet wurden. Fehlt dieser Schlüssel komplett oder zeigt eine Zahl nahe null, obwohl das Preload-Skript deutlich mehr Klassen abdecken sollte, ist die Konfiguration fehlerhaft, meist weil opcache.preload auf einen falschen Pfad zeigt oder das Skript beim Start einen stillen Fehler wirft.

Für den produktiven Betrieb empfiehlt sich ein einfacher Health-Check-Endpunkt, der opcache_get_status() abfragt und die Zahl der preloadeten Klassen gegen einen erwarteten Mindestwert prüft. Ein Monitoring-System kann diesen Endpunkt regelmäßig abfragen und Alarm schlagen, wenn nach einem Deploy die Anzahl preloadeter Klassen unerwartet einbricht, was meist auf ein fehlerhaftes Preload-Skript oder eine übersehene Exception während des Worker-Starts hindeutet.

Ergänzend liefert opcache_get_status() auch Speicherstatistiken unter memory_usage, aufgeteilt in used_memory, free_memory und wasted_memory. Ein kontinuierlich hoher Wert bei wasted_memory nach mehreren Deploys ohne vollständigen PHP-FPM-Neustart deutet darauf hin, dass alte, durch Preloading fixierte Klassenversionen noch immer Speicher belegen, während neue Versionen daneben existieren, ein klares Signal, dass der Restart-Workflow in der Pipeline nicht sauber greift.

9. Preloading im direkten Vergleich

Ein direkter Vergleich zeigt, wie sich reines Opcode-Caching und OPcache Preloading in den entscheidenden Eigenschaften unterscheiden.

Eigenschaft Reines Opcode-Caching OPcache Preloading Konsequenz
Wann geladen Bei jedem ersten Zugriff pro Worker Einmalig beim Worker-Start Kein Ladeaufwand pro Request
Vererbung auflösen Bei jedem Request erneut Einmal, dauerhaft verlinkt Kein Interface-Check pro Request
Deploy-Sichtbarkeit Nach Reload sichtbar Nur nach vollem Neustart Rolling Restart notwendig
Speicherbelegung Kann verdrängt werden Dauerhaft fixiert Speichergröße bewusst planen
Größter Effekt bei Jeder Codebasis Vielen Klassen, tiefer Vererbung Kleine Skripte profitieren kaum

Der Vergleich macht deutlich: OPcache Preloading ist keine Ergänzung, die man einfach dazuschaltet, sondern eine Betriebsänderung mit direkten Auswirkungen auf Deployment und Monitoring. Wer diese Konsequenzen kennt, bevor opcache.preload in Produktion aktiviert wird, vermeidet die häufigsten Ausfälle, die typischerweise direkt nach der ersten Aktivierung auftreten.

10. Zusammenfassung

OPcache Preloading in PHP 8 richtig konfigurieren bedeutet mehr als nur opcache.preload in der php.ini zu setzen. Ein robustes Preload-Skript lädt gezielt den Hot Path der Anwendung über class_exists(), nicht über direkte Instanziierung, um Nebeneffekte durch Konstruktoren zu vermeiden. opcache.validate_timestamps=0 gehört zwingend dazu, weil preloadete Klassen ohnehin nur durch einen vollständigen Neustart aktualisiert werden.

Der Deployment-Workflow muss auf echte Neustarts statt reiner Reloads umgestellt werden, idealerweise als Rolling Restart über mehrere Server hinweg. Monitoring über opcache_get_status() zeigt zuverlässig, ob das Preloading tatsächlich greift und wie viel Speicher dauerhaft dafür reserviert ist. Wer diese Punkte beachtet, bekommt aus OPcache Preloading einen echten, messbaren Performance-Gewinn für Anwendungen mit vielen Klassen, statt einer Konfigurationszeile, die in der Praxis wirkungslos bleibt.

OPcache Preloading, das Wichtigste auf einen Blick

opcache.preload richtig setzen

Pfad zum Preload-Skript in php.ini, nur im FPM- oder CLI-Server-Modus wirksam, kombiniert mit opcache.validate_timestamps=0.

class_exists statt Instanziierung

Löst Vererbung und Interfaces auf, ohne Konstruktoren auszuführen und ohne Umgebungszustand versehentlich einzufrieren.

Deploy erfordert Neustart

Preloadete Klassen aktualisieren sich nur bei vollständigem PHP-FPM-Neustart, ein Rolling Restart verhindert Ausfallzeiten.

Monitoring über opcache_get_status

preload_statistics zeigt die tatsächliche Anzahl preloadeter Klassen, wasted_memory zeigt veraltete Versionen nach fehlerhaftem Restart.

11. FAQ: OPcache Preloading in PHP 8 konfigurieren

1Was ist der Unterschied zwischen OPcache und OPcache Preloading?
OPcache cacht Bytecode, muss Vererbung aber bei jedem Request neu auflösen. Preloading löst das einmalig beim Worker-Start und hält Klassen dauerhaft verlinkt im Speicher.
2Wo wird opcache.preload konfiguriert?
In der php.ini, nicht in .htaccess oder zur Laufzeit. Zeigt auf ein Preload-Skript und wirkt nur im FPM- oder CLI-Server-Modus.
3Warum class_exists statt Instanziierung?
class_exists löst Deklaration und Vererbung auf, ohne den Konstruktor auszuführen. Instanziierung kann Umgebungswerte des Preload-Kontexts einfrieren.
4Reicht ein Reload nach Deploy?
Nein, preloadete Klassen brauchen einen vollständigen Neustart der Worker. Ein Graceful Reload reicht nicht, das Preload-Skript läuft nur beim Prozessstart.
5Muss die gesamte Codebasis preloaden?
Nein, eine gezielte Auswahl des Hot Path reicht. Vollständiges Preloading kostet mehr Speicher und erhöht das Risiko blockierender Fehler beim Worker-Start.
6Wie prüfe ich, ob Preloading aktiv ist?
opcache_get_status() liefert unter preload_statistics die Anzahl preloadeter Klassen. Ein Wert von null oder deutlich zu niedrig zeigt einen Konfigurationsfehler.
7Funktioniert Preloading mit Composer-Autoloading?
Ja, der Composer-Autoloader wird im Preload-Skript eingebunden. Nach dem Preloading spielt er für diese Klassen keine Rolle mehr, sie liegen bereits vollständig im Speicher.
8Was, wenn die Elternklasse noch unbekannt ist?
Das Preloading dieser Klasse schlägt fehl und wird protokolliert. Mit class_exists löst der Autoloader die Elternklasse meist automatisch vorher auf.
9Lohnt sich Preloading für kleine Anwendungen?
Kaum. Der Effekt ist am größten bei vielen Klassen und tiefer Vererbung. Bei wenigen Dutzend Klassen sind die Auflösungskosten pro Request gering.
10Wie sollte der Deployment-Workflow aussehen?
Rolling Restart über mehrere Server, Load Balancer leitet Traffic um, während Worker neu starten. Ein Smoke-Test danach prüft, ob PHP-FPM wieder Requests annimmt.

Mironsoft

PHP Performance-Tuning, OPcache-Konfiguration und Deployment-Automatisierung

OPcache Preloading zuverlässig in eurem Setup einführen?

Wir analysieren eure Klassenstruktur, bauen ein robustes Preload-Skript und passen den Deployment-Workflow so an, dass Rolling Restarts ohne Ausfallzeiten laufen.

Preload-Skript-Erstellung

Gezielte Auswahl des Hot Path, robuste Fehlerbehandlung und Verifikation über opcache_get_status

OPcache-Tuning

Speicherkonfiguration, validate_timestamps und Zusammenspiel mit Realpath Cache aufeinander abstimmen

Deployment-Pipeline

Rolling Restarts, Smoke-Tests und Monitoring für Preloading-Setups in CI/CD-Pipelines integrieren