Symfony OPcache Preloading: Produktions-Performance ohne Kaltstart
AI generated
SF
{ }
Symfony · OPcache · PHP-FPM · Deployment
Symfony OPcache Preloading
Produktions-Performance ohne Kaltstart

Jede Symfony-Anfrage kompiliert ohne Preloading dieselben tausend Klassen erneut aus dem OPcache-Bytecode. Mit opcache.preload und einem sauberen preload.php landen Container, Router und Kernel-Klassen dauerhaft im Speicher jedes PHP-FPM-Workers, bevor die erste Anfrage überhaupt ankommt.

18 Min. Lesezeit opcache.preload · preload.php · PHP-FPM · Docker Symfony 7.x · PHP 8.3+

1. Was OPcache Preloading wirklich löst

Symfony OPcache Preloading löst ein Problem, das viele Betreiber nie direkt sehen, weil es sich in jeder einzelnen Anfrage versteckt. Ohne Preloading lädt PHP zwar den kompilierten Bytecode einer Klasse aus dem OPcache, muss aber bei jeder Anfrage erneut prüfen, ob die Datei geändert wurde, den Klassenbaum auflösen und Vererbungsbeziehungen zwischen Interfaces, Traits und Basisklassen neu verknüpfen. Bei einem Symfony-Projekt mit mehreren tausend Klassen aus Vendor-Bundles, generiertem Container-Code und eigenen Services summiert sich dieser Overhead auf jeder Anfrage zu einer messbaren Latenz.

Preloading verändert dieses Modell fundamental. Ein einziges Skript lädt beim Start jedes PHP-FPM-Workers eine definierte Liste von Klassen vollständig in den geteilten Opcache-Speicher, kompiliert sie einmalig und verknüpft alle Vererbungsbeziehungen sofort. Danach ist die komplette Klassenhierarchie für die Lebensdauer des Workers permanent verfügbar, ganz ohne dass eine einzelne Anfrage diesen Aufwand noch einmal tragen muss. Für Symfony-Anwendungen mit hohem Traffic ist das einer der wenigen Hebel, die reale Millisekunden pro Request einsparen, ohne dass Anwendungscode angepasst werden muss.

2. Wie Preloading unter der Haube funktioniert

Die PHP-Konfigurationsdirektive opcache.preload zeigt auf ein PHP-Skript, das PHP beim Start jedes Workers exakt einmal ausführt. In diesem Skript ruft man typischerweise opcache_compile_file() für jede relevante Datei auf, oder man nutzt require, damit PHP die referenzierten Klassen automatisch mitkompiliert. Wichtig für Symfony OPcache Preloading: Das Preload-Skript läuft in einem separaten, isolierten Kontext ohne Zugriff auf die eigentliche Anfrage, ohne Superglobals und ohne Datenbankverbindung. Es dient nur dazu, Klassen zu kompilieren und Reflection-Metadaten aufzubauen, niemals zur Ausführung von Geschäftslogik.

Ein zweiter Aspekt betrifft opcache.preload_user. Da der PHP-FPM-Master-Prozess das Preload-Skript oft als root ausführt, verlangt PHP aus Sicherheitsgründen die explizite Angabe eines Benutzers, unter dem der Code laufen soll, sobald das Skript selbst Systemaufrufe enthält. In der Praxis reicht meist der Web-Server-Benutzer, etwa www-data. Ohne diese Direktive bricht PHP den Preload-Vorgang mit einer klaren Fehlermeldung ab, sobald root-Rechte für den Skriptlauf erkannt werden.

3. Ein Preload-Skript für ein Symfony-Projekt schreiben

Der pragmatischste Einstieg in Symfony OPcache Preloading ist kein handgeschriebenes Skript, sondern die Nutzung von Symfonys eigenem Preload-Generator. Das symfony/framework-bundle liefert eine Klasse namens ContainerPreloader, die aus dem kompilierten Container automatisch alle darin referenzierten Klassen ermittelt und ein passendes Preload-Skript generiert. Diese Datei wird typischerweise während des Deployments mit bin/console cache:warmup --env=prod erzeugt und landet unter var/cache/prod/App_KernelProdContainer.preload.php.

Wer präziser steuern möchte, welche Klassen ins Preloading aufgenommen werden, schreibt ein eigenes Skript, das die generierte Symfony-Preload-Datei einbindet und um projektspezifische Klassen ergänzt. Das ist besonders sinnvoll, wenn eine Anwendung Hot Paths hat, die selten durch den Standard-Container-Scan erfasst werden, etwa dynamisch geladene Value Objects oder DTOs aus einer API-Platform-Integration.


<?php
// config/preload.php — custom Symfony preload entry point
declare(strict_types=1);

// Reuse Symfony's own generated preload file as the foundation.
// It already contains every class referenced by the compiled container.
$containerPreload = __DIR__ . '/../var/cache/prod/App_KernelProdContainer.preload.php';

if (file_exists($containerPreload)) {
    require $containerPreload;
}

// Add project-specific classes that the container scan misses,
// e.g. DTOs used only inside API Platform serializer groups.
$extraClasses = [
    __DIR__ . '/../src/Dto/OrderSummaryDto.php',
    __DIR__ . '/../src/Dto/CustomerAddressDto.php',
    __DIR__ . '/../src/ValueObject/Money.php',
];

foreach ($extraClasses as $file) {
    if (is_file($file)) {
        opcache_compile_file($file);
    }
}

4. Container, Router und Cache-Klassen einbeziehen

Die drei teuersten Klassen beim Symfony-Boot sind fast immer der kompilierte Dependency-Injection-Container, der kompilierte Router und die kompilierten Serializer-Metadaten. Ohne Preloading müssen diese generierten PHP-Dateien bei jedem Request neu geparst werden, obwohl sich ihr Inhalt zwischen zwei Deployments nie ändert. Symfony OPcache Preloading zielt gezielt auf genau diese generierten Artefakte, weil sie besonders groß sind und in praktisch jedem Request durchlaufen werden.

Die Konfiguration in config/packages/prod/framework.yaml steuert, ob Symfony den Preload-Prozess automatisch mit einbezieht. Mit framework.cache.prefix_seed und einem eigenen compiler_pass, der zusätzliche Klassen für den Container-Compile registriert, lässt sich der Umfang gezielt erweitern. Wichtig: Value Objects, die nur über Reflection instanziiert werden, etwa in Doctrine-Hydratoren, sollten manuell zur Preload-Liste hinzugefügt werden, weil der automatische Container-Scan sie oft übersieht.


; php.ini — production OPcache preloading configuration
opcache.enable=1
opcache.enable_cli=0
opcache.preload=/var/www/app/config/preload.php
opcache.preload_user=www-data

; Preloaded classes stay in memory permanently — size the shared
; memory pool generously so it never gets evicted under pressure.
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.interned_strings_buffer=16
opcache.validate_timestamps=0

5. Preloading in PHP-FPM und Docker aktivieren

In containerisierten Symfony-Deployments ist Preloading besonders wirkungsvoll, weil jeder neu gestartete Container einen frischen PHP-FPM-Master-Prozess mitbringt, der beim Boot das Preload-Skript genau einmal ausführt. Das Dockerfile muss dafür sorgen, dass cache:warmup --env=prod während des Image-Builds läuft, damit die generierte Preload-Datei bereits im Image liegt und nicht erst beim ersten Request entsteht.

Ein häufiger Fehler: Entwickler bauen das Image, ohne den Warmup-Schritt in der richtigen Reihenfolge nach dem Composer-Install und vor dem finalen Stage-Copy auszuführen. Dann fehlt die Preload-Datei im Produktions-Image, und opcache.preload zeigt ins Leere, ohne dass PHP einen sichtbaren Fehler wirft, weil eine fehlende Preload-Datei von PHP stillschweigend ignoriert wird.


# Dockerfile excerpt — bake the preload file into the production image
FROM php:8.3-fpm AS build

WORKDIR /var/www/app
COPY . .
RUN composer install --no-dev --optimize-autoloader --classmap-authoritative

# Generate the compiled container, router and preload file at build time,
# not at first request time in production.
RUN php bin/console cache:clear --env=prod --no-debug \
    && php bin/console cache:warmup --env=prod --no-debug

FROM php:8.3-fpm AS runtime
COPY --from=build /var/www/app /var/www/app
COPY docker/opcache-preload.ini /usr/local/etc/php/conf.d/opcache-preload.ini

6. Restart-Pflicht bei jedem Deploy verstehen

Der wichtigste operative Punkt bei Symfony OPcache Preloading: Preloaded Klassen bleiben im Speicher, bis der Worker-Prozess neu startet. Ein reines opcache_reset() oder cache:clear reicht bei aktivem Preloading nicht aus, um neuen Code zu aktivieren, weil die preloaded Klassen fest im geteilten Speicher verankert sind und nicht invalidiert werden können. Jedes Deployment muss deshalb einen vollständigen Neustart von PHP-FPM oder, in Kubernetes, einen Rolling Restart der Pods auslösen.

Wer das übersieht, bekommt einen der frustrierendsten Bugs im laufenden Betrieb: Der Code wurde deployed, Tests sind grün, aber die Anwendung verhält sich weiter wie die alte Version, weil preloaded Klassen aus dem vorherigen Deployment noch aktiv sind. In Kubernetes empfiehlt sich deshalb, den Preload-Schritt in die Container-Startsequenz zu legen und über die Readiness Probe sicherzustellen, dass ein Pod erst dann Traffic bekommt, wenn der neue Preload-Vorgang abgeschlossen ist.

7. Preloading messen: Benchmarks und reale Verbesserungen

Die reale Wirkung von OPcache Preloading lässt sich nicht seriös schätzen, sie muss gemessen werden. Ein einfacher, aber aussagekräftiger Test: Zwei identische PHP-FPM-Container, einer mit opcache.preload aktiviert, einer ohne, jeweils gegen dieselbe Symfony-Route mit einem Lasttest-Tool wie wrk oder k6. In der Praxis zeigen sich die größten Verbesserungen bei kurzen, containerbasierten Requests mit vielen Autowiring-Abhängigkeiten, weniger bei sehr rechenintensiven Endpunkten, deren Laufzeit ohnehin von Datenbankabfragen dominiert wird.

Als Faustregel gilt: Preloading spart pro Request typischerweise einen niedrigen einstelligen bis niedrigen zweistelligen Millisekundenbereich, abhängig von Klassenanzahl und Vererbungstiefe des Projekts. Bei API-lastigen Symfony-Anwendungen mit sehr kurzer Antwortzeit macht dieser Anteil oft zehn bis zwanzig Prozent der Gesamtlatenz aus, weshalb sich der Aufwand für Hochlast-Systeme klar lohnt, während er bei seltenen internen Admin-Tools kaum ins Gewicht fällt.


# Benchmark preload impact with wrk against a warm-cache Symfony endpoint
wrk -t4 -c100 -d30s --latency https://staging.example.com/api/orders/42

# Compare p50/p99 with opcache.preload=0 vs opcache.preload=/path/preload.php
# by toggling the ini directive and restarting php-fpm between runs
sudo systemctl restart php8.3-fpm

8. Fallstricke: Reihenfolge, Fatal Errors, Autoloading

Der häufigste Fehler bei Symfony OPcache Preloading ist die falsche Reihenfolge beim Laden von Klassen mit Vererbungsbeziehungen. Wird eine Kindklasse geladen, bevor ihre Elternklasse bekannt ist, bricht PHP den gesamten Preload-Vorgang mit einem Fatal Error ab, und der komplette Worker startet ohne jegliches Preloading, ohne dass dies im laufenden Betrieb sofort auffällt. Deshalb sollte das Preload-Skript niemals händisch Dateien in beliebiger Reihenfolge einbinden, sondern konsequent auf Composers Autoloader-Reihenfolge oder den von Symfony generierten Preload-Datei setzen, die diese Abhängigkeiten bereits korrekt auflöst.

Ein zweiter Fallstrick betrifft Klassen, die zur Laufzeit Bedingungen prüfen, etwa class_exists()-Checks für optionale Abhängigkeiten. Solche Klassen dürfen nicht bedingungslos preloaded werden, wenn die geprüfte Abhängigkeit im Produktions-Image fehlt, weil PHP sonst beim Kompilieren selbst schon einen Fehler wirft. In der Praxis empfiehlt es sich, das Preload-Skript in der Staging-Umgebung mit vollem Logging laufen zu lassen und jede Warnung ernst zu nehmen, bevor der Rollout in Produktion erfolgt.


<?php
// config/preload.php — defensive preloading with error isolation
declare(strict_types=1);

set_error_handler(static function (int $errno, string $errstr): bool {
    // Log but never let a single failing class abort the whole preload run
    error_log(sprintf('[preload] Skipped class due to: %s', $errstr));
    return true;
});

$files = require __DIR__ . '/preload-file-list.php'; // ordered by autoloader

foreach ($files as $file) {
    try {
        if (is_file($file)) {
            opcache_compile_file($file);
        }
    } catch (\Throwable $e) {
        error_log(sprintf('[preload] Failed compiling %s: %s', $file, $e->getMessage()));
    }
}

restore_error_handler();

9. Preloading im direkten Vergleich

Um die Entscheidung für oder gegen Symfony OPcache Preloading greifbar zu machen, hilft ein direkter Vergleich der drei realistischen Betriebsmodi. Jeder Modus hat einen klaren Kompromiss zwischen Boot-Overhead pro Request und operativer Komplexität beim Deployment.

Modus Klassenladung pro Request Deployment-Komplexität Empfehlung
Kein OPcache Vollständige Neukompilierung Keine Nur lokale Entwicklung
OPcache ohne Preloading Bytecode aus Cache, Verknüpfung pro Request Niedrig Solider Standard
OPcache mit Preloading Klassen dauerhaft verknüpft im Speicher Hoch, Restart-Pflicht pro Deploy Hochlast-Produktionssysteme
Preloading + validate_timestamps=0 Maximal, keine Dateisystemprüfung Sehr hoch, striktes Deploy-Skript nötig Container-Deployments mit unveränderlichen Images

In containerisierten Symfony-Umgebungen mit unveränderlichen Images ist die Kombination aus Preloading und opcache.validate_timestamps=0 die konsequenteste Stufe, weil ein Image ohnehin nie im laufenden Betrieb verändert wird. In klassischen Server-Setups mit gelegentlichem Hotfix per FTP oder rsync ist diese Kombination riskant, weil geänderte Dateien schlicht ignoriert werden, bis ein expliziter Neustart erfolgt.

Mironsoft

Symfony-Performance, Deployment-Automatisierung und PHP-FPM-Tuning

Symfony-Anwendung mit spürbarem Boot-Overhead pro Request?

Wir analysieren eure Container-Compile-Zeit, richten OPcache Preloading produktionsreif ein und integrieren den Warmup-Schritt sauber in eure Deployment-Pipeline, inklusive Rolling Restart ohne Downtime.

Performance-Audit

Boot-Zeit-Analyse und Identifikation der teuersten Container-Klassen

Preload-Setup

Produktionsreifes preload.php inklusive Fehlerbehandlung und Logging

Deployment-Integration

Rolling Restart, Readiness Probes und Docker-Image-Optimierung

10. Zusammenfassung

Symfony OPcache Preloading entfernt einen der wenigen verbleibenden Overhead-Anteile, die bei jeder einzelnen Anfrage anfallen, obwohl sich der zugrunde liegende Code zwischen Deployments nie ändert. Ein sauberes Preload-Skript, das auf der von Symfony generierten Container-Preload-Datei aufbaut und um projektspezifische Klassen erweitert wird, ist der pragmatischste Einstieg. Die Konfiguration in opcache.preload und opcache.preload_user muss zwingend als Teil des Deployment-Images gebaut werden, nicht erst zur Laufzeit.

Der entscheidende operative Punkt bleibt die Restart-Pflicht: Preloading ohne einen zuverlässigen Rolling-Restart-Mechanismus führt früher oder später zu veralteten Klassen im Produktionsbetrieb. Wer diesen Prozess sauber automatisiert, in Docker-Images bäckt und mit Lasttests validiert, gewinnt bei API-lastigen Symfony-Anwendungen einen messbaren, dauerhaften Performance-Vorteil pro Request, ganz ohne Änderung am eigentlichen Anwendungscode.

Symfony OPcache Preloading — Das Wichtigste auf einen Blick

Konfiguration

opcache.preload und opcache.preload_user in der php.ini, Preload-Datei aus cache:warmup --env=prod generiert.

Reihenfolge

Elternklassen vor Kindklassen laden, sonst bricht der gesamte Preload-Vorgang mit einem Fatal Error ab.

Deployment

Warmup zur Build-Zeit im Docker-Image, niemals erst beim ersten Request in Produktion.

Betrieb

Jeder Deploy braucht einen vollständigen Worker-Neustart, sonst laufen preloaded Klassen aus dem alten Deployment weiter.

11. FAQ: Symfony OPcache Preloading

1Unterschied OPcache und Preloading?
OPcache cached Bytecode, muss Vererbung aber pro Request auflösen. Preloading verknüpft Klassen einmalig dauerhaft beim Worker-Start.
2Preload-Skript selbst schreiben?
Nicht nötig. cache:warmup --env=prod generiert bereits eine passende Datei. Eigenes Skript nur für zusätzliche Klassen sinnvoll.
3Neustart nach jedem Deploy nötig?
Ja. Preloaded Klassen bleiben bis zum Worker-Ende im Speicher, cache:clear reicht nicht aus.
4Was macht opcache.preload_user?
Legt den Ausführungsbenutzer fest, meist www-data, weil root-Ausführung aus Sicherheitsgründen blockiert wird.
5Lohnt sich Preloading bei kleinen Projekten?
Kaum. Der Effekt skaliert mit Klassenanzahl, bei wenigen hundert Klassen ist der Gewinn meist minimal.
6Fehler im Preload-Skript?
Bricht ohne Fehlerbehandlung den gesamten Vorgang ab. Ein Error-Handler mit Logging verhindert das Alles-oder-nichts-Verhalten.
7Preloading in lokaler Entwicklung?
Unüblich, da Code ständig wechselt. Gehört ausschließlich in Produktion und Staging.
8Wie messe ich die Wirkung?
Lasttest mit wrk oder k6, einmal mit und ohne Preload-Direktive, jeweils nach frischem Neustart vergleichen.
9validate_timestamps deaktivieren nötig?
Nein, unabhängige Direktive. Verstärkt den Effekt, erfordert aber diszipliniertes Deployment mit Neustart.
10Unterschied Kubernetes vs. klassischer Server?
Rolling Deployments ersetzen Pods automatisch mit frischem PHP-FPM. Klassische Server brauchen manuellen Neustart im Deploy-Skript.