Skip to main content Skip to page footer

Einrichten eines modularen Multi-Host Varnish-Setups unter NixOS

Erstellt von Philipp Herzog | | Blog

Speziell für PHP-Anwendungen kann das richtige Caching und Cache-Handling unglaublich wichtig sein, um eine Website so performant wie möglich zu halten.

Unser Standard-Setup für PHP-Anwendungen, die normalerweise in Wordpress laufen, ist eine Proxy-Kette aus

Nginx -> Varnish -> HAProxy -> ein oder mehrere LAMP-Backends, 
häufig kombiniert mit einem Redis.

Die Nginx-Instanz nimmt alle Verbindungen an und leitet sie auf der Grundlage des Inhalts der Anfrage, zum Beispiel des Host-Headers, weiter. Sie leitet sie dann an einen Varnish weiter, der entweder eine gecachte Version der Antwort des Backends zurückgibt oder sie an eine HAProxy-Instanz weiterleitet, die wiederum die Anfrage an einen der Server in einem ihrer Backends weiterleitet, wo die Anfrage schließlich von einer der LAMP-Instanzen verarbeitet wird.

Da die Anforderungen an die Cache-Verarbeitung von Kunde zu Kunde unterschiedlich sind, bietet uns jede Schicht dieses Aufbaus eine große Flexibilität, um das spezifische Caching-Verhalten genau richtig einzustellen. Übliche Beispiele für Dinge, die eine Setup-spezifische Feinabstimmung erfordern, sind die Handhabung von Sessions, Content-Paywalls zum Beispiel von Verlagen und Zeitungen sowie große Dateien (Assets).

Ein Sonderfall dieser Einrichtung ist die gemeinsame Nutzung derselben Proxy-Kette durch mehrere Hosts. Bei diesem Setup werden Anfragen durch dieselben Nginx-, Varnish- und HAProxy-Instanzen geleitet und dann an die entsprechenden Backends weitergeleitet, normalerweise basierend auf einem speziellen X-Backend-Header, der in unserer Nginx-Konfiguration gesetzt wird, um das richtige Backend für diese Anfrage anzugeben. Sowohl bei Nginx als auch bei HAProxy ist das Einrichten einer Multi-Host-Konfiguration relativ einfach, da sie nur dazu dienen, eingehende Anfragen auf eine nicht-zuständige Weise weiterzuleiten.
 

Varnish

Unser bisheriger Ansatz bestand darin, eine einzige Varnish-Konfiguration zu schreiben und das Caching-Verhalten pro Anfrage auf der Grundlage des von Nginx gesetzten X-Backend-Headers festzulegen. Dieser Ansatz hat jedoch zwei große Schwachstellen:

1. Da die Konfiguration in einer einzigen Datei enthalten ist, kann ihre Verwaltung sehr umständlich sein. Dies gilt insbesondere, wenn unser Deployment-Tool batou verwendet wird und die hostspezifischen Einstellungen für Varnish auf mehrere Git-Repositories aufgeteilt sind, was bedeutet, dass sie irgendwie zusammengeführt werden müssen.

2. Die Anpassung der Konfiguration eines Hosts erfordert einen Neustart des Varnish-Dienstes, was dazu führt, dass nicht verwandte Hosts eine verminderte Leistung erfahren, bis der gesamte Cache wieder aufgewärmt ist.

Wie sich herausstellt, hat Varnish eine eingebaute Lösung für diese beiden Probleme: Labels.

Eine übliche Varnish-Konfiguration sah bisher so aus:

default.vcl:

backend haproxy_site1 {
	.host = "10.0.0.1";
}

backend haproxy_site2 {
	.host = "10.0.0.2";
}

sub vcl_recv {
	if (req.http.X-Backend == "site1") {
		set req.backend_hint = haproxy_site1;
        return (hash);
	} else if (req.http.X-Backend == "site2") {
		set req.backend_hint = haproxy_site2;
		return (hash);
	} else {
		return (synth(404));
	}
}

sub vcl_backend_response {
	if (bereq.http.X-Backend == "site1") {
		set beresp.ttl = 10m;
	} else if (bereq.http.X-Backend == "site2") {
		set beresp.ttl = 10d;
	}
}

Mit Hilfe von Labels kann diese eine Datei in drei aufgeteilt werden: eine Toplevel-Konfigurationsdatei zur Auswahl des geeigneten Handlers und eine hostspezifische Konfiguration für jede der beiden Domains. Das Dummy-Backend muss hier hinzugefügt werden, da eine gültige Varnish-Konfigurationsdatei mindestens ein Backend angeben muss, da sich sonst der Compiler beschwert.

default.vcl:

vcl 4.0;

backend dummy {
	.host = "127.0.0.1:0";
}

sub vcl_recv {
	if (req.http.X-Backend == "site1") {
		return (vcl(label-site1));
	} else if (req.http.X-Backend == "site2") {
		return (vcl(label-site2));
	} else {
		return (synth(404));
	}
}

Und dann wird für jede der Websites eine Konfiguration wie die folgende die Arbeit erledigen.

site1.vcl:

vcl 4.0;

backend haproxy_site1 {
	.host = "10.0.0.1";
}

sub vcl_recv {
   set req.backend_hint = haproxy_site1;
   return (hash);
}

sub vcl_backend_response {
   set beresp.ttl = 10m;
}

Die Konfiguration von Site2 sieht genau so aus, nur mit leicht anderen Werten.

Um diese Art von geteilter Konfiguration zu verwenden, müssen Sie varnish mitteilen, wo diese Bezeichnungen zu finden sind. Da die Labels zur Laufzeit injiziert werden, müssen Sie zuerst die Labels laden und dann die Konfigurationsdatei, die diese Labels verwendet. Schließlich können Sie varnish anweisen, zur neuen Toplevel-Konfiguration zu wechseln.

$ sudo -u varnish varnishadm
200
-----------------------------
Varnish Cache CLI 1.0
-----------------------------
Linux,5.15.166,x86_64,-jnone,-smalloc,-sdefault,-hcritbit
varnish-7.4.3 revision b659b7ae62b44c05888919c5c1cd03ba6eaec681

Type 'help' for command list.
Type 'quit' to close CLI session.

varnish> vcl.load vcl-site1 site1.vcl
200

varnish> vcl.label label-site1 vcl-site1
200

varnish> vcl.load vcl-default default.vcl
200

varnish> vcl.label label-default vcl-default
200

varnish> vcl.use label-default
200
VCL 'label-default' now active
varnish>

Sollte sich die Varnish-Konfiguration der Website ändern, muss nur noch die neue Konfiguration geladen, das Label angepasst und die alte Konfiguration verworfen werden.

$ sudo -u varnish varnishadm
200
-----------------------------
Varnish Cache CLI 1.0
-----------------------------
Linux,5.15.166,x86_64,-jnone,-smalloc,-sdefault,-hcritbit
varnish-7.4.3 revision b659b7ae62b44c05888919c5c1cd03ba6eaec681

Type 'help' for command list.
Type 'quit' to close CLI session.

varnish> vcl.load vcl-site1-new site1-new.vcl
200

varnish> vcl.label label-site1 vcl-site1-new
200

varnish> vcl.discard vcl-site1
200

varnish>

Glücklicherweise können diese Anweisungen in eine Datei eingefügt und an varnish übergeben werden, damit es sie nach dem Start ausführt, oder sie können einfach ausgeführt werden, indem sie an varnishadm als Argumente übergeben werden. Jetzt müssen nur noch alle Konfigurationsdateien und die Skripte zum Laden und Aktivieren der Labels verwaltet und mit etwas systemd-Magie schön zusammengefügt werden.

NixOS

Da unsere Infrastruktur auf NixOS läuft, war es naheliegend, das Modulsystem für die Verwaltung des Varnish-Dienstes zu wählen. Wenn Sie noch nicht wissen, was NixOS ist, empfehle ich Ihnen einen Blick auf nixos.org zu werfen.

Nixpkgs bietet bereits einen perfekt brauchbaren Varnish-Dienst, allerdings fehlt es ihm ein wenig an Modularität für unseren Anwendungsfall. Sie können ein paar Optionen wie das Statusverzeichnis, zusätzliche zu ladende Module, die Adresse, an die gebunden werden soll, eine ausführliche Konfiguration und zusätzliche Kommandozeilenflags definieren. Letzteres kann für unseren früheren Ansatz verwendet werden, bei dem sich alle Backends eine einzige Konfigurationsdatei teilen, allerdings kann es nicht ohne weiteres in Verbindung mit Labels verwendet werden. Wir haben beschlossen, auf diesem Dienst aufzubauen, indem wir die extraCommandLine-Option nutzen, um ein Skript an varnish zu übergeben, das alle Konfigurationsdateien in der richtigen Reihenfolge lädt und den generierten systemd-Dienst so anpasst, dass er sie lädt, kennzeichnet, verwendet und bei Bedarf im Reload-Abschnitt des Dienstes verwirft.

Das endgültige Design erinnert an das Nginx-Modul mit seinem vhost-ähnlichen Ansatz zur Deklaration der Varnish-Konfiguration. Das obige Beispiel könnte implementiert werden als

{
   flyingcircus.services.varnish.virtualHosts = {
      "site1" = {
         condition = "req.http.X-Backend == \"site1\"";
         config = ''
            vcl 4.0;

            backend haproxy_site1 {
               .host = "10.0.0.1";
            }

            sub vcl_recv {
               set req.backend_hint = haproxy_site1;
               return (hash);
            }

            sub vcl_backend_response {
               set beresp.ttl = 10m;
            }
         '';
      };

      "site2" = {
         condition = "req.http.X-Backend == \"site2\"";
         config = ''
            vcl 4.0;

            backend haproxy_site2 {
               .host = "10.0.0.2";
            }

            sub vcl_recv {
               set req.backend_hint = haproxy_site2;
               return (hash);
            }

            sub vcl_backend_response {
               set beresp.ttl = 10d;
            }
         '';
      };
   };
}

Das Modul generiert dann ein Skript für varnishadm, um alle Konfigurationsdateien zu laden und zu kennzeichnen. Es erzeugt eine Toplevel-Konfigurationsdatei, die Anfragen an das richtige Label weiterleitet, basierend auf den Bedingungen jedes Hosts, und es behandelt Änderungen an jeder Konfiguration, lädt die neue Konfiguration und kennzeichnet sie neu, um die neue Konfiguration zu aktivieren, ohne varnish neu starten zu müssen. Als zusätzlicher Bonus wird auch versucht, jede Konfiguration zur Build-Zeit zu kompilieren, um zu verhindern, dass versehentlich zu einer ungültigen Konfiguration gewechselt wird.

Ein weiterer großer Vorteil der Verwendung des NixOS-Modulsystems ist die Möglichkeit, Konfigurationen zusammenzuführen, so dass die oben genannten Konfigurationen in eine Datei für jeden virtualHost aufgeteilt werden können, so dass die Konfigurationen mehrerer Standorte auf derselben VM mit Varnish bereitgestellt werden können, ohne dass sie alle von Hand zusammengeführt werden müssen.

Zurück
Rabbit runs over a field