Das ist die Entwickler-Fassung zu Warum dein Magento-2-Shop trotz Varnish und Redis langsam wirkt. Das andere reichst du dem Händler weiter. Dieser Artikel ist für dich.
Wenn du mehr als eine Handvoll Magento-2-Shops auditiert hast, kennst du das Bild: Varnish läuft, Redis läuft, die Hardware ist in Ordnung, und die Hit-Rate weit unter dem liegt, wo sie sein sollte. Die Maschine langweilt sich. PHP macht die ganze Arbeit. Auf den Shops, bei denen wir dieses Muster im vergangenen Jahr gesehen haben, ist es dieselbe kurze Liste von Ursachen. Nachstehend, wie wir sie finden, in der Reihenfolge, die deine Zeit wert ist.
TL;DR
Hit-Raten unter 30 Prozent auf einem Magento-2-Shop haben fast immer eine dieser Ursachen, meistens mehrere gleichzeitig:
-
Set-Cookie: PHPSESSID=...auf Antworten, die als Gast-Antworten cachebar sein sollten. - PHP sendet
Cache-Control: no-storeoderno-cache, weil eine Erweiterung$response->setNoCacheHeaders()aufgerufen oder eine Sitzung gestartet hat. - Die als FPC-Tag-Store konfigurierte Redis-Datenbank hat 0 Keys, tag-basierte Purges schlagen still fehl, und Varnish speichert getaggte Objekte nie sauber.
-
X-Magento-Varyfragmentiert den Cache pro Kundengruppe, pro Währung, pro Store View, meist ohne dass das jemand beabsichtigt hätte. - VCL ohne Normalisierung der Layered-Nav-Parameter und ohne Stripping der Marketing-Parameter (
gclid,utm_*,fbclid,srsltid), sodass jeder Ad-Klick ein kalter MISS ist. - Ein oder zwei Marketplace-Erweiterungen, die
setPublicCookie()aufrufen oder auf jeder Anfrage aus einem Block, Observer oder Layout-XML-Include eine Sitzung starten.
Fixe das Cookie und den No-Store-Header, erholt sich die Hit-Rate eines Shops üblicherweise deutlich innerhalb eines Nachmittags. Der Rest der Liste ist das, was dich auf 90+ Prozent bringt.
Die tatsächliche Hit-Rate messen
Bevor du irgendetwas anfasst: Basiswerte nehmen. Ohne Zahl hast du keinen Bug, sondern ein Bauchgefühl.
Der kürzeste Weg ist varnishstat:
1varnishstat -1 | grep -E 'cache_hit|cache_miss'
Gesunde Ausgabe auf einem normalen Storefront: cache_hit eine Größenordnung über cache_miss. Wenn cache_miss in derselben Gegend liegt wie cache_hit oder sogar darüber, bist du in dem Terrain, um das es hier geht. Notiere die exakten Zahlen. Du brauchst einen Vorher/Nachher-Vergleich.
Für einen Live-Blick liefert varnishstat -1 einen Snapshot; varnishstat (ohne Flags) ist die interaktive TUI. Der MAIN-Abschnitt enthält cache_hit, cache_miss, cache_hitpass und cache_hitmiss. Ein hoher cache_hitpass-Wert bedeutet, dass Anfragen auf ein hit-for-pass-Objekt treffen. Das ist meistens ein Symptom des Cookie/No-Store-Problems weiter unten, nicht ein eigener Bug.
Wenn du pro-URL-Granularität willst, sample eine Minute Varnishlog:
1varnishlog -g request -q 'ReqMethod eq "GET"' -i ReqURL,RespHeader:X-Magento-Cache-Debug,VCL_call
Gesund: Die meisten GETs auf Kategorie- und Produkt-URLs loggen VCL_call HIT. Wenn du MISS und X-Magento-Cache-Debug: MISS auf einem nackten /herrenschuhe ohne Cookies siehst, hast du ein Cachebarkeitsproblem, kein Trafficproblem.
Der übliche Verdächtige: Set-Cookie auf jeder Antwort
Das ist der Killer. Varnish wird mit einer VCL ausgeliefert, die jede Antwort mit Set-Cookie als nicht cachbar behandelt, und Magentos Standard-VCL behält dieses Verhalten für alles außerhalb seiner Whitelist bei. Ein einziger unbedachter setPublicCookie()-Aufruf in einem Block, der auf jeder Seite gerendert wird, und du hast den gesamten Storefront zu Pass-Through-Traffic gemacht.
Untersuche die Antwort. Das ist der einzelne nützlichste Befehl in diesem Artikel:
1curl -sI https://storefront.example.com/ | grep -iE 'cache-control|set-cookie|x-magento-vary|x-cache|age'
Gesunde Ausgabe auf einer Gast-Startseite:
1Cache-Control: max-age=86400, public, s-maxage=864002X-Magento-Vary: ... # Hash vorhanden, das ist gut3Age: 43 # Varnish liefert ein gecachtes Objekt aus
Kaputte Ausgabe, was du wahrscheinlich gleich sehen wirst:
1Cache-Control: no-store, no-cache, must-revalidate, max-age=02Set-Cookie: PHPSESSID=abc123...; path=/
Wenn Set-Cookie: PHPSESSID auf einer Anfrage auftaucht, bei der keine Sitzung hätte starten sollen, hat irgendetwas in der Request-Kette eine gestartet. PHPs session_start() ist eifrig: Alles, was $_SESSION liest, Magento\Framework\Session\SessionManager::start() aufruft oder eine Klasse nutzt, die das transitiv tut, gibt das Cookie aus. Varnish verweigert dann das Caching.
Um die Quelle schnell zu finden, grepp app/code nach den üblichen Kandidaten:
1grep -rn --include='*.php' -E 'setPublicCookie|->start\(\)|SessionManager|CustomerSession|CheckoutSession' app/code/ | grep -v Test
Gesund: Treffer nur in Checkout-, Customer- und Wishlist-Modulen. Alles in einem Block, Observer, Layout-Processor oder generischen Helper, der auf Katalogseiten gerendert wird, ist verdächtig. Wir haben Session-Starts in Header-Blocks gefunden, in Promo-Bannern, in GeoIP-Helfern und einmal in einem „Währungs-Detektor", der mit Sessions nichts zu tun hatte.
session_start. Magentos Session-Manager wrappt das. Grepp nach SessionManager, CustomerSession, CheckoutSession, ->start( und setPublicCookie. Letzterer ist heimtückisch: setPublicCookie() startet selbst keine Sitzung, aber verbreitete Marketplace-Cookie-Consent-Erweiterungen fahren pro Aufruf eine DB-Abfrage, dazu gleich mehr.
Der zweite Verdächtige: Cache-Control no-store aus PHP
Manchmal ist das Cookie nicht das Problem. Die Antwort selbst sagt Varnish, sie nicht zu cachen, und zwar über PHP. Das passiert, wenn eine Erweiterung Cache-Control: no-store oder no-cache direkt in die Response-Header schreibt, meist in einem beforeDispatch-Plugin oder einem dispatch-Observer.
Magentos \Magento\PageCache\Model\Config::isEnabled() und das PageCacheHeader-Plugin sollten max-age und public setzen. Wenn etwas danach läuft und überschreibt, hast du verloren. $response->setNoCacheHeaders() ist der übliche Täter.
Prüfe deine gemergte Plugin-/Observer-Konfiguration:
1bin/magento dev:di:info 'Magento\Framework\App\Response\Http' 2>&1 | head -50
Du suchst nach Plugins in der Magento\Framework\App\Response\Http-Pipeline, die dort nichts zu suchen haben. Ein Plugin aus einem Nicht-Core-Modul mit Around auf sendHeaders oder sendResponse ist ein Warnsignal. Lies es. Wenn es konditional Cache-Control: no-store basierend auf einem Cookie setzt, das auf jeder Anfrage existiert, hast du deine Regression gefunden.
Ein minimales korrektes Plugin sieht so aus:
1<?php2final class PreserveCacheHeaders3{4 public function beforeSendResponse(\Magento\Framework\App\Response\Http $subject): void5 {6 // tut auf cachbaren Routen nichts an den Cache-Headern7 }8}
Ein kaputtes sieht aus wie $subject->setHeader('Cache-Control', 'no-store', true), unkonditional aufgerufen. Diese eine Zeile reißt deine Hit-Rate auf jeder Route, die sie berührt, auf Null.
FPC-Redis-Store leer? Tag-basiertes Purging ist kaputt
Magento speichert die Full-Page-Cache-Tags in einer dedizierten Redis-Datenbank — dem Index, über den Varnish-Tag-Purges koordiniert werden. Welche Datenbanknummer das ist, hängt ausschließlich von der Konfiguration in app/etc/env.php ab. Es gibt keine Anforderung, dass es DB0, DB1 oder DB2 ist. Finde zuerst heraus, was tatsächlich konfiguriert ist:
1grep -A 10 'page_cache' app/etc/env.php
Such den Wert database unter dem backend_options-Eintrag der page_cache-Frontend-Konfiguration. Das ist der DB-Index, den du abfragen willst.
1redis-cli -n <deine_page_cache_DB> dbsize
Gesund: irgendwo zwischen einigen tausend und mehreren hunderttausend Keys auf einem aktiven Shop. Null ist es nicht.
Wenn die FPC-Datenbank (integer) 0 zurückgibt, geh die übliche Checkliste durch:
-
app/etc/env.phphatcache->frontend->page_cacheauf eine Redis-Instanz zeigen (nichtdefault, nicht fehlend), mit einem explizit gesetztendatabase-Schlüssel. -
CM_Cache_Backend_Redisist die konfigurierte Backend-Klasse, und der Datenbank-Index inenv.phpstimmt mit dem überein, was du abfragst. -
bin/magento cache:enable full_pagemeldet aktiviert. - Varnishs
vcl_backend_responsein deiner VCL entfernt nicht denX-Magento-Tags-Header aus der Backend-Antwort (alte Custom-VCLs machen das manchmal „der Sauberkeit halber" und zerbrechen damit Tag-Purges).
Warum das zählt: ohne einen befüllten FPC-Tag-Store funktioniert tag-basierte Invalidierung nicht. bin/magento cache:flush trifft den Default-Cache, Varnishs PURGE-Request geht raus, aber der Tag-Store, mit dem das koordiniert sein soll, ist leer. Varnish scheint zu cachen. Aber Objekte laufen über TTL aus, nicht über Content-Änderung. Die Hit-Rate sieht auf statischen Katalogen okay aus und bricht am Tag nach einem Bulk-Produktimport ein, weil jede Produktseite misst und Varnish den gesamten Cache kalt neu füllt.
X-Magento-Vary: wenn Desktop und Mobile deinen Cache fragmentieren
X-Magento-Vary ist der Weg, mit dem Magento Varnish sagt: „Diese Antwort ist spezifisch für diese Kundengruppe / Währung / Store View." Der Hash deckt die im DI-Container registrierten HTTP-Context-Variablen ab — standardmäßig: Login-Status, Kundengruppe, Währung und Store. Extensions können weitere hinzufügen. Varnish speichert ein gecachtes Objekt pro eindeutigem Vary-Hash. Zu viele unterschiedliche Hashes, und du cachst pro Besucher, was gleichbedeutend ist mit nicht cachen.
Prüfe, worauf du tatsächlich fragmentierst:
1for i in 1 2 3 4; do curl -sI -A "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15" \2 https://storefront.example.com/ | grep -i x-magento-vary; done
Gesund: viermal derselbe Hash. Der Vary-Hash ist pro Geräteklasse stabil und kippt nicht auf jeder Anfrage.
Die übliche Pathologie: Desktop und Mobile erzeugen unterschiedliche Vary-Hashes für dieselbe logische Seite und verdoppeln deinen Arbeitsbestand. Wenn du getrennte Desktop-/Mobile-Themes über Magentos Design-Config fährst, ist das Absicht, und die Hit-Rate-Obergrenze ist effektiv halbiert. Wenn nicht und du trotzdem zwei Hashes pro URL siehst, kippt irgendetwas in design/*/view.xml oder in einem Theme-Plugin den Hash unnötig auf Basis des User-Agents.
Desktop-TTFB gegen Mobile-TTFB auf derselben URL aus demselben Standort zu samplen ist ein billiger Sanity-Check. Wenn Desktop auf cachbaren Kategorieseiten 2x oder mehr langsamer ist als Mobile, misst es wahrscheinlich auf Desktop und hittet auf Mobile (oder andersherum), weil du den Cache entlang einer Geräte-Achse fragmentiert hast, die du nicht geplant hattest.
VCL-Hygiene: Layered-Nav-Parameter und Marketing-Parameter
deine VCL vcl_recv entscheidet, was im Cache-Key landet. Zwei Stellschrauben sind wichtig.
Layered-Navigation-Parameter. Eine URL wie /herrenschuhe?brand=nike&price=50-100&p=2 sollte cachbar sein. Jeder Magento-Shop trifft das. Wenn deine VCL den Query-String nicht normalisiert (Parameter alphabetisch sortieren, in Kleinbuchstaben, Nulls entfernen), sind ?brand=nike&price=50-100 und ?price=50-100&brand=nike zwei verschiedene Cache-Einträge. Multipliziere das über Filterkombinationen, und du bist zurück beim kalten Cache.
Der Standard-Fix ist ein std.querysort()-Aufruf und eine konservative Whitelist von Filter-Parametern in vcl_recv. Magentos Referenz-VCL in vendor/magento/module-page-cache/etc/varnish6.vcl enthält std.querysort(req.url) bereits. Wenn deine nicht, ergänz es.
Marketing-Parameter. gclid, fbclid, utm_source, utm_medium, utm_campaign, srsltid, _gl, epik, mc_cid, mc_eid sind für die Seite semantisch irrelevant. Das ist Attribution, die von Ad-Plattformen drangehängt wird. Wenn du sie in den Cache-Key aufnimmst, ist jeder einzelne Ad-Klick ein kalter MISS. Stripp sie vor dem Hashing:
1sub vcl_recv {2 # Marketing-Parameter vor dem Querysort entfernen3 set req.url = regsuball(req.url, "(\?|&)(gclid|fbclid|utm_[^=]+|srsltid|_gl|epik|mc_cid|mc_eid)=[^&]*", "");4 set req.url = regsub(req.url, "\?&", "?");5 set req.url = regsub(req.url, "\?$", "");6 set req.url = std.querysort(req.url);7}
Gesund danach: Google-Ads-Traffic-TTFB vergleichbar mit organischem Traffic-TTFB. Kaputt davor: Google-Ads-Traffic kann auf derselben URL 2- bis 3-mal langsamer sein als organischer, weil jeder gclid-Wert einen neuen Cache-Eintrag anlegt.
X-Magento-Vary, private_content_version, form_key, mage-cache-sessid, section_data_ids) und liefert gelegentlich gecachte Seiten an eingeloggte Kunden aus. Wenn dein CDN eine cache_everything-Regel auf HTML-Pfaden hat, deaktivier sie.
Modulmuster, die wir immer wieder sehen
Dieselbe Handvoll Marketplace-Module taucht in diesen Audits immer wieder auf. Drei Muster sind erwähnenswert.
Plumrocket CookieConsent: eine DB-Abfrage pro setPublicCookie(). Das Modul fährt eine Datenbankabfrage für jeden gesetzten Public Cookie, und es setzt mehrere pro Seite. Auf einem kalten Seitenaufruf siehst du vier bis zehn zusätzliche Queries allein aus dieser einen Erweiterung. Zusätzlich zu den Query-Kosten landet jeder setPublicCookie()-Aufruf in Response-Headern, über die Varnish dann entscheiden muss. Der Fix ist entweder das Modul zu entfernen oder den Cookie-Config-Lookup statisch im Request-Lifetime zwischenzuspeichern.
Bss_PromotionBar: Session-Start auf jeder Anfrage. Der Observer, der die Kundengruppen-ID für den Promo-Bar zieht, greift in die Customer-Session, löst session_start() aus, und das führt zu Set-Cookie: PHPSESSID auf Antworten, die als Gast cachbar sein sollten. Ein Cache-Layer um den Gruppen-ID-Lookup hilft gegen die DB-Last, aber solange der Observer die Sitzung anfasst, cacht Varnish die Antwort immer noch nicht. Der einzige echte Fix ist, den Lookup auf eine zustandslose Quelle umzuschreiben: ein Cookie, das von einer kleinen Edge-Side-Logik gesetzt wird, oder ein Client-Side-AJAX-Personalisierungs-Aufruf, nachdem das cachbare HTML gelandet ist.
Bss_FacebookPixel: das richtige Muster. Erwähnenswert, weil es der positive Fall ist. Die Session-Storage-Klasse des Moduls lässt sich als In-Memory-DTO, auf den Request gescoped, ohne Aufrufe in Magentos Session-Manager umschreiben. Keine Session-Contention, kein Set-Cookie, und der Pixel feuert trotzdem. Wenn du ein Magento-Modul schreibst, das „Session-scoped" Daten für einen Gast will: das ist das Muster. Request-scoped DTO, nicht Session.
Amasty_Geoip und Plumrocket_GeoIPLookup. GeoIP-Module neigen stark dazu, schwere Datenbanken zu laden oder Sitzungen auf jeder Anfrage zu starten, um das Land zu bestimmen. Auf einem Varnish-fronted Shop ist jede per-Request-GeoIP-Logik mit Caching inkompatibel. Verschieb den Lookup entweder auf einen Edge-Worker und gib das Ergebnis als Header an Varnish weiter, über den du varyen kannst, oder tu es client-seitig, nachdem die cachbare Hülle gelandet ist.
MSP_NoSpam und ältere Mageworx-Module. Historische Muster hier umfassten zu weit gefasste Session-Starts auf jedem Controller-Dispatch. Die meisten sind zugunsten zustandsloser Ansätze deprecated. Wenn du auf einem alten Build unterwegs bist und solche Module noch mitschleppst, ist Entfernen meistens die richtige Wahl.
Allgemeine Regel: Jedes Modul, das cachbares HTML personalisieren will, macht etwas falsch. Personalisierung gehört in den private_content_version / section_data_ids AJAX-Layer, nicht in die initiale HTML-Antwort.
Weiterführende Artikel
-
Redis Session Locking in Magento 2: wann
disable_locking=1Ihren Checkout rettet. Varnish und Redis Session Locking potenzieren sich auf demselben langsamen Seiten-Pfad; wenn deine Hit-Rate stimmt und der Shop sich auf eingeloggten Interaktionen trotzdem langsam anfühlt, ist der Lock der nächste Punkt. - Observer-Rekursion in Magento 2: die versteckten Kosten von sales_order_save_after. Wenn die langsame Seite die Danke-Seite nach dem Checkout ist, ist die Cache-Seite unschuldig und eine Observer-Kaskade die übliche Ursache.
- Die N+1-Falle auf Magento-2-Produktseiten: getUsedProducts und Freunde. Die andere Hälfte von „PDP fühlt sich langsam an". Cache-Misses erklären eine langsame Anfrage; N+1 erklärt, warum jede Anfrage langsam ist, sobald sie PHP erreicht.
Die 8-Punkte-Checkliste für Magento-Varnish-Audits
-
01
Hit-Rate als Baseline nehmen
Führe varnishstat -1 | grep -E 'cache_hit|cache_miss' aus und notiere das Verhältnis. Unter 30 % ist kaputt. Unter 80 % ist untersuchungswürdig. Nach jedem Fix erneut laufen lassen, um die Bewegung zu bestätigen.
-
02
Response-Header auf einer Gast-Seite prüfen
curl -sI auf Startseite und Kategorie. Cache-Control sollte max-age=N, public sein. Kein Set-Cookie auf Gast-Antworten. X-Magento-Vary präsent, Age auf Wiederholungs-Requests steigend.
-
03
Nach versehentlichen Session-Starts greppen
grep -rn --include='*.php' -E 'setPublicCookie|->start\(\)|SessionManager|CustomerSession|CheckoutSession' app/code/. Jeder Treffer außerhalb von Checkout-, Customer- oder Wishlist-Code ist verdächtig. Lesen, prüfen, ob Session wirklich nötig ist.
-
04
FPC-Redis-Datenbank auf Belegung prüfen
grep -A10 'page_cache' app/etc/env.php um den konfigurierten Datenbank-Index zu finden, dann redis-cli -n <diese_DB> dbsize. Null bedeutet, dass der FPC-Tag-Store kaputt ist, tag-basierte Purges still fehlschlagen und der Cache nur über TTL altert. Erst env.php fixen, dann bin/magento cache:flush und erneut prüfen.
-
05
Response-Header-Pipeline auditieren
bin/magento dev:di:info 'Magento\Framework\App\Response\Http'. Jedes Nicht-Core-Plugin auf sendHeaders oder sendResponse ist ein Kandidat für überschriebene Cache-Control-Header. Lesen und bestätigen, dass es no-store nicht erzwingt.
-
06
X-Magento-Vary-Stabilität bestätigen
Dieselbe URL viermal mit demselben UA und denselben Cookies anfragen. Vary-Hash muss übereinstimmen. Wenn er driftet, varyt etwas den Hash auf Request-scoped State, das es nicht sollte.
-
07
Query-Strings in der VCL normalisieren
std.querysort() muss in vcl_recv stehen. gclid, fbclid, utm_*, srsltid, _gl, epik, mc_cid, mc_eid vor dem Querysort entfernen. Die Layered-Nav-Parameter auf eine Whitelist setzen, die du tatsächlich getrennt cachen willst.
-
08
Bekannt kritische Erweiterungen identifizieren
Plumrocket CookieConsent (DB pro Cookie), Bss_PromotionBar (Session-Start im Observer), GeoIP-Module (per-Request-DB-Lookups). Prüfen, ob sie aktiv sind. Falls ja: entfernen, zustandslos umschreiben oder die Hit-Rate-Kosten einkalkulieren.
Bei 25 % festgefahren und die Ideen gehen aus?
Wir fahren fokussierte Magento-2-Performance-Audits. du bekommst die varnishstat-Zahlen, die genaue Erweiterung, die deine Hit-Rate killt, einen VCL-Diff und eine priorisierte Fix-Liste. Keine 80-seitige PDF. Zwei bis drei Tage, direkt am Code.