OPcache Fragmentierung bremst PHP-Anwendungen sichtbar aus, weil der Cache freien Speicher in kleine Inseln zerlegt und so neue Bytecode-Blöcke schlechter aufnimmt. Ich zeige dir, wie du Fragmentierung sicher erkennst, zielgerichtet beseitigst und mit sauberen Deploys sowie passender OPcache-Konfiguration dauerhaft verhinderst.
Zentrale Punkte
Die folgenden Kernaspekte geben dir einen schnellen Fahrplan, den du Schritt für Schritt umsetzen kannst und so Performance stabilisierst.
- Kennzahlen lesen: used/free/wasted_memory, current_wasted_percentage, opcache_hit_rate.
- Schwellen setzen: wasted_memory < 5 % gut, ab 15–30 % handeln.
- Konfiguration stärken: memory_consumption, max_accelerated_files, interned_strings_buffer.
- Reset-Strategie: geplanter opcache_reset() oder Dienstneustart mit Warmup.
- Deploy-Disziplin: stabile Pfade, kontrollierte Invalidierung, Monitoring.
Warum OPcache-Fragmentierung entsteht
OPcache speichert kompilierten Bytecode im Shared Memory, doch häufige Deployments, wechselnde Verzeichnisse oder viele Plugin-Wechsel hinterlassen Lücken. Solche Lücken sind nicht als zusammenhängender Block nutzbar, wodurch der Cache ineffizient arbeitet und Bytecode öfter neu kompiliert. Kurze Revalidierungsintervalle treiben Invalidierungen hoch und verschärfen das Muster gebrochener Speicherblöcke. Zu geringe Limits für Speicher oder Dateiindizes erhöhen Evictions und fördern ein instabiles Speicherlayout. Ich prüfe zuerst die Deploy-Gewohnheiten und setze auf konstante Pfade, sonst wächst die Fragmentierung mit jedem Release weiter.
Die richtigen OPcache-Kennzahlen lesen
Ich werte regelmäßig used_memory, free_memory und wasted_memory aus, weil diese Größen die tatsächliche Cache-Qualität zeigen. Besonders wichtig ist current_wasted_percentage, denn der Prozentwert lässt sich gut mit festen Schwellen verbinden. Fällt die opcache_hit_rate spürbar unter 99 %, deutet die Entwicklung auf ungenutzte Potenziale oder Fragmentierung. Ich behalte zusätzlich num_cached_scripts im Blick, um zu erkennen, ob Limits für Dateieinträge Engpässe auslösen. Ohne diese Metriken tappt man bei schwankender Performance im Dunkeln.
Schwellenwerte, ab wann du handeln musst
Unter 5 % wasted_memory läuft ein OPcache meist unauffällig und ich belasse die Einstellungen vorerst. Ab 15 % plane ich Gegenmaßnahmen, insbesondere wenn free_memory gleichzeitig knapp wird. Spätestens bei 30 % wasted_memory gilt der Cache faktisch als verkleinert, und ich setze einen Reset oder Neustart an. Sinkt free_memory auf etwa 10 % und der Cache meldet sich als voll, beschleunigt sich die Fragmentierungsschraube. Solche Grenzen machen Entscheidungen klar, weil sie Aktion statt Bauchgefühl erzwingen.
Symptome im Live-Betrieb sicher deuten
Gleichmäßige Latenzerhöhungen über viele Endpunkte verraten eine breit wirkende Bremse wie Fragmentierung. Steigende CPU-Last bei identischem Traffic passt ebenfalls zu häufigeren Neukompilationen durch einen zerschnittenen Cache. Eine anhaltend niedrige Hit-Rate nach dem Warmup bestätigt das Muster zusätzlich. Häufen sich OPcache-Restarts oder Evictions ohne große Codeänderungen, fehlt dem Cache schlicht Platz in zusammenhängenden Blöcken. Ich ordne diese Indizien den Metriken zu und löse dann gezielte Maßnahmen aus.
OPcache zuverlässig überwachen und auslesen
Ein kleines Skript mit opcache_get_status() liefert mir die nötigen Daten direkt aus PHP. Für schnelle Checks reicht phpinfo(), für Trendanalysen speichere ich die Werte regelmäßig ins Monitoring. Ich visualisiere wasted_memory, Hit-Rate und free_memory, damit schleichende Verschlechterungen auffallen. Zeitbasierte Vergleiche nach Deploys zeigen, ob bestimmte Release-Muster Fragmentierung beschleunigen. Ohne diesen Blick auf den Verlauf lassen sich Ursachen schwer einordnen.
Konfiguration: Speicher und Dateilimits sauber festlegen
Über opcache.memory_consumption dimensioniere ich den Speicher passend zur Codebasis: kleine WordPress-Setups fahren oft mit 128–256 MB gut, mittlere Sites mit 256–384 MB, größere Shops benötigen 384–512 MB oder mehr. Mit opcache.max_accelerated_files verhindere ich, dass zu wenige Dateiindizes die Caching-Quote drücken; 8000–10000 für kleine WordPress-Seiten, 20000+ für WooCommerce oder große Frameworks haben sich bewährt. Ich zähle die PHP-Dateien inklusive Vendor und setze das Limit auf das 1,3–1,5-fache dieser Zahl. Wer tiefer einsteigen will, findet Hintergründe zur OPcache-Konfiguration in einer praxisnahen Anleitung. Solide Limits stabilisieren das Speicherlayout und verringern die Fragmentierung spürbar.
Interned Strings und Huge Code Pages nutzen
Mit opcache.interned_strings_buffer minimiere ich doppelte Strings im Speicher; 16–32 MB helfen größeren Projekten, den Platz effizienter zu nutzen. Wer mehr Traffic hat, profitiert häufig von noch etwas größeren Puffern. Optional beschleunigen opcache.huge_code_pages die Ausführung, sofern das System große Seiten bereitstellt. Weniger Verwaltungs-Overhead bedeutet meist etwas niedrigere Latenzen und tendenziell weniger Zersplitterung. Ich aktiviere diese Option erst nach Testläufen, damit keine Überraschungen im Betrieb entstehen.
Timestamp-Validierung und Revalidierung richtig einstellen
Zu aggressive Timestamp-Checks invalidieren Bytecode häufig und treiben die Fragmentierung in die Höhe. In Entwicklung halte ich validate_timestamps=1 und revalidate_freq niedrig, damit Änderungen sofort sichtbar sind. In Produktion wähle ich moderate Intervalle von 60–300 Sekunden oder setze auf validate_timestamps=0 plus expliziten OPcache-Reset beim Release. Wer Ursachen tiefer untersuchen will, nutzt Analysen zur OPcache-Invalidierung und möglichen Performance-Spitzen. Mit kontrollierter Invalidierung bleibt der Speicher zusammenhängender, die Hit-Rate stabil.
Fragmentierung zielgerichtet beseitigen: Reset-Strategien
Wenn wasted_memory deutlich steigt und die Hit-Rate fällt, starte ich einen Reset via opcache_reset() in ruhigeren Zeitfenstern. Direkt danach fahre ich ein Warmup wichtiger Routen, um den Cache schnell zu füllen und Lastspitzen zu vermeiden. Alternativ starte ich PHP-FPM oder Apache neu, was das Shared-Memory-Segment vollständig erneuert. Nach jedem Reset überwache ich Hit-Rate, wasted_memory und free_memory, damit sich der Cache wie geplant erholt. Geplante Neustarts in der Nacht bewähren sich bei Setups, die häufiger Fragmentierung aufbauen.
Prävention: saubere Deploys und Warmups
Ich deploye Releases in neue Verzeichnisse und schalte per Symlink auf einen fixen Pfad wie /var/www/html/current um, damit OPcache keine Altlasten aus wechselnden Pfaden hortet. Unmittelbar nach dem Umschalten führe ich einen kontrollierten Reset aus. Ein Skript, das populäre Seiten, REST-Routen und Shop-Ansichten anfragt, wärmt den Cache gezielt an. So steigt die Hit-Rate zügig auf ein hohes Niveau, und Nutzer spüren Wartungsfenster kaum. Mit dieser Disziplin sinkt die Fragmentierung dauerhaft.
Praxis-Tipps für WordPress, WooCommerce und Frameworks
WordPress-Blogs mit wenigen Plugins profitieren oft von 128–256 MB memory_consumption und mindestens 8000 max_accelerated_files, dazu revalidate_freq um 60–120 Sekunden. Größere WooCommerce-Shops fahren mit 256–512 MB Speicher, 20000+ max_accelerated_files und einem interned_strings_buffer von 16–32 MB zuverlässiger. Frameworks wie Laravel oder Symfony benötigen häufig 20000–40000 Dateiindizes und 256–512 MB oder mehr, abhängig von Vendor-Größe. Wer typische Stolpersteine vermeiden möchte, findet eine kompakte Hilfe zu OPcache-Fehlkonfigurationen in WordPress-Setups. Die folgende Tabelle fasst sinnvolle Richtwerte zusammen.
| Projekt-Typ | memory_consumption | max_accelerated_files | revalidate_freq | interned_strings_buffer |
|---|---|---|---|---|
| Kleines WordPress | 128–256 MB | 8.000–10.000 | 60–120 s | 8–16 MB |
| WooCommerce/Medium | 256–384 MB | 20.000+ | 60–180 s | 16–32 MB |
| Großer Shop/Multisite | 384–512 MB+ | 30.000+ | 120–300 s | 32–48 MB |
| Laravel/Symfony | 256–512 MB+ | 20.000–40.000 | 60–180 s | 16–32 MB |
Fortgeschritten: Preloading und JIT ohne Nebenwirkungen
Ab PHP 7.4 kann ich mit opcache.preload häufig genutzte Klassen und Funktionen beim Start laden. Das senkt Kaltstart-Latenzen und stabilisiert die Hit-Rate. Ich beachte, dass Preloading stark an den Lebenszyklus des PHP-Prozesses gebunden ist: Ändern sich vorbeladene Dateien, plane ich einen gezielten Neustart von PHP-FPM/Apache, weil solche Änderungen nicht allein durch opcache_reset() sauber wirksam werden. In PHP 8.x lohnt außerdem ein Blick auf JIT: Der Parameter opcache.jit_buffer_size reserviert separaten Speicher für JIT-Compilation. JIT beeinflusst die OPcache-Metriken nicht direkt, kann aber CPU-Last und Antwortzeiten verbessern. Bei aktivem JIT teste ich Warmup und Speicherheadroom besonders sorgfältig, damit kein zusätzlicher Druck auf das Shared-Memory-Segment entsteht.
Allocator-Details verstehen: free vs. wasted
OPcache verwaltet den Shared Memory in Chunks. Beim Löschen oder Ersetzen von Skripten entstehen Lücken, die häufig nicht exakt zur Größe neuer Bytecode-Blöcke passen. Diese Lücken zählen als wasted_memory. free_memory dagegen ist zusammenhängender, sinnvoll nutzbarer Speicher. Ein hoher wasted-Anteil bei gleichzeitig scheinbar „viel frei“ ist der Klassiker, der reale Kapazität versteckt. Ich beobachte, wie schnell wasted_memory nach Deploys wächst: Explodiert die Quote bereits nach wenigen Minuten, deute ich das als Zeichen für instabile Pfade, zu kurze Revalidierungsintervalle oder viel wechselnden Code (z. B. häufig regenerierte Template-Dateien). Huge Code Pages reduzieren Verwaltungs-Overhead und können damit die Fragmentierungstendenz leicht dämpfen, ersetzen aber keine saubere Deploy-Strategie.
Größenfindung mit Methode: so dimensioniere ich korrekt
Statt nur „gefühlt“ zu erhöhen, gehe ich planvoll vor:
- Ich ermittle die Spitzenwerte von used_memory nach einem vollständigen Warmup plus Tageslast.
- Ich addiere den durchschnittlichen wasted_memory-Wert in stabilen Phasen (nach Reset, vor Deploys).
- Ich plane 20–30 % Headroom für Releases, saisonale Last und Wachstum ein.
Aus diesen Bausteinen ergibt sich ein Zielwert für opcache.memory_consumption. Für opcache.max_accelerated_files zähle ich alle PHP-Dateien (inkl. Vendor) und setze das Limit um 30–50 % höher als die reale Datei-Anzahl, um Fluktuationen durch Updates abzufangen. Nach der Anpassung prüfe ich, ob num_cached_scripts dauerhaft deutlich unterhalb des Limits bleibt und die Hit-Rate nach Warmup stabil über 99 % liegt.
Warmup-Playbook: schnell und gezielt auf Betriebstemperatur
Ein Warmup verhindert Kaltstart-Peaks und verteilt Bytecode gleichmäßiger. Ich fahre zwei Stufen:
- Technisches Warmup: Ich triggere zentrale Routen (Home, Login, Warenkorb, Checkout, Such-API) parallel.
- Inhalts-Warmup: Ich lade stark frequentierte Seiten und REST-Endpunkte aus Logs/Analytics.
Beispiel für ein kompaktes Warmup-Skript (Shell):
#!/usr/bin/env bash
set -euo pipefail
BASE="https://example.org"
URLS=(
"/" "/wp-login.php" "/shop/" "/cart/" "/checkout/"
"/wp-json/wp/v2/posts?per_page=1" "/wp-json/wc/store/products?per_page=1"
)
for u in "${URLS[@]}"; do
curl -fsS -m 10 -H "User-Agent: Warmup" "$BASE$u" &
done
wait
Für tiefere Integrationen kann ich zusätzlich einen PHP-Endpunkt nutzen, der opcache_compile_file() für häufige Dateien aufruft. Wichtig: Warmup-Skripte gehören in die Release-Pipeline, direkt nach Reset und vor dem Öffnen des Traffics.
Deploy-Varianten: Blue/Green, Rolling, Symlinks
Blue/Green-Deployments mit fixem Symlink-Pfad vermeiden Pfadfluktuation. Bei Rolling-Strategien über mehrere App-Server synchronisiere ich Schritte strikt: Zuerst neuen Code synchronisieren, dann Reset+Warmup je Host, zuletzt Traffic umschwenken. Bei PHP-FPM unterscheide ich zwischen Reload und Restart: Ein Reload lädt Konfigurationen neu, lässt das bestehende Shared-Memory-Segment aber häufig weiterlaufen; ein Restart erzeugt das Segment neu und beseitigt Fragmentierung zuverlässig. Unter Apache mit PHP als Modul erreiche ich den gleichen Effekt mit einem sauberen Neustart. Ich dokumentiere je Umgebung klar, welcher Befehl „wirklich“ den OPcache leert, damit nächtliche Wartungsfenster planbar bleiben.
Sonderfälle: Multi-Tenant, CLI und Worker
In Multi-Tenant-Setups nutze ich getrennte FPM-Pools und setze opcache.validate_permission=1, damit ein Mandant keinen Code eines anderen nutzt. Das erhöht Sicherheit und reduziert unerwartete Cache-Kollisionen. Für CLI-Jobs prüfe ich opcache.enable_cli: Standardmäßig ist es aus, was Fragmentierung im Webpfad nicht beeinflusst. Betreibe ich jedoch langlaufende CLI-Worker, kann ein aktivierter CLI-OPcache sinnvoll sein – dann gelten dieselben Regeln für Reset und Warmup. Bei dynamisch generierten oder sehr häufig wechselnden PHP-Dateien (z. B. Build-Artefakte, Templating-Ausgaben) blacklist’e ich diese mit opcache.blacklist_filename, um churn und damit Fragmentierung zu vermeiden.
Datei- und Pfadvalidierung feinjustieren
Mit opcache.revalidate_path bestimme ich, ob OPcache bei geänderten include_path/Symlinks Pfade neu auflöst. In stabilen Produktionspfaden lasse ich den Wert meist auf 0. Wechsle ich per Symlink zwischen Releases, prüfe ich, ob die Applikation darauf angewiesen ist – gegebenenfalls aktiviere ich revalidate_path gezielt. file_update_protection verhindert zu schnelle Re-Compiles direkt nach Dateiänderungen (kurzes Schutzfenster in Sekunden). In Build-Pipelines, die Dateien atomar austauschen, halte ich den Wert moderat, damit frisch bereitgestellter Code zügig in den Cache kommt. Der opcache.file_cache (Second-Level-Cache auf Disk) ist optional nützlich, um nach Neustarts schneller warm zu werden; für Fragmentierung im Shared Memory ist er kein Ersatz, aber er reduziert Kaltstartkosten und damit die Häufigkeit hektischer Compiles.
Häufige Irrtümer und Anti-Pattern
- „Mehr Speicher löst alles“: Zu großer Cache ohne Disziplin fragmentiert nur später. Deploy- und Reset-Strategie zuerst klären.
- „Reload reicht schon“: In vielen Umgebungen bleibt das Shared-Memory-Segment bestehen. Für einen echten Reset plane ich einen Restart oder opcache_reset()+Warmup.
- „Hit-Rate 98 % ist doch ok“: Unter Last bedeuten 1–2 % mehr Kompilationen spürbare Latenzspitzen. Ziel bleibt > 99 % nach Warmup.
- „Wir invalidieren ständig – ist sicherer“: Häufige Invalidierungen beschleunigen Fragmentierung. Besser: kontrollierte Invalidierung an Release-Punkten.
Fehlersuche: strukturierter Ablauf
- Status erfassen: opcache_get_status(), used/free/wasted_memory, num_cached_scripts, opcache_hit_rate sichern.
- Grenzen prüfen: Liegen wasted_memory >= 15 % oder free_memory <= 10 %? Dann Gegenmaßnahmen einplanen.
- Limits verifizieren: max_accelerated_files vs. reale Datei-Anzahl, interned_strings_buffer vs. Strings-Usage.
- Reset+Warmup testen: In einer ruhigen Phase ausführen, Metriken vor/nachher vergleichen.
- Deploy-Muster anpassen: Feste Pfade, Symlink-Umschaltung, Invalidierung nur beim Release.
- Monitoring schärfen: Trends über Tage/Wochen beobachten, Peaks nach Deploys korrelieren.
Ein minimalistischer Status-Endpunkt für das Monitoring hilft mir in der Praxis sehr:
<?php
header('Content-Type: application/json');
echo json_encode(opcache_get_status(false));
Kurz zusammengefasst für den Alltag
Ich halte wasted_memory unter 5 %, die Hit-Rate über 99 % und free_memory fern der 10 %-Grenze, weil solche Marken klare Signale liefern. Steigen die Werte in kritische Zonen, plane ich sofort einen Reset mit Warmup oder erhöhe sauber dimensioniert Speicher und Dateiindizes. Deploys auf stabile Pfade plus kontrollierte Invalidierung verhindern, dass Altlasten den Cache zusetzen. Kontinuierliches Monitoring deckt Muster auf, die reine Momentaufnahmen nicht zeigen. Mit diesem Vorgehen bleibt die Performance gleichmäßig und der OPcache arbeitet als verlässlicher Beschleuniger statt als Risikoquelle.


