Varnish Cache performant revalidieren

Für eine Applikation haben wir eine Möglichkeit gesucht, um einzelne Objekte bzw. URLs in einem vorgeschalteten Varnish Cache vor Ablauf der maximalen Caching-Dauer zu erneuern. Die Anwendung sollte dabei möglichst wenig belastet werden. Die Clients sollen zudem bei der Erneuerung von Inhalten keine erhöhten Antwortzeiten wahrnehmen.

Wir setzen Varnish als performanten HTTP Cache vor verschiedenen Applikationen ein, um die Auslastung der Anwendungsserver zu minimieren und den Clients latenzarme Antworten zu liefern.

Varnish ist durch die Varnish Configuration Language (VCL) extrem flexibel konfigurierbar. Durch vordefinierte Subroutinen (Subs) und Aktionen kann Einfluss auf die HTTP-Requests und Responses in verschiedenen Verarbeitungsstufen des Cache genommen werden. Die Varnish-Dokumentation bietet auch eine sehr gute grafische Darstellung zur Verarbeitung von Requests und dem Weg eines Requests durch die Subroutinen von Varnish.

Die Verarbeitung eines HTTP Requests in Varnish sieht grob wie folgt aus:

  1. Ein HTTP Request wird zuerst von der Sub vcl_recv empfangen, wo ggf. erste Modifikationen oder Prüfung erfolgen.
  2. Anschließend wird der Request an die Sub vcl_hash geleitet. Hier wird geprüft ob für die angefragte URL bereits ein Objekt im Cache vorliegt. Es können hierbei verschiedene Informationen einbezogen werden wie Domainname, URL-Pfad, HTTP Header, usw.
  3. Ist schon ein zum Request passendes Objekt vorhanden, wird der Request an die Sub vcl_hit geleitet, von wo die Auslieferung aus dem Cache erfolgt.
  4. Sollte kein passendes Objekt im Cache vorliegen, wird der Request an die Sub vcl_miss geleitet. Diese kümmert sich mit weiteren Subs um die Abfrage vom Backend (den Applikationsservern). Anschließend erhält der Client eine Antwort und das Objekt wird basierend auf der Varnish-Konfiguration und den HTTP Response Headern des Backends ggf. im Cache gespeichert.

Optionen

Von Varnish werden verschiedene Methoden zur Invalidierung oder forcierten Revalidierung von Inhalten angeboten: Varnish Users Guide – Purging and banning

PURGE und BAN

Die Optionen PURGE und BAN zur Invalidierung sind für uns nicht geeignet, da mit diesen ein Objekt komplett aus dem Cache entfernt wird. Ruft ein Client das invalidierte Objekt auf, so muss es von Varnish erst bei der Applikation angefragt werden, wodurch eine Latenz für den User entsteht. Außerdem kann Varnish nach einer Invalidierung mit diesen Methoden keine Stale Objects mehr ausliefern, wenn die Backend Server der Applikation offline oder überlastet sind.

req.hash_always_miss

Mit der VCL-Variable req.hash_always_miss ist es möglich ein Objekt ohne Auswirkungen auf den Client zu revalidieren. Ist diese Variable auf true gesetzt, so fragt Varnish die aufgerufene URL immer beim Backend-Server an und speichert die Antwort später im Cache ab. Solange die Antwort vom Backend noch nicht vorliegt, wird anderen Clients, die parallel die selbe URL abfragen, ein evtl. schon im Cache vorliegendes Objekt zurückgeliefert.

Wenn die Variable req.hash_always_miss für einen Request gesetzt ist, dann erfolgt immer eine Weiterleitung an das Backend ohne ein evtl. im Cache vorliegendes Objekt zu nutzen. Die Variable req.hash_always_miss kann einem Request aber auch nur bei Erfüllung bestimmter Bedingungen gesetzt werden, sodass eine Revalidierung nur von bestimmten Usern oder unter anderen Voraussetzungen erfolgen kann.

Lösungsmöglichkeit

Simple Umsetung

Hier ein einfaches Konfigurationsbeispiel: Wird ein HTTP GET Request für die zu erneuernde URL mit einem Token im HTTP Request Header X-Refresh-Token an Varnish gesendet, so wird die Variable req.hash_always_miss auf true gesetzt und das betreffende Objekt im Cache erneuert. Der Revalidierungsprozess geschieht automatisch durch Varnish in der Sub vcl_hash, die einen Cache Miss erzeugt und an das Backend weiterleitet.

sub vcl_recv {
  [...]
  if(req.http.X-Refresh-Token == "mysecret") {
    set req.hash_always_miss = true;
  }
  [...]
}

Damit erreichen wir sehr einfach eine Revalidierung ohne Beeinträchtigung der Clients.

Backend nicht überlasten

Ein weiteres Problem musste allerdings noch gelöst werden: Wir betreiben mehere Varnish-Instanzen im Verbund. Nicht alle Varnish-Instanzen müssen dauerhaft für die Anwendung in Betrieb sein. Ein Objekt liegt daher nicht zwangsläufig in allen Varnish-Instanzen vor.

Wenn wir eine Revalidierung mit req.hash_always_miss gegen alle Instanzen ausführen würden, wird die Anwendung mit zusätzlichen Requests belasten. Diese Situation wollen wir möglichst vermeiden.

Als Lösung kann die Variable req.hash_always_miss in der Sub vcl_recv erst gesetzt werden, wenn von Varnish ermittelt wurde, ob für die URL schon im Cache vorhanden ist oder nicht. Diese Ermittlung des Cache-Status kann über die Sub vcl_hash realisert werden. Ist für eine URL bereits ein Objekt im Cache vorhanden, so wird der Request an die Sub vcl_hit weitergeleitet. Ist die URL noch nicht im Cache vorhanden, wird an die Sub vcl_miss weitergeleitet. In der Subs vcl_hit kann dann ein Variable gesetzt werden, um der Sub vcl_recv den Cache-Status mitzuteilen.

Beim standardmäßigen Verarbeitungsprozess eines Requests gelangt dieser nicht von vcl_hit zurück an vcl_recv, daher müssen wir uns hier der Restart-Funktion der VCL bedienen. Durch einen Restart wird die gesamte VCL für einen Request nochmal durchlaufen, wobei Änderungen am Request aber nicht verloren gehen.

Der geplante Verarbeitungsweg einer Revalidierungsanfrage ist nachfolgend einmal grafisch dargestellt:

Verarbeitung einer Revalidierungsanfrage
Verfeinerte Konfiguration

Nachfolgend ist eine mögliche Konfiguration für die genannten Optimierung beschrieben.

In der Sub vcl_recv setzen wir die Variable req.hash_always_miss nur, wenn festgestellt wurde, ob eine Revalidierung überhaupt nötig ist. Die Feststellung des Cache-Status einer URL erfolgt weiter unten in der vcl_hit.

sub vcl_recv {
  [...]
  if(req.restarts > 0 && req.http.X-Refresh-Required && req.http.X-Refresh-Token == "mysecret") {
    set req.hash_always_miss = true;
  }
  [...]
}

Wenn die zu revalidierende URL bereits im Cache vorliegt, wird der Request nach dem Empfang durch vcl_recv durch vcl_hash an vcl_hit geleitet. In vcl_hit wird im Request der HTTP Request Header X-Refresh-Required auf true gesetzt und ein Restart der VCL ausgeführt. Der gesetzte HTTP Header zeigt der vcl_recv im zweiten VCL-Durchlauf an, dass das Objekt im Cache vorliegt. In vcl_recv kann dann die Variable req.hash_always_miss gesetzt werden.

sub vcl_hit {
  [...]
  if (req.http.X-Refresh-Token == "mysecret") {
    set req.http.X-Refresh-Required = true;
    return(restart);
  }
  [...]
}

Ist laut Sub vcl_hash für die URL kein Objekt im Cache vorhanden, so wird die Revalidierungsanfrage an vcl_miss geleitet. Dort wird der Request durch die Action error beendet und dem Client das über einen HTTP Code mitteilen. So wird bei einem versuchten Refresh von noch nicht gecachten Inhalten eine überflüssige Anfrage an die Backend-Server vermieden.

sub vcl_miss {
  [...]
  if (!req.http.X-Refresh-Required && req.http.X-Refresh-Token == "mysecret") {
      error(204, "Miss in cache");
  }
  [...]
}

Eine Revalidierungsanfrage von gecachten Inhalten kommt im zweiten VCL-Durchlauf auch bei der Sub vcl_miss an. Die Weiterleitung an vcl_miss geschieht durch die Sub vcl_hash, da die Variable req.hash_always_miss=true gesetzt ist.

Damit die Revalidierungsanfrage mit noch gesetztem HTTP Request Header X-Refresh-Token nicht abgerochen wird, wie es bei Revalidierungen für noch nicht gecachte Inhalte passiert, muss hier geprüft werden, ob der Request Header X-Refresh-Required nicht true ist.

Fehler minimieren

Zusätzlich ist es sinnvoll, dass Varnish während der Revalidierung bei Fehlern seitens des Backends keine Inhalte aus dem Cache entfernt. Wenn für eine URL während eines Backend Fetch vom Backend ein HTTP Code größer 500 zurückgeliefert wird, dann sollen normale Clients weiterhin das im Cache vorhande Objekt erhalten:

sub vcl_backend_response {
  [...]
  if(bereq.is_bgfetch && beresp.status >= 500) {
    return (abandon);
  }
  [...]
}

Falls dieser Artikel dein Interesse geweckt hat, we are hiring 😉