Monday, January 2, 2012

Varnish är min vän (och libvmod-header)

Efter många timmars testande och googlande har jag byggt en lösning som jag är hyfsat nöjd med.

Några saker jag lärt mig:
- Varnish har inte så bra stöd för ETags som jag trodde
- Varnish är sjukt snabbt
- Ingen gillar Set-Cookie-headern; och browsers gillar speciellt inte när man merge:ar flera Set-Cookie-headers till en (även om det ska vara lugnt enligt RFC:n)

Då till problemet; jag ville ha flera parallella versioner av en resurs, som utåt sett skulle ha samma URL. För att veta vilken av dessa som ska hämtas behöver man minst ett anrop till backend.

Utöver detta vill jag helst kunna få cookies från backend vid varje sidvisning; om det absolut inte går vill jag strippa Set-Cookie-rader helt från cachade svar. Detta skulle dock resultera i att en massa oanvända sessioner startas, men skulle nog gå att komma förbi på något sätt.

ETags galore?
Först försökte jag olika lösningar med ETags, Vary: ETag, 304-svar från backend, osv. Lyckades dock inte få det att funka, då Varnish inte skickar If-None-Match om inte klienten skickat det, och då blir det ingen gemensam cache; och alltså ganska värdelöst.

302 Found
Så, vi skippar ETags helt; vi behöver en unik URL för varje version av resursen. Kan vi då dölja denna URL för besökaren? Vi bakar helt enkelt in ETag:en i URL:en istället.

- Varnish frågar backend efter resursen med URL "/forum"
- Backend svarar med en 302-redirect till "/forum?etag=foo"

Här kommer det vackra, i vcl_fetch;
if(beresp.status == 302) {
        set req.url = beresp.http.location;
        return (restart);
}
Redirecten hanteras nu alltså internt av Varnish, klienten har ännu inte fått något tillbaka.

- Backend svarar på "/forum?etag=foo" med en helt vanlig sida, med lämpliga cache-headers
- Varnish cachar sidan med ETag:en i URL:en, och skickar den till klienten
- Nästa gång någon hämtar samma resurs och den fortfarande är kvar i cachen med samma ETag så kommer den bara träffa backend en gång (då den får 302-svaret)

Cookies då?
Vi gör ju faktiskt redan en helt vanlig request mot backend som har klientens cookies med sig. Vi sparar helt enkelt Set-Cookie-raderna vi får tillbaka, i den nya requestens header X-COOKIES i vcl_fetch:
if(beresp.status == 302) {
        set req.url = beresp.http.location;
        header.copy(beresp.http.Set-Cookie, req.http.X-COOKIES);
        return (restart);
}
Sedan i vcl_deliver så plockar vi ut dessa och lägger i Set-Cookie på vårt svar:
unset resp.http.Set-Cookie;
header.copy(req.http.X-COOKIES, resp.http.Set-Cookie);
Detta vore inte möjligt utan libvmod-header, som tillhandahåller header.copy-funktionen. Om man hade använt Varnish inbyggda funktioner för att sätta Set-Cookie så hade man nämligen bara fått med den första Set-Cookie-raden om det varit flera.

No comments: