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.
Inhaltsverzeichnis
- 1. Was OPcache Preloading tatsächlich löst
- 2. Grundkonfiguration: opcache.preload aktivieren
- 3. Ein Preload-Skript richtig aufbauen
- 4. Klassenreihenfolge und Vererbungsauflösung
- 5. Zusammenspiel mit Composer-Autoloading
- 6. Preloading im Deployment-Workflow
- 7. Typische Stolperfallen in Produktion
- 8. Preloading überwachen und verifizieren
- 9. Preloading im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
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?
2Wo wird opcache.preload konfiguriert?
3Warum class_exists statt Instanziierung?
4Reicht ein Reload nach Deploy?
5Muss die gesamte Codebasis preloaden?
6Wie prüfe ich, ob Preloading aktiv ist?
7Funktioniert Preloading mit Composer-Autoloading?
8Was, wenn die Elternklasse noch unbekannt ist?
9Lohnt sich Preloading für kleine Anwendungen?
10Wie sollte der Deployment-Workflow aussehen?
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