Best Practices für die Verwendung des Vary-Headers
Was ist Vary?
Vary ist einer der leistungsstärksten HTTP-Antwort-Header. Richtig eingesetzt kann er Wunder bewirken. Aber leider wird dieser Header oft falsch verwendet, was zu miserablen Hitraten führen kann. Schlimmer noch: Wenn er gar nicht verwendet wird, wenn er eigentlich verwendet werden sollte, werden unter Umständen falsche Inhalte ausgeliefert.
Anstatt Sie nur auf die Vary-Spezifikation zu verweisen, möchte ich Ihnen den Vary-Header anhand seines häufigsten Anwendungsfalls erklären: Komprimierung.
Wenn Sie das Apache Programm mod_deflate verwenden, wird der korrekte Vary-Header automatisch zu Ihren Antworten hinzugefügt. Dasselbe gilt für das gzip Feature von Fastly. Da es sich dabei um einen sehr einfachen Anwendungsfall von Vary handelt, werde ich daran demonstrieren, wie Vary funktioniert.
undefined
Anatomie einer HTTP-Anfrage
Normalerweise werden gecachte Objekte anhand von lediglich zwei Teilen einer in einem der Fastly Caches eingehenden Anfrage gesucht: dem Pfad (und dazugehörigen Query-String, falls vorhanden) und dem Host-Header.
Im Folgenden sehen Sie eine gängige Anfrage für http://example.com/somepage.php:
GET **/somepage.php** HTTP/1.1
Host: **example.com**
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36
Accept-Language: en-US,en;q=0.8
Accept-Encoding: gzip,deflate,sdch
Der Browser sendet zusammen mit der URL eine Menge Informationen. Der Accept-Header gibt an, welche Art von Inhalten der Browser bevorzugt, der User-Agent gibt an, um welche Browserversion es sich handelt, Accept-Language enthält eine Liste an vom Nutzer konfigurierten Sprachen (und Varianten) und Accept-Encoding gibt an, welche Komprimierungsschemata der Browser unterstützt.
Aus praktischen Gründen interessiert uns nur gzip
. deflate
ist veraltet und sdch
wird von niemandem außer Google verwendet.
Komprimierung in der Praxis
Hypothetische Situation: Sie haben einen Webserver ohne mod_deflate
, aber Sie haben herausgefunden, wie Sie die Komprimierung mit gzip in PHP durchführen können. Wenn Sie also gzip
im Accept-Encoding
-Header sehen, weisen Sie den Content-Encoding: gzip
-Header an, dem Browser mitzuteilen, was Sie tun, und komprimieren den Antworttext.
Stellen Sie sich nun vor, dass dieser Server der Origin-Server für einen Fastly Service ist, und diese Seite gecacht werden kann. Was passiert, wenn ein Browser, der keine Komprimierung unterstützt, diese Seite als Erster anfordert? Das würde zu einer nicht komprimierten Seite in unserem Cache führen.
Ist das ein Problem? Nur ein kleines. Wenn ein Browser, der Komprimierung unterstützt, diese Seite anfordert, erhält er die unkomprimierte Version aus unserem Cache und kann sie problemlos darstellen. Die unkomprimierte Version ist allerdings größer und kostet Sie bei der Übertragung mehr Bandbreite, was zu einer langsameren Auslieferung an den Endnutzer und zu höheren Kosten auf Ihrer Seite führt.
Das größere Problem ist, wenn eine Anfrage für ein Objekt zuerst von einem Browser kommt, der eine Komprimierung durchführt, und die komprimierte Version dann in unserem Cache gespeichert wird. Denn wenn diese Datei nun von einem Browser angefragt wird, der Komprimierung nicht unterstützt, kann dieser Browser die Datei dem Nutzer nicht ordentlich anzeigen.
Wie Sie Vary zum Lösen von Problemen verwenden
Es gibt zwei Möglichkeiten, dieses Problem zu beheben. Zunächst könnten Sie in Erwägung ziehen, Ihren Cache-Schlüssel in Ihrer Fastly Konfiguration zu ändern, was jedoch zusätzliche Herausforderungen mit sich bringt:
Sie müssten sowohl die komprimierte als auch die nicht komprimierte Version separat bereinigen.
Ein Fehler an dieser Stelle könnte dann dazu führen, dass alle URLs dasselbe einzelne Objekt zurückgeben.
Um diese beiden Probleme zu vermeiden, können Sie stattdessen den Vary-Header verwenden.
Der Vary-Header teilt jedem HTTP-Cache mit, welche Teile des Anfrage-Headers, abgesehen vom Pfad und Host-Header, beim Versuch, das richtige Objekt zu finden, zu berücksichtigen sind. Er tut dies, indem er die Namen der relevanten Header auflistet – in diesem Fall Accept-Encoding
. Wenn es mehrere Header gibt, die Einfluss auf die Antwort nehmen, werden sie alle in einem einzigen Header, getrennt durch Kommata, aufgelistet.
Die Antwort-Header für eine komprimierte Antwort sollten in etwa so aussehen:
HTTP/1.1 200 OK
Content-Length: 3458
Cache-Control: max-age=86400
Content-Encoding: gzip
**Vary: Accept-Encoding**
Und bei einer nicht komprimierten Antwort so:
HTTP/1.1 200 OK
Content-Length: 8365
Cache-Control: max-age=86400
**Vary: Accept-Encoding**
Beachten Sie, dass der Vary-Header in der Antwort vorhanden ist, unabhängig davon, ob eine Komprimierung durchgeführt wird oder nicht.
Warum ist das so? Sehen wir uns an, was passiert, wenn der Header tatsächlich vorhanden ist.
Zuerst kommt eine Anfrage für ein Objekt ohne Accept-Encoding-Header. Das Objekt befindet sich nicht im Cache, deshalb fordern wir es vom Origin-Server an, der es mit dem Vary-Header zurückgibt. Wenn Fastly das Objekt im Cache speichert, wird der Vary-Header vermerkt und die Werte der relevanten Header aus der Anfrage werden ebenfalls gespeichert.
Nun gibt es ein Objekt im Cache mit einem kleinen Flag, das besagt, „nur für Anfragen zu verwenden, die keinen Accept-Encoding
in der Anfrage enthalten“.
Stellen Sie sich nun vor, dass ein Browser, der Komprimierung unterstützt, die Anfrage wie oben beschrieben sendet. Zuerst suchen wir das Objekt anhand des Host-Headers und des Pfads. So finden wir zwar das Objekt, die Anfrage enthält aber einen Accept-Encoding-Header mit der Anweisung gzip,deflate,sdch
, was nicht mit dem diesem Objekt zugewiesenen Flag übereinstimmt.
Fastly geht also zurück zu Ihrem Origin-Server, und dieses Mal sollten wir eine komprimierte Version des Objekts zurückbekommen. Diese Antwort wird dann mit einem Flag gespeichert, das angibt, dass diese Version nur für Anfragen mit Accept-Encoding: gzip,deflate,sdch
verwendet werden soll.
Wenn der Vary-Header in der ersten Antwort nicht vorhanden gewesen wäre, hätten wir nicht gewusst, dass wir das gecachte Objekt nicht für die zweite Anfrage verwenden können.
Normalisierung
Sie fragen sich vielleicht, ob heutzutage alle Browser den gleichen Accept-Encoding-Header senden.
Die Antwort lautet leider Nein.
Ich habe mir eine Stichprobe von 100.000 Anfragen an einen unserer Caches angesehen und 44 verschiedene Accept-Encoding-Header ermittelt. (Wenn Sie an meiner Methodik oder den genauen Zahlen interessiert sind, klicken Sie einfach hier.)
Wenn alle diese Anfragen für dieselbe URL gewesen wären, hätten wir 44 „verschiedene“ Versionen in unserem Cache gehabt. Aber weil der Origin-Server nur zwei Versionen generieren kann – eine, wenn gzip
im Accept-Encoding
-Header vorhanden ist, und eine, wenn das nicht der Fall ist –, sind das 42 Anfragen an den Origin-Server, die wir gerne vermeiden möchten.
Da sich der Origin-Server nur dafür interessiert, ob gzip vorhanden ist oder nicht, könnten wir den Accept-Encoding-Header doch einfach dahingehend normalisieren, dass er entweder gzip
enthält oder überhaupt nicht vorhanden ist.
Es gibt immer nur zwei Varianten des Accept-Encoding-Headers in unseren Anfragen. Deshalb sind auch immer nur zwei Varianten des Objekts in unserem Cache gespeichert.
Mit ein wenig VCL-Code lässt sich das ganz einfach lösen:
# do this only once per request
if (req.restarts == 0) {
# normalize Accept-Encoding to reduce vary
if (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} else {
unset req.http.Accept-Encoding;
}
}
Da Sie vielleicht nach wie vor ein paar alte HTTP-Clients unterstützen möchten, fügen wir Unterstützung für deflate
hinzu und stellen sicher, dass Internet Explorer 6 sich nicht um die Komprimierung kümmern muss (denn er ist bekanntermaßen ziemlich schlecht darin).
# do this only once per request
if (req.restarts == 0) {
# normalize Accept-Encoding to reduce vary
if (req.http.Accept-Encoding) {
if (req.http.User-Agent ~ "MSIE 6") {
unset req.http.Accept-Encoding;
} elsif (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} elsif (req.http.Accept-Encoding ~ "deflate") {
set req.http.Accept-Encoding = "deflate";
} else {
unset req.http.Accept-Encoding;
}
}
}
Ein dem hier überaus ähnlicher VCL Codeabschnitt ist im Übrigen seit der Gründung unseres Unternehmens Teil der Fastly Master VCL.
Tipps zur Verwendung von Vary
Wie Sie sehen können, hätte das Hinzufügen eines einfachen Vary-Headers zu Ihrer Antwort ohne eine Normalisierung der Anfrage-Header eine ziemlich katastrophale Auswirkung auf die Anzahl der an den Origin-Server gesendeten Anfragen haben können. Das Einzige, was dies verhindert, ist die Standardnormalisierung, die Fastly durchführt.
Zunächst einmal sollten Sie Vary nicht auf einem Header mit vielen Variationen ohne Normalisierung anwenden.
Zweitens: Versuchen Sie beim Normalisieren, den Header auf maximal eine Handvoll Möglichkeiten zu reduzieren. Sie können dazu zum Beispiel die Werte in Ihrem VCL-Code hartkodieren, anstatt sich nur auf regsub()
zu verlassen. Eine gute Faustregel: Bei beliebten Inhalten mit langen Ablaufzeiten skaliert die Menge des Traffics zu Ihrem Origin-Server linear mit der Menge der möglichen Werte.
Im Folgenden finden Sie einige Vary-Header, die ich im Laufe der Jahre gesehen habe. Ich zeige, warum sie ungeeignet sind und wie man sie normalisieren kann:
Vary: User Agent
Es gibt buchstäblich Tausende verschiedene User-Agent-Strings. In einer Stichprobe von 100.000 Anfragen fand ich knapp 8.000 Varianten.
Stellen Sie sich vor, Sie möchten Ihre Website auf Mobilgeräten in einem anderen Format anzeigen lassen. In diesem Beispiel wird der User-Agent-Header durch einen einfachen String ersetzt, der anders als die regulären Werte für diesen Header aussieht. Wenn Sie diesen String verwenden, sollten Sie unbedingt sicherstellen, dass Ihr Origin-Server weiß, wie er damit umzugehen hat.
Sie könnten zum Beispiel folgenden VCL-Code verwenden:
if (req.http.User-Agent ~ "(Mobile|Android|iPhone|iPad)") {
set req.http.User-Agent = "mobile";
} else {
set req.http.User-Agent = "desktop";
}
Vary: Referer
Besonders beliebte Inhalte werden von vielen anderen Seiten verlinkt, und jede Suchanfrage bei Google hat eine eindeutige URL. Beides führt zu einer hohen Anzahl eindeutiger Referer-Werte.
Nehmen wir einmal an, Sie möchten Besuchern, die über eine Drittseite auf eine Ihrer Seiten gelangt sind, eine Art Begrüßungs-Pop-up anzeigen, nicht aber Besuchern, die innerhalb Ihrer Website navigieren.
Der VCL-Code, den Sie dafür brauchen, sieht folgendermaßen aus:
if (req.http.Referer ~ "^https?://www.example.com/") {
set req.http.Referer = "http://www.example.com/";
} else {
unset req.http.Referer;
}
Vary: Cookie
Cookie
ist wahrscheinlich einer der eindeutigsten Anfrage-Header und daher für unsere Zwecke ungeeignet. Cookies enthalten oft Authentifizierungsdetails. In diesem Fall versuchen Sie besser, Seiten nicht zu cachen, sondern sie nur weiterzuleiten. Wenn Sie sich für das Caching mit Tracking-Cookies interessieren, finden Sie hier mehr dazu.
Manchmal werden Cookies aber auch für A/B-Tests genutzt. In diesem Fall ist es eine gute Idee, Vary für einen nutzerdefinierten Header zu verwenden und den Cookie-Header intakt zu lassen. So ersparen Sie sich eine Menge zusätzlicher Logik, um sicherzustellen, dass der Cookie-Header für URLs, die ihn benötigen (und wahrscheinlich nicht cachefähig sind), beibehalten wird.
sub vcl_recv {
# set the custom header
if (req.http.Cookie ~ "ABtesting=B") {
set req.http.X-ABtesting = "B";
} else {
set req.http.X-ABtesting = "A";
}
...
}
...
sub vcl_fetch {
# vary on the custom header
if (beresp.http.Vary) {
set beresp.http.Vary = beresp.http.Vary ", X-ABtesting";
} else {
set beresp.http.Vary = "X-ABtesting";
}
...
}
...
sub vcl_deliver {
# Hide the existence of the header from downstream
if (resp.http.Vary) {
set resp.http.Vary = regsub(resp.http.Vary, "X-ABtesting", "Cookie");
}
# Set the ABtesting cookie if not present in the request
if (req.http.Cookie !~ "ABtesting=") {
# 75% A, 25% B
if (randombool(3, 4)) {
add resp.http.Set-Cookie = "ABtesting=A; expires=" now + 180d "; path=/;";
} else {
add resp.http.Set-Cookie = "ABtesting=B; expires=" now + 180d "; path=/;";
}
}
...
}
Fügen Sie in vcl_recv
, das sich vor dem Cache Lookup befindet, mit VCL einen nutzerdefinierten Header hinzu, der auf dem Cookie basiert. Dabei wird davon ausgegangen, dass der Cookie-Wert entweder B
oder A
ist. Wenn das Cookie nicht vorhanden ist, gilt standardmäßig A
.
Fügen Sie anschließend in vcl_fetch
Ihren nutzerdefinierten Header in Vary ein und ersetzen Sie ihn abschließend in vcl_deliver
durch Cookie
. So bleibt Ihr nutzerdefinierter Header nicht nur vor der Außenwelt verborgen, sondern Ihre Endnutzer erhalten trotzdem die korrekte Variante, wenn es zufällig einen gemeinsamen Cache zwischen Fastly und den Endnutzern gibt.
Jetzt haben Sie also A/B-Tests, bei denen Browser einfach nur ein weiteres Cookie sehen und Ihr Origin-Server anhand eines sehr simplen Headers entscheiden kann, was zu tun ist (X-ABtesting
).
Vary: *
Verwenden Sie diesen Code auf keinen Fall!
Die HTTP-RFC besagt, dass, wenn ein Vary-Header den speziellen Namen*
enthält, jede Anfrage für diese URL als einmalige (und nicht cachefähige) Anfrage behandelt werden soll.
Viel klarer lässt sich das Ganze durch Cache-Control: private
darstellen. Außerdem können Sie so jedem zwischengeschalteten Cache signalisieren, dass das Objekt niemals gespeichert werden soll, was viel sicherer ist.
Vary: Accept-Encoding, User Agent, Cookie, Referer
Kein Scherz, solche komplett ohne Normalisierung angewendete Header habe ich tatsächlich schon gesehen. Wie Sie sich sicherlich denken können, liegt die Chance, dass es dadurch jemals zu einem Cache Hit kommt, vielleicht bei eins zu Googolplex.
Kompliziertere Anwendungsfälle
Bisher habe ich Vary für die Komprimierung besprochen, eine (einfache) Logik für die Geräteerkennung eingeführt und eine A/B-Testlogik auf der Edge eingerichtet. In meinem nächsten Blogpost werde ich auf fortgeschrittenere Anwendungen von Vary eingehen, wie GeoIP und Accept-Language.