Shopware HTTP Cache mit Varnish XKey

Vorwort

Shopware entfernt die ursprüngliche Methode für die Verwendung von Varnish als HTTP-Cache mit der Version v6.7.0 und stellt eine Alternative über Varnish XKey bereit. Varnish XKey ist Teil der Varnish Module Collection, welches nicht in Varnish selbst integriert ist. Wir stellen hier 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 Entwickler Dokumentation bereit:

https://developer.shopware.com/docs/guides/hosting/infrastructure/reverse-http-cache.html#configure-shopware


Voraussetzung

  • Varnish-Server mit Varnish 7.6
  • Varnish Modules Collection


Bei Managed Servern wird die Installation von Varnish 7.6 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 interne TRUSTED_PROXIES Konfiguration aktiviert.

# config/packages/framework.yaml

framework:
    trusted_proxies: '%env(TRUSTED_PROXIES)%'


Beispiel Konfiguration über .env:

TRUSTED_PROXIES=127.0.0.1,10.20.0.1/32,10.20.0.2/32


In dieser Beispiel-Konfiguration werden die beiden Varnish-Instanzen 10.20.0.1 und 10.20.0.2 als Proxy autorisiert.

Mehr Informationen zu den Trusted Proxies 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 Proxies 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.


Erstellen Sie die folgende Datei und fügen 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:
          - '10.20.0.2'

Zusätzlich muss die Umgebungsvariable SHOPWARE_HTTP_CACHE_ENABLED=1 in der .env Datei gesetzt werden.



Varnish 7.6 Konfiguration

Die Varnish-Konfiguration kann direkt über das Konfigurationsmodul im Kundencenter editiert und ausgerollt werden. Navigieren Sie zu Server → Ihr Server → 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 initial konfigurieren, empfehlen wir eine Test-Instanz zur Evaluierung der korrekten Konfiguration.


Beispiel Konfiguration der default.vcl von Varnish mit XKey Hard-Purge:

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 = "XX.XX.XX.XX";
    .port = "80";
}

# ACL for purgers IP. (This needs to contain app server ips)
acl purgers {
    "127.0.0.1";
    "localhost";
    "::1";
}

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);
    }

    # Always pass these paths directly to php without caching
    # 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 req.http.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.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.cache-hash != "") {
        hash_data("+context=" + req.http.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.cache-hash;
    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;

    if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
        unset beresp.http.Surrogate-Control;
        set beresp.do_esi = true;
        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 not want the client to cache
    if (resp.http.Cache-Control !~ "private" && req.url !~ "^/(theme|media|thumbnail|bundles)/") {
        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";
    }

    # 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;
}



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 aktualisiert.


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);



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 zu 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 Systemd-Services vorgenommen werden, wodurch neue Standardgrößen gesetzt werden.


Lösung

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 \
...
         -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 der Varnish Instanz 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.