Shopware: „removed-ids.php: Failed to open stream“ – wenn der Container-Cache unter laufenden Prozessen verschwindet
Ein Plugin-Update im Admin, kurz danach ein Cache-Leeren, und im PHP-Log stehen plötzlich Hunderte Warnungen über eine Datei namens removed-ids.php. Manchmal hört es nach ein paar Minuten von selbst auf. Manchmal läuft es den ganzen Tag. In den Shops, die wir überwachen, kam er zuletzt rund 570-mal vor, fast ausschließlich auf Shared Hosting.
Die Fehlermeldung
Meist erscheint der Fehler als Warning:
php.WARNING: Warning: require(/var/www/shop/var/cache/prod_h<hash>/Container<id>/removed-ids.php):
Failed to open stream: No such file or directory
{"exception":"[object] (ErrorException(code: 0): ... at /var/www/shop/var/cache/prod_h<hash>/...)"}
Es gibt aber auch die härtere Variante, bei der der Request abbricht:
php.CRITICAL: Uncaught Error: Failed opening required
'/var/www/shop/var/cache/prod_h<hash>/Container<id>/removed-ids.php'
(include_path='.:/opt/php-8.4/lib/php')
Die Datei ist kein Teil des Shopware-Codes, im Repository suchst du sie also vergeblich. Sie liegt im kompilierten Symfony-Container unter var/cache.
Was dahintersteckt
Shopware legt den kompilierten Service-Container in var/cache/prod_h<hash>/ ab. Den Hash berechnet der Kernel aus der Cache-ID, der Shopware-Revision und der Liste der aktiven Plugins samt Versionen (Kernel::getCacheHash() in src/Core/Kernel.php). Wird ein Plugin installiert, aktiviert oder aktualisiert, ändert sich der Hash. Shopware baut dann einen neuen Container in einem neuen Verzeichnis und räumt die alten Verzeichnisse auf. Das passiert über die asynchrone Nachricht CleanupOldCacheFolders, die alle prod_*-Ordner außer dem aktuellen löscht.
Symfony schreibt den Container als viele einzelne PHP-Dateien und lädt manche davon erst bei Bedarf. removed-ids.php gehört dazu. Die generierte Methode getRemovedIds() macht nichts anderes als require $this->containerDir.'/removed-ids.php'. Aufgerufen wird sie aber nur, wenn ein Service angefragt oder gesetzt wird, den der kompilierte Container nicht kennt. Ein Prozess kann also minutenlang problemlos mit seinem Container arbeiten und erst beim ersten Zugriff auf eine unbekannte Service-ID merken, dass sein Verzeichnis inzwischen gelöscht ist.
Genau das ist der Fehler. Ein PHP-Prozess hat den alten Container geladen, ein anderer löscht dessen Verzeichnis, und dann greift der erste auf eine Datei zu, die nicht mehr da ist. Am häufigsten trifft es Messenger-Worker, die seit Minuten oder Stunden laufen und beim Plugin-Update gerade mitten in einer Nachricht stecken. Seltener erwischt es einen FPM-Request, der zufällig im falschen Moment läuft. Auch der Button „Caches leeren“ im Admin gehört zu den Auslösern: CacheClearer::clear() ruft am Ende cleanupOldContainerCacheDirectories() auf.
Und dann gibt es noch den Fall, dass zwei Prozesse gleichzeitig aufräumen.
Den hat Shopware als Bug bestätigt. In Issue #10706 ließ sich unter 6.7.0.0 mit aktivem Commercial-Plugin kein weiteres Plugin mehr installieren, die Meldung war Failed opening required '…/removed-ids.php'. Der Fix in PR #10907 („fix: reduce too many cache cleaning operations“) beschreibt die Ursache recht offen: Zwei Prozesse lösen gleichzeitig Cache-Leerungen aus, und einer löscht PHP-Dateien, die der andere gerade braucht. Seitdem setzt der CacheClearer vor dem Löschen der Container-Verzeichnisse einen Lock mit 30 Sekunden Laufzeit. Im Tag v6.7.1.0 ist der Lock drin, in v6.7.0.1 noch nicht. Im 6.6er-Zweig (zuletzt geprüft: v6.6.10.29) haben wir ihn nicht gefunden, obwohl das Issue zwischenzeitlich für einen Backport wieder geöffnet wurde.
Der PR spricht übrigens selbst nur davon, das Risiko zu „reduzieren“. Den Fall „Worker hält alten Container, jemand löscht ihn“ löst der Lock nicht. Dafür musst du auf deiner Seite sorgen.
Prüfen, ob es wirklich das ist
Schau zuerst, wann die Warnungen kommen. Fallen sie in Gruppen kurz nach einem Plugin-Update, einer Plugin-Installation oder einem Klick auf „Caches leeren“ an, ist der Fall ziemlich klar.
# Welche Container-Verzeichnisse gibt es gerade?
ls -la var/cache/
# Taucht der Ordner aus der Fehlermeldung noch auf?
ls -d var/cache/prod_h*/Container*
Steht in der Meldung ein prod_h…- oder Container…-Name, den es nicht mehr gibt, hast du die Bestätigung. Prüf außerdem, wie deine Worker laufen:
crontab -l | grep messenger
ps aux | grep messenger:consume
Ein Worker, der seit Stunden läuft und vor dem letzten Plugin-Update gestartet wurde, ist ein heißer Kandidat.
Hält der Fehler länger an, nicht nur ein paar Minuten nach einer Änderung, gibt es eine Variante, die wir nicht abschließend belegen können: OPcache mit opcache.validate_timestamps=0. Dann kann PHP-FPM eine veraltete Container-Loader-Datei aus dem Speicher ausliefern, die noch auf das gelöschte Verzeichnis zeigt. Ein Thread im Shopware-Forum zu 6.6.6.0 beschreibt einen ähnlichen Verlauf: rund 300.000 Einträge an einem Tag, die erst aufhörten, als ein neuer Container-Ordner angelegt wurde. Eine bestätigte Ursache gibt es in dem Thread nicht.
php -i | grep -E 'opcache.enable|validate_timestamps'
Die Lösung
Die eigentliche Regel ist einfach: Kein Prozess sollte mit einem Container weiterarbeiten, dessen Dateien gerade gelöscht werden. Daraus ergeben sich ein paar konkrete Schritte.
Erstens die Worker nach jeder Änderung am Container sauber neu starten. Symfony empfiehlt in der Messenger-Doku für Deployments, messenger:stop-workers aufzurufen, nachdem der neue Code liegt und der Cache aufgewärmt ist:
bin/console cache:clear
bin/console cache:warmup
bin/console messenger:stop-workers
stop-workers setzt nur ein Signal im App-Cache. Jeder Worker beendet danach seine aktuelle Nachricht und steigt aus, Supervisor oder der nächste Cronlauf startet ihn mit dem neuen Container. Shopware setzt dieses Signal bei Plugin-Lifecycle-Aktionen auch selbst (signalWorkerStopInOldCacheDir() im PluginLifecycleService). Ein Worker prüft es aber erst zwischen zwei Nachrichten. Steckt er gerade in einem langen Import, kann es trotzdem knallen.
Zweitens Plugins auf Produktivsystemen nicht über den Admin installieren oder aktualisieren, sondern im Deployment per CLI, am besten mit einer kurzen Wartungsphase:
bin/console plugin:refresh
bin/console plugin:update MeinPlugin
bin/console cache:clear
bin/console messenger:stop-workers
Drittens auf 6.7.1.0 oder neuer aktualisieren, wenn du auf 6.7 bist. Damit ist der parallele Cache-Clear aus Issue #10706 entschärft. Wer noch auf 6.6 läuft, hat diesen Lock nach unserem Stand nicht und muss sich umso mehr an die Reihenfolge oben halten.
Bei mehreren App-Servern gibt es zusätzlich shopware.deployment.cluster_setup. Laut Shopware-Doku verhindert die Option ab 6.5.6.0 unter anderem, dass Shopware zur Laufzeit lokal Symfony-Cache-Dateien löscht. Plugin-Änderungen gehören dann ohnehin ins Deployment.
# config/packages/shopware.yaml
shopware:
deployment:
cluster_setup: true
Für einen Server mit Plugin-Verwaltung im Admin ist das nichts. Mit der Option sind Plugin-Änderungen über den Admin praktisch abgeschaltet.
Wenn das nicht hilft
Läuft der Fehler nach einer Änderung weiter, hilft als Sofortmaßnahme meistens, alle Prozesse auf den aktuellen Container zu zwingen:
bin/console messenger:stop-workers
bin/console cache:clear
# OPcache leeren: PHP-FPM neu laden (bei Managed Hosting über das Kundenmenü)
sudo systemctl reload php8.4-fpm
Das ist ein Workaround für den akuten Zustand, keine Lösung. Wenn die Warnungen danach bei jedem Plugin-Update wiederkommen, liegt es an der Reihenfolge im Deployment oder an Workern, die nie neu gestartet werden.
Auf Shared Hosting ohne Supervisor gibt es noch einen Sonderfall: Laufen per Cron mehrere messenger:consume-Prozesse mit je fünf Minuten Laufzeit überlappend, ist praktisch immer einer davon mitten in der Arbeit, wenn du den Cache leerst. Kürzere --time-limit-Werte verkleinern das Fenster, schließen es aber nicht.
Diese Warnung zeigt sich selten im Storefront. Sie sammelt sich im PHP-Log, oft zu Hunderten, oft ohne dass jemand hinschaut. ShopSignal zählt Fehler und Warnungen pro Shop, macht die Logs im Volltext durchsuchbar und schlägt per Alert-Regel an, wenn die Zahl nach einem Update plötzlich hochspringt. So siehst du die removed-ids.php-Welle am selben Nachmittag und nicht erst beim nächsten Blick ins Log.