Wenn der JTL-Shop den Webspace mit Millionen Dateien flutet
Ein Praxisfall: 3,2 Millionen Cache-Dateien, drei Mahnungen vom Hoster — und warum schnelleres Aufräumen die falsche Lösung war.
Die Mail vom Hoster kam zum dritten Mal, diesmal mit Frist: 3,2 Millionen Dateien im Verzeichnis des Webshops, das Inode-Kontingent des Pakets war deutlich überschritten. Der Kunde hatte alles richtig gemacht — es lief seit Wochen ein Cronjob, der den Cache regelmäßig aufräumte. Nur half er offensichtlich nicht.
Der Fall ist lehrreich, weil er zwei Fehler enthält, die ich seitdem an mehreren Shops wiedergefunden habe.
Woher die Dateien kommen
Der JTL-Shop 5 speichert im Objekt-Cache alles, was teuer zu berechnen ist: Kategoriebäume, Einstellungen, Sprachvariablen — und, das ist der entscheidende Punkt, die Ergebnismengen von Filtern. Steht der Cache-Typ auf advancedfile, landet jeder dieser Einträge als Datei auf der Platte.
Solange nur Menschen den Shop bedienen, ist das harmlos. Kritisch wird es, wenn Bots die Filternavigation entdecken. Jede Kombination aus Hersteller, Preisspanne, Merkmal und Sortierung ergibt eine eigene URL — und damit einen eigenen Cache-Eintrag. Ein Crawler, der sich systematisch durch die Kombinationen arbeitet, erzeugt in wenigen Tagen sechsstellige Dateimengen. Das ist kein Angriff, das ist ein Crawler, der seine Arbeit macht.
Fehler 1: Der Cronjob zählte nur die Hälfte
Das Aufräumskript des Kunden sah aus wie tausend andere im Netz:
find /var/www/shop/includes/cache -type f -mmin +120 -delete
Sieht vernünftig aus. Ist es auch — für die Hälfte des Problems.

Der advancedfile-Treiber legt pro Cache-Eintrag nämlich zwei Dateisystemeinträge an: die eigentliche Datendatei und einen Symlink, über den der Eintrag gefunden wird. Der Filter -type f trifft ausschließlich reguläre Dateien. Symlinks fallen komplett durch das Raster.
Im konkreten Fall: 523.267 reguläre Dateien standen 592.363 Symlinks gegenüber. Der Cronjob löschte Woche für Woche brav die Dateien und ließ mehr als die Hälfte der Inodes unangetastet liegen. Auf dem Dashboard sah es aus, als würde aufgeräumt. Im Dateisystem wuchs der Berg weiter.
Die korrigierte Zeile:
find /var/www/shop/includes/cache -mindepth 1 -mmin +120 -delete
Kein -type f. Wer beim Zählen wissen will, was tatsächlich liegt, prüft nach Typ getrennt:
find /pfad/cache -mindepth 1 -type f | wc -l # Dateien
find /pfad/cache -mindepth 1 -type l | wc -l # Symlinks
Fehler 2: Schneller putzen ist keine Lösung
Nach der Korrektur des Cronjobs sank die Zahl deutlich — aber sie stieg zwischen zwei Durchläufen genauso schnell wieder an. Alle zwei Stunden aufzuräumen bedeutet eben auch, dass zwei Stunden lang Dateien entstehen dürfen. Und ein find über ein Verzeichnis mit einer Million Einträgen ist selbst eine Belastung für den Server, weil es jeden Eintrag anfassen muss.
Die eigentliche Lösung setzt eine Ebene höher an: Der Filter-Cache muss gar nicht erst auf die Platte.
Im JTL-Shop steuert die Einstellung caching_types_disabled, welche Cache-Gruppen übersprungen werden. Ein Fallstrick am Rande: Der Wert liegt in der Datenbank PHP-serialisiert vor. Wer ihn direkt per SQL setzt, muss die Serialisierung korrekt schreiben, sonst ignoriert der Shop den Wert stillschweigend. Über die Shop-Administration ist der Weg unkritischer.
Nach dem Abschalten der Filter-Caches fiel die Neuanlage im Praxisfall auf einen Bruchteil — der Shop blieb dabei genauso schnell, weil die teuren Cache-Gruppen (Einstellungen, Kategorien, Sprachvariablen) weiterhin aktiv sind.
Die saubere Variante: Redis statt Dateien
Wer die Wahl hat, betreibt den Objekt-Cache gar nicht auf dem Dateisystem. JTL-Shop 5 unterstützt Redis als Cache-Backend. Redis hält alles im Arbeitsspeicher, kennt eine harte Speicherobergrenze mit automatischer Verdrängung der ältesten Einträge — und legt exakt null Dateien an. Das Inode-Thema verschwindet damit vollständig.
Der Haken: klassische Shared-Hosting-Pakete bieten keinen Redis. Auf einem eigenen Server oder bei einem Anbieter mit Redis-Option ist es die deutlich robustere Wahl.
Und die Bots?
Zwei Maßnahmen haben sich bewährt, jeweils als Ergänzung, nicht als Ersatz:
- Kombinierte Filter blockieren. Ein Nutzer kombiniert selten mehr als drei Filter gleichzeitig, ein Crawler geht durch bis zur letzten Kombination. Eine Regel, die URLs mit sehr vielen Filterparametern mit Status 410 beantwortet, wirkt trennscharf. Die Schwelle sollte man vorher im Zugriffslog verifizieren, statt sie zu raten — sonst sperrt man echte Kunden aus.
noindexauf Filterseiten. Verhindert nicht das Crawlen, sorgt aber dafür, dass die Kombinationen nicht in den Index geraten und dort weitere Crawler anziehen.
Was in der Praxis dagegen kaum trägt: Rate-Limits nach IP und Erkennung über den User-Agent. Verteilte Crawler bleiben je IP unterhalb jeder sinnvollen Schwelle und geben sich als normaler Browser aus.
Die Kurzfassung für den nächsten Fall
- Zählen Sie ohne
-type f, sonst sehen Sie nur die halbe Wahrheit. - Prüfen Sie, welcher Cache-Typ eingestellt ist.
advancedfilebedeutet zwei Inodes pro Eintrag. - Schalten Sie den Filter-Cache ab, statt häufiger aufzuräumen.
- Wenn Redis verfügbar ist: umstellen, Thema erledigt.
- Filterkombinationen begrenzen — mit einer Schwelle, die Sie am eigenen Log geprüft haben.
Der Kunde ist inzwischen bei knapp über 100.000 Einträgen, stabil, ohne Mahnung. Aufgeräumt wird weiterhin — nur muss der Cronjob jetzt nicht mehr gegen einen Berg anlaufen, der schneller wächst als er abtragen kann.
Häufige Fragen
Was sind Inodes und warum begrenzt mein Hoster sie?
Ein Inode ist der Verwaltungseintrag, den ein Linux-Dateisystem für jede Datei, jeden Ordner und jeden Symlink anlegt. Die Anzahl ist beim Anlegen des Dateisystems festgelegt und nicht beliebig erweiterbar. Webhoster begrenzen Inodes pro Paket, weil sehr viele kleine Dateien Backups, Virenscans und Dateisystemprüfungen für alle Kunden auf demselben Server ausbremsen.
Kann ich den JTL-Shop-Cache einfach komplett abschalten?
Können ja, sollten Sie aber nicht. Ohne Objekt-Cache werden Kategorien, Einstellungen und Sprachvariablen bei jedem Seitenaufruf neu aus der Datenbank gelesen — das kostet spürbar Ladezeit. Sinnvoller ist es, den Cache-Typ gezielt einzuschränken oder auf Redis umzustellen, der überhaupt keine Dateien anlegt.
Wie finde ich heraus, wie viele Dateien mein Shop-Verzeichnis enthält?
Per SSH mit `find /pfad/zum/shop -mindepth 1 | wc -l` — bewusst ohne den Filter `-type f`, denn sonst zählen Symlinks nicht mit. Genau dieser Filter ist der Grund, warum viele Aufräumskripte die Hälfte des Problems übersehen.