Shopware HTTP Cache mit Varnish XKey
Shopware entfernt Redis als Abhängigkeit für Varnish als HTTP-Cache mit der Version 6.7.0 und stellt eine Alternative über Varnish XKey bereit. Varnish XKey ist Teil der Varnish Module Collection, welche nicht in Varnish Cache selbst integriert ist. Wir stellen ein entsprechendes Debian-Repository bereit, welches die Installation der Varnish Module Collection über apt ermöglicht und somit die Integration von Varnish XKey vereinfacht.
Für die Konfiguration von Varnish mit XKey stellt Shopware eine vollständige Konfiguration in der Shopware-Entwicklerdokumentation bereit:
Voraussetzung
- Shopware 6 Cluster
- Varnish-Server mit Varnish 7.7
- Varnish Modules Collection
Bei Managed Servern wird die Installation von Varnish 7.7 sowie der Varnish Modules Collection von unserem Support vorgenommen. Sofern Sie die Software installieren möchten, kontaktieren Sie gerne unseren Support.
Trusted Proxies
Tragen Sie in der Konfiguration TRUSTED_PROXIES die Quell-IP-Adresse des Proxyservers ein. Wir empfehlen hier den Betrieb innerhalb eines creoline VPC-Netzwerkes, um die interne Infrastruktur zu demilitarisieren.
Ab Shopware 6.6 muss zunächst eine framework.yaml konfiguriert werden, die die Shopware-internen TRUSTED_PROXIES Konfiguration aktiviert.
# config/packages/framework.yaml
framework:
trusted_proxies: '%env(TRUSTED_PROXIES)%' Beispielkonfiguration über .env:
TRUSTED_PROXIES=127.0.0.1,10.20.0.1/32,10.20.0.2/32 In dieser Beispielkonfiguration werden die beiden Varnish-Instanzen 10.20.0.1 und 10.20.0.2 als Proxy autorisiert.
Mehr Informationen zu den Trusted Proxys finden Sie in der offiziellen Symfony-Dokumentation unter Symfony Proxy Docs.
Shopware 6 XKey-Konfiguration
Damit Shopware 6 den korrekten Cache-Control Header setzt, ist die Konfiguration des Reverse Proxys erforderlich. Mithilfe der folgenden Konfiguration kann sichergestellt werden, dass Shopware anstelle des Cache-Control Header-Wertes private, no-cache den Wert public, must-revalidate setzt und die Caches entsprechend in den Varnish-Servern via BAN Requests invalidiert.
Bevor Sie die Konfiguration von Shopware durchführen, lohnt es sich, einen Blick in die Fehleranalyse zu werfen und die dort beschriebenen Einstellungen ggf. im Vorfeld vorzunehmen. Somit kann i.d.R. eine Downtime vermieden werden, da im Vorfeld und bspw. ohne Staging-Cluster schwer abzuschätzen ist, wie viel Overhead in den HTTP-Headern entstehen wird.
Erstellen Sie die folgende Datei und fügen Sie entsprechend den folgenden Inhalt ein:
nano config/packages/shopware.yml # Be aware that the configuration key changed from storefront.reverse_proxy to shopware.http_cache.reverse_proxy starting with Shopware 6.6
shopware:
http_cache:
reverse_proxy:
enabled: true
use_varnish_xkey: true
hosts:
# Die Bezeichnung für App-Slave Server beginnen bei 2, da Nr. 1 der App-Master Server ist
- '10.20.0.XX' # <- Varnish-Cache für App-Slave 2 (10.20.0.X)
- '10.20.0.ZZ' # <- Varnish-Cache für App-Slave 3 (10.20.0.Z) Zusätzlich muss die Umgebungsvariable SHOPWARE_HTTP_CACHE_ENABLED=1 in der .env Datei gesetzt werden.
Varnish 7.7 Konfiguration
Die Varnish-Konfiguration kann direkt über das Konfigurationsmodul im Kundencenter editiert und ausgerollt werden. Navigieren Sie zu Server → Server X → Konfigurationsdateien → Varnish, um die Varnish-Konfiguration anzupassen.
Bitte beachten Sie, dass das Speichern der Änderungen der Varnish-Konfiguration umgehend einen Configtest mit einem anschließenden Reload der Varnish-Instanz auslöst. Sollten Sie Varnish-Cache initial konfigurieren, empfehlen wir eine Test-Instanz zur Evaluierung der korrekten Konfiguration.
Shopware 6 Varnish XKey VCL
Wir stellen Ihnen die Shopware 6 Varnish XKey VCL bereit und unterstützen Sie gerne bei Fragen zur Integration. Bitte beachten Sie, dass die Sicherstellung der korrekten Funktion des Varnish-Caches in Ihrer Umgebung in Ihrer Verantwortung liegt. Sollten Plugins verwendet werden, die das HTTP-Cache-Verhalten von Shopware 6 nicht vollständig abbilden, kann dies zu unerwartetem Verhalten des Caches führen. Wir empfehlen daher, den Varnish-Cache zunächst in einer Test-Instanz oder idealerweise in einem Staging-Cluster zu prüfen, um mögliche Konflikte frühzeitig zu erkennen.
Die aktuellste Shopware 6 Varnish VCL kann direkt von GitHub bezogen werden:
vcl 4.1;
import std;
import xkey;
import cookie;
# Specify your app nodes here. Use round-robin balancing to add more than one.
backend default {
.host = "__SHOPWARE_BACKEND_HOST__";
.port = "__SHOPWARE_BACKEND_PORT__";
}
# ACL for purgers IP. (This needs to contain app server ips)
acl purgers {
"127.0.0.1";
"localhost";
"::1";
__SHOPWARE_ALLOWED_PURGER_IP__;
}
sub vcl_recv {
# Handle PURGE
if (req.method == "PURGE") {
if (client.ip !~ purgers) {
return (synth(403, "Forbidden"));
}
if (req.http.xkey) {
set req.http.n-gone = xkey.purge(req.http.xkey);
return (synth(200, "Invalidated "+req.http.n-gone+" objects"));
} else {
return (purge);
}
}
if (req.method == "BAN") {
if (client.ip !~ purgers) {
return (synth(403, "Forbidden"));
}
ban("req.url ~ "+req.url);
return (synth(200, "BAN URLs containing (" + req.url + ") done."));
}
# Only handle relevant HTTP request methods
if (req.method != "GET" &&
req.method != "HEAD" &&
req.method != "PUT" &&
req.method != "POST" &&
req.method != "PATCH" &&
req.method != "TRACE" &&
req.method != "OPTIONS" &&
req.method != "DELETE") {
return (pipe);
}
if (req.http.Authorization) {
return (pass);
}
# We only deal with GET and HEAD by default
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Micro-optimization: Always pass these paths directly to php without caching
# to prevent hashing and cache lookup overhead
# Note: virtual URLs might bypass this rule (e.g. /en/checkout)
if (req.url ~ "^/(checkout|account|admin|api)(/.*)?$") {
return (pass);
}
cookie.parse(req.http.cookie);
# set cache-hash cookie value to header for hashing based on vary header
# if header is provided directly the header will take precedence
if (!req.http.sw-cache-hash) {
set req.http.sw-cache-hash = cookie.get("sw-cache-hash");
}
set req.http.currency = cookie.get("sw-currency");
set req.http.states = cookie.get("sw-states");
if (req.url == "/widgets/checkout/info" && (req.http.sw-cache-hash == "" || (cookie.isset("sw-states") && req.http.states !~ "cart-filled"))) {
return (synth(204, ""));
}
# Ignore query strings that are only necessary for the js on the client. Customize as needed.
if (req.url ~ "(\?|&)(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=") {
# see rfc3986#section-2.3 "Unreserved Characters" for regex
set req.url = regsuball(req.url, "(pk_campaign|piwik_campaign|pk_kwd|piwik_kwd|pk_keyword|pixelId|kwid|kw|adid|chl|dv|nk|pa|camid|adgid|cx|ie|cof|siteurl|utm_[a-z]+|_ga|gclid)=[A-Za-z0-9\-\_\.\~%]+&?", "");
}
set req.url = regsub(req.url, "(\?|\?&|&)$", "");
# Normalize query arguments
set req.url = std.querysort(req.url);
# Set a header announcing Surrogate Capability to the origin
set req.http.Surrogate-Capability = "shopware=ESI/1.0";
# Make sure that the client ip is forward to the client.
if (req.http.x-forwarded-for) {
set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip;
} else {
set req.http.X-Forwarded-For = client.ip;
}
return (hash);
}
sub vcl_hash {
# Consider Shopware HTTP cache cookies
if (req.http.sw-cache-hash != "") {
hash_data("+context=" + req.http.sw-cache-hash);
} elseif (req.http.currency != "") {
hash_data("+currency=" + req.http.currency);
}
}
sub vcl_hit {
# Consider client states for response headers
if (req.http.states) {
if (req.http.states ~ "logged-in" && obj.http.sw-invalidation-states ~ "logged-in" ) {
return (pass);
}
if (req.http.states ~ "cart-filled" && obj.http.sw-invalidation-states ~ "cart-filled" ) {
return (pass);
}
}
}
sub vcl_backend_fetch {
unset bereq.http.currency;
unset bereq.http.states;
}
sub vcl_backend_response {
# Serve stale content for three days after object expiration
set beresp.grace = 3d;
unset beresp.http.X-Powered-By;
unset beresp.http.Server;
# This should happen before any early return via deliver, so that ESI can still be processed
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
# Reducing hit-for-miss duration for dynamically uncacheable responses
if (beresp.http.sw-dynamic-cache-bypass == "1") {
# Mark as "Hit-For-Miss" for the next n seconds
set beresp.ttl = 1s;
set beresp.uncacheable = true;
unset beresp.http.sw-dynamic-cache-bypass;
return (deliver);
}
if (bereq.url ~ "\.js$" || beresp.http.content-type ~ "text") {
set beresp.do_gzip = true;
}
if (beresp.ttl > 0s && (bereq.method == "GET" || bereq.method == "HEAD")) {
unset beresp.http.Set-Cookie;
}
}
sub vcl_deliver {
## we don't want the client to cache anything except assets and store-api responses
if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(theme|media|thumbnail|bundles|store-api)/") {
set resp.http.Pragma = "no-cache";
set resp.http.Expires = "-1";
set resp.http.Cache-Control = "no-store, no-cache, must-revalidate, max-age=0";
}
# Set a cache header to allow us to inspect the response headers during testing
if (obj.hits > 0) {
unset resp.http.set-cookie;
set resp.http.X-Cache = "HIT";
if (obj.ttl <= 0s && obj.grace > 0s) {
set resp.http.X-Cache = "STALE";
}
} else {
set resp.http.X-Cache = "MISS";
}
# invalidation headers are only for internal use
unset resp.http.sw-invalidation-states;
unset resp.http.xkey;
unset resp.http.X-Varnish;
unset resp.http.Via;
unset resp.http.Link;
} Backend definieren
Ein Varnish-Cache wird zur einfachen vertikalen Skalierung bei unseren Clustern immer im Verhältnis von 1:1 zu den App-Slaves betrieben. Das bedeutet, dass bei 2 App-Slave-Servern, ebenfalls 2 Varnish-Cache Server im Setup vorhanden sein müssen.
Im Backend default wird der 1:1 zugeordnete App-Slave-Server angegeben. Beispiel für Varnish-Server #2:
backend default {
.host = "10.20.0.Y"; # <- VPC IP-Adresse App-Slave #2
.port = "80"; # <- Webserver-Port für 10.20.0.Y
} Purger definieren
Die Purger sind die Server, welche den Cache über einen BAN-Request leeren dürfen. Was in erster Linie die App-Server sind. Es müssen alles App-Server angegeben werden.
# ACL for purgers IP. (This needs to contain app server ips)
acl purgers {
"127.0.0.1";
"localhost";
"::1";
"10.20.0.X"; # <- VPC IP-Adresse App-Master #1
"10.20.0.Y"; # <- VPC IP-Adresse App-Slave #2
"10.20.0.Z"; # <- VPC IP-Adresse App-Slave #3
# ...
} Load Balancer für BAN-Request über URL freigeben
Varnish XKey unterstützt ebenfalls den Ban über den Pfad shop.creoline-demo.de/products, sodass nur Teilbereiche der Webseite aus dem HTTP-Cache gelöscht werden. Oftmals wird der BAN-Request direkt über den Domainnamen aufgelöst, weswegen der Load Balancer in dem Fall ebenfalls als Purger hinterlegt werden muss.
Es muss sichergestellt sein, dass BAN-Requests seitens des Load Balancers ausschließlich von bestimmten IP-Adressen erlaubt sind. Beispielsweise kann hier die WAN IPv4-Adresse der App-Server in der Konfiguration des Load Balancers freigegeben werden. Andernfalls wäre es möglich, dass Dritte den Cache leeren können und Ihre Webseite somit an Performance verliert.
# ACL for purgers IP. (This needs to contain app server ips)
acl purgers {
"127.0.0.1";
"localhost";
"::1";
"10.20.0.X"; # <- VPC IP-Adresse App-Master #1
"10.20.0.Y"; # <- VPC IP-Adresse App-Slave #2
"10.20.0.Z"; # <- VPC IP-Adresse App-Slave #3
# ...
"10.20.0.1"; # <- VPC IP-Adresse Load Balancer
} Soft-Purge vs. Hard-Purge
Die obige Varnish-Konfiguration verwendet im Standard Hard-Purges, was dafür sorgt, dass eine Seite vom Cache entfernt wird und der nächste Request mehr Zeit benötigt. Um dem entgegenzuwirken, können Soft-Purges verwendet werden, welche dem Client einmal die veraltete Seite ausliefern und im Hintergrund dazu, den Cache für diese Webseite aktualisieren.
Soft-Purges können mit einer Anpassung der Varnish-Konfiguration verwendet werden, siehe Zeile 27 in der o. g. Varnish-Konfiguration:
# Hard-Purge
set req.http.n-gone = xkey.purge(req.http.xkey);
# Soft-Purge
set req.http.n-gone = xkey.softpurge(req.http.xkey); HTTP-Cache für angemeldete Benutzer oder Besucher mit Warenkörben
Der Shopware HTTP-Cache steht in der Standardeinstellung nur für nicht angemeldete Besucher ohne Warenkorb-Inhalte zur Verfügung. Sofern Sie keine Anpassungen für angemeldete Benutzer verwenden, kann der HTTP-Cache auch für angemeldete Benutzer mit Warenkörben aktiviert werden.
Stellen Sie vor Aktivierung dieser Funktion sicher, dass Sie keine Preise basierend auf Kunden oder Kundengruppen bereitstellen.
# config/packages/prod/shopware.yaml
shopware:
cache:
invalidation:
http_cache: [] Debugging
Die Standardkonfiguration entfernt alle HTTP-Header, bis auf den Age, welcher für die Bestimmung des Cache-Alters verwendet wird. Ein Age von 0 bedeutet, dass der Cache nicht funktioniert. Dies liegt meistens daran, dass der Cache-Control: public HTTP-Header nicht von der Web-Applikation gesetzt wurde.
Zur Überprüfung kann der Befehl curl wie folgt verwendet werden:
curl -vvv -H 'Host: <sales-channel-domain>' <app-server-ip> 1> /dev/null welche folgende Antwort zurückliefern sollte:
< HTTP/1.1 200 OK
< Cache-Control: public, s-maxage=7200
< Content-Type: text/html; charset=UTF-8
< Xkey: theme.sw-logo-desktop, ... Sofern der Cache-Control: public oder xkey: … HTTP-Header nicht in der Antwort zu finden ist, liegt dies wahrscheinlich an einer fehlerhaften Konfiguration in der Web-Applikation selbst.
Überprüfen Sie die Konfiguration von Shopware 6, ob der Reverse-Proxy Modus korrekt aktiviert wurde.
Fehleranalyse
HTTP-Status 503 Backend Fetch failed auf einzelnen Artikel- und/oder Kategorieseiten
Unter Umständen kann es sein, dass je nach Anforderung Ihres Shops, die Größe des HTTP-Headers, die in Varnish konfigurierte Standardgröße für ein oder mehrere Artikel- und Kategorieseiten übersteigt.
Prüfung
Verbinden Sie sich per SSH mit Ihrem Varnish-Server und führen Sie folgenden Befehl aus:
curl -I -H "Host: <sales-channel-domain>" -H "X-Forwarded-Proto: https" http://<app-server-ip>/pfad/mit/fehler/503 | wc -c Sollte das Ergebnis 8192 übersteigen, muss hier eine Anpassung des Varnish-Cache Systemd-Services vorgenommen werden, wodurch neue Standardgrößen gesetzt werden.
Lösung
Hierfür werden Root-Berechtigungen benötigt, bei Managed-Servern ist die Datei mittlerweile über das Konfigurationsmodul im Kundencenter verfügbar. Sollte die Datei nicht verfügbar sein, wenden Sie sich einmal an unseren Support.
Führen Sie den folgenden Befehl als Root-Benutzer aus:
systemctl edit varnish Passen Sie die ExecStart-Einstellung wie folgt an:
[Service]
ExecStart=/usr/sbin/varnishd -a :80 -a localhost:8443,PROXY -f /etc/varnish/default.vcl -P %t/%N/varnishd.pid -p feature=+http2 -s malloc,2g \
-p http_req_hdr_len=32k \
-p http_resp_hdr_len=32k \
-p http_resp_size=64k
Restart=on-failure Führen Sie anschließend ein Reload von Varnish-Cache aus, um die Konfiguration zu übernehmen.
systemctl reload varnish Sofern es sich bei Ihrem Server um einen Managed Server handelt, können die Befehle, die als Root-Benutzer ausgeführt werden müssen, ausschließlich durch unseren Support ausgeführt werden. Bitte kontaktieren Sie unseren Support, damit wir die gewünschten Änderungen durchführen können.
HTTP-Status Fehler 502 Bad Gateway
Prüfung FastCGI Proxy Buffers
Es besteht die Möglichkeit, dass die Größe der FastCGI Proxy Buffers in Nginx nicht für Varnish XKey ausreicht, weswegen hier die folgenden Einstellungen in der Server- oder Location-Direktive hinterlegt werden müssen.
Prüfen Sie die entsprechende Nginx Error-Log Datei auf folgende Fehlermeldung:
Der Dateiname error.log kann von Ihrer Konfiguration abweichen.
grep "upstream sent too big header" /var/log/nginx/error.log Lösung
Erhöhen Sie die folgenden Einstellungen innerhalb Ihrer Nginx Server- oder Location-Direktive wie folgt:
Der Dateiname webseite.conf kann von Ihrer Konfiguration abweichen.
nano /etc/nginx/conf.d/webseite.conf Option 1: Server-Direktive
server {
...
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
...
} Option 2: Location-Direktive
location XXX {
...
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
...
} Testen und laden Sie anschließend die Nginx-Konfiguration neu:
nginx -t && systemctl reload nginx