Hands-on


HTTP Caching with Varnish


Bulgaria PHP Conference, September 25th, 2015


© David Buchmann

Step 1

apt-get install varnish
            

Step 1

apt-get install varnish
            

Step 2

service apache2 restart
service varnish restart
            

Step 3

What could possibly go wrong?

httpstatusdogs.com

Overview




HTTP Refresher

What is a reverse proxy again?

HTTP is simple

GET /path

HTTP/1.1 200 OK
Content-Type: text/html

<html>...</html>
            

HTTP verbs

HTTP response codes

twitter.com/stevelosh/status/372740571749572610

Tools and helpers

                wget -Sq --spider varnish.lo/solutions/expiration.php
                curl -o /dev/null -sD - varnish.lo/solutions/expiration.php
            



HTTP Cache Control

Cache control headers

HTTP 1.1, RFC 2616, Sections 13.2 and 13.3

Cache Expiration

Cache-Control: s-maxage=3600, max-age=900
Expires: Thu, 15 May 2014 08:00:00 GMT
            
  1. s-maxage
  2. max-age
  3. Expires (HTTP 1.0 - avoid!)
  4. Default to default_ttl if nothing specified

Hands on: Cache headers

curl -sD - varnish.lo/noinfo.php
curl -sD - varnish.lo/exercises/expiration.php
            

Cache validation (I)

ETag: 82901821233
            
If-None-Match: 82901821233

304 Not Modified
            

Hands on: Etag

curl -sD - varnish.lo/exercises/etag.php
curl -H "If-None-Match: abc" -sD - varnish.lo/exercises/etag.php
            

Cache validation (II)

Last-Modified: Tue, 13 May 2014 08:13:20 GMT
            
If-Modified-Since: Tue, 13 May 2014 08:13:20 GMT

304 Not Modified
            

Hands on: If-Modified-Since

curl -sD - varnish.lo/exercises/last-modified.php
curl -H "If-Modified-Since: {previous timestamp}" -sD - varnish.lo/exercises/last-modified.php
            











There are dog images too, but I won’t show the 304 one.

Validation: ETag or Last-Modified

Know your tools

Do not cache

Cache-Control: s-maxage=0, private, no-cache
            

www.varnish-cache.org/trac/browser/bin/varnishd/default.vcl?rev=3.0 (Varnish 3)
www.varnish-cache.org/trac/browser/bin/varnishd/builtin.vcl?rev=4.0 (Varnish 4)

Hands on: Do not cache

curl -sD - varnish.lo/exercises/nocache.php
            

Default Varnish behaviour

Hands on: Cookie

curl -sD - varnish.lo/cookie.php
curl -H "Cookie: x" -sD - varnish.lo/solutions/expiration.php
            

Keep variants apart

Content depending on request headers

GET /resource
Accept: application/json
            
GET /resource
Accept: text/xml
            
Vary: Accept
            

Hands on: Content negotiation

curl -sD - varnish.lo/exercises/content-negotiation.php
curl -H "Accept: application/json" -sD - varnish.lo/exercises/content-negotiation.php
            


Let’s look into Varnish

Warning!


Varnish does what you tell it


Think carefully and test thoroughly

Varnish Configuration Language

Restart Varnish like a pro

VCL: Debug hit or miss, TTL

sub vcl_backend_response {
    set beresp.http.X-TTL = beresp.ttl;
}

sub vcl_deliver {
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
    } else {
        set resp.http.X-Cache = "MISS";
    }
}
            

VCL: Two applications

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}
backend legacy {
    .host = "127.0.0.1";
    .port = "8000";
}
sub vcl_recv {
    if (req.url ~ "^/archive/") {
        set req.backend_hint = legacy;
    } else {
        set req.backend_hint = default;
    }
}
            

VCL: Remove cookies

sub vcl_recv {
    if (req.http.Cookie) {
        if (req.url ~ "^/cache") {
            remove req.http.Cookie;
        }
    }
}
curl -H "Cookie: x" -sD - varnish.lo/solutions/expiration.php
            

Bad Practice: This is leaking knowledge about URLs to Varnish config

VCL: Custom TTL

header('X-Reverse-Proxy-TTL: 30');
C{ #include <stdlib.h>; }C
sub vcl_backend_response {
    if (beresp.http.X-Reverse-Proxy-TTL) {
        C{
            char *ttl;
            const struct gethdr_s hdr = { HDR_BERESP, "\024X-Reverse-Proxy-TTL:" };
            ttl = VRT_GetHdr(ctx, &hdr);
            VRT_l_beresp_ttl(ctx, atoi(ttl));
        }C
        unset beresp.http.X-Reverse-Proxy-TTL;
    }
}
...

Hands on: Custom TTL

# config/varnish/common.vcl
include "custom-ttl.vcl";
            
sudo service varnish restart
            
curl -sD - varnish.lo/exercises/custom-ttl.php
            

VCL can do a lot of things


But first make your application behave correctly!



Monitoring Varnish

What’s up?

Be on top of it

Average number of requests over last 60 seconds

varnishtop
            

Top URLs that are missing the cache:

varnishtop -i BereqUrl
            

Client or backend only:

varnishtop -c
varnishtop -b
            

Show header values:

varnishtop -I ReqHeader:Cookie
            

Get down to the nitty-gritty

Log all the things:

varnishlog
            

Again, only client or backend:

varnishlog -c
varnishlog -b
            

Log entries for specific URL:

varnishlog -q 'ReqUrl ~ "/exercises"'
varnishlog -q 'BereqUrl ~ "/exercises"'
            

Reading varnishlog

<< Request  >> 32770
-   Begin          req 32769 rxreq
-   Timestamp      Start: 1433683498.334761 0.000000 0.000000
-   Timestamp      Req: 1433683498.334761 0.000000 0.000000
-   ReqStart       127.0.0.1 45447
-   ReqMethod      PURGE
-   ReqURL         /cache.php
-   ReqProtocol    HTTP/1.1
-   ReqHeader      User-Agent: GuzzleHttp/6.0.1 curl/7.38.0 PHP/5.6.7-1
-   ReqHeader      Host: localhost:6181
-   ReqHeader      X-Forwarded-For: 127.0.0.1
-   VCL_call       RECV
-   VCL_acl        MATCH invalidators "127.0.0.1"
-   VCL_return     purge
-   VCL_call       HASH
-   VCL_return     lookup
-   VCL_call       PURGE
-   Debug          "VCL_error(201, PURGED!)"
-   VCL_return     synth
-   Timestamp      Process: 1433683498.334931 0.000170 0.000170
-   RespHeader     Date: Sun, 07 Jun 2015 13:24:58 GMT
-   RespHeader     Server: Varnish
-   RespHeader     X-Varnish: 32770
-   RespProtocol   HTTP/1.1
-   RespStatus     201
-   RespReason     Created
-   RespReason     PURGED!
-   VCL_call       SYNTH
-   RespHeader     Content-Type: text/html; charset=utf-8
-   RespHeader     Retry-After: 5
-   VCL_return     deliver
-   RespHeader     Content-Length: 243
-   Debug          "RES_MODE 2"
-   RespHeader     Connection: keep-alive
-   Timestamp      Resp: 1433683498.334985 0.000224 0.000054
-   ReqAcct        105 0 105 197 243 440
                

Hands on: log your own

  1. Start varnishlog and varnishtop
  2. and click around on varnish.lo.
  3. Then go back to the previous exercise:
  4. curl -sD - varnish.lo/solutions/content-negotiation.php
    curl -H "Accept: application/json" -sD - varnish.lo/solutions/content-negotiation.php
                    
  5. and find the Accept header.

Hands on: log your own (solution)

varnishtop -I ReqHeader:'Accept:'
            
varnishlog -I ReqHeader:'Accept:'
            



Test-driven caching

FOSHttpCache Test

Hands on: Test-driven caching

Run the tests:

vendor/bin/phpunit

Edit the test:

tests/CacheTest.php

Edit the code:

web/exercises/post.php

Make it green!


Advanced topics

Cache Invalidation

There are only two hard things in computer science:

  1. Cache invalidation
  2. Naming things
  3. Off by one errors

Cache busting

<link rel="stylesheet" href="/css/style.css?v1" type="text/css"/>
...
<script src="/js/scripts.js?v1"></script>
            

Explicit cache invalidation

Invalidation flavors

Needs custom Varnish configuration to work

Configure Varnish

acl invalidators {
    "localhost";
}

if (req.method == "PURGE") {
    if (!client.ip ~ invalidators) {
       return (synth(405, "Not allowed"));
    }
    return (purge);
}

...
            

Hands on: Cache purging

# config/varnish/common.vcl
include "invalidation.vcl";
            
sudo service varnish restart
            
curl -sD - varnish.lo/solutions/expiration.php
curl -X PURGE -sD - varnish.lo/solutions/expiration.php
curl -sD - varnish.lo/solutions/expiration.php
            

Demo: Purge and refresh from PHP

curl -sD - varnish.lo/invalidation/
curl -X POST --data 'method=Purge' -sD - varnish.lo/invalidation/update.php
curl -sD - varnish.lo/invalidation/
curl -X POST --data 'method=Refresh' -sD - varnish.lo/invalidation/update.php
curl -sD - varnish.lo/invalidation/
            

Banning

Demo: Banning

curl -sD - varnish.lo/invalidation/?x
curl -X POST --data 'method=Purge' -sD - varnish.lo/invalidation/update.php
curl -sD - varnish.lo/invalidation/?x
curl -X POST --data 'method=Ban' -sD - varnish.lo/invalidation/update.php
curl -sD - varnish.lo/invalidation/?x
            



Cache Tagging

Cache Tagging

ban("obj.http.x-cache-tags ~ "
      + req.http.x-cache-tags
);
            

Hands on: Tagging

curl -sD - varnish.lo/exercises/tagging/?filter=apple
curl -sD - varnish.lo/exercises/tagging/?filter=orange
curl -X POST --data 'applepie=New description' -sD - varnish.lo/exercises/tagging/update.php
curl -sD - varnish.lo/exercises/tagging/?filter=apple
curl -sD - varnish.lo/exercises/tagging/?filter=orange
            

FOSHttpCacheBundle

/** @var $cm CacheManageer */
$cm->tagResponse($response, array('comment-42'));
...
$cm->invalidateTags(array('comment-42'));
use FOS\HttpCacheBundle\Configuration\Tag;

class CommentController extends Controller
{
    /**
     * @Tag({"comments", "'comment-'~id"})
     */
    public function commentAction($id)
    // ...



Edge Side Includes

Use Edge Side Includes

Like server side include, but on Varnish:

ESI HTML

<html>
    <body>
        Main body.
        <esi:include src="fragment.php" />
    </body>
</html>
            
sub vcl_recv {
    // Add a Surrogate-Capability header
    // to announce ESI support.
    set req.http.Surrogate-Capability = "abc=ESI/1.0";
}
sub vcl_backend_response {
    // Check for ESI acknowledgement
    // and remove Surrogate-Control header.
    if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
        unset beresp.http.Surrogate-Control;
        set beresp.do_esi = true;
    }
}

Hands on: ESI

# config/varnish/common.vcl
include "esi.vcl";
            
sudo service varnish restart
            
curl -sD - varnish.lo/exercises/esi/
curl -sD - varnish.lo:8080/exercises/esi/
curl -sD - varnish.lo/exercises/esi/fragment.php
            

Symfony has built-in ESI support

# app/config/config.yml
framework:
  esi: { enabled: true }
  fragments: { path: /_fragment }
            
Make sure either your webserver is only reachable from the Varnish server or add access restrictions for /_fragment
{# index.html.twig #}
{% render_esi(controller( 'AppBundle:Comments:comments', {'param': 42 })) %}
            



Caching and Sessions

Strategies when working with sessions

VCL: Remove all but session cookie

sub vcl_recv {
    set req.http.cookie = ";" + req.http.cookie;
    set req.http.cookie = regsuball(req.http.cookie, "; +", ";");
    set req.http.cookie = regsuball(req.http.cookie, ";(PHPSESSID)=", "; \1=");
    set req.http.cookie = regsuball(req.http.cookie, ";[^ ][^;]*", "");
    set req.http.cookie = regsuball(req.http.cookie, "^[; ]+|[; ]+$", "");
    if (req.http.Cookie == "") {
        remove req.http.Cookie;
    }
}
            

varnish-cache.org/trac/wiki/VCLExampleRemovingSomeCookies #RemovingallBUTsomecookies

Demo: Avoid Session

if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time()-3600);
}
            
varnish.lo/limit-session/ (with your browser)

User Context


Group based caching

Demo: User Context

# config/varnish/common.vcl
include "custom-ttl.vcl";
include "user-context.vcl";
            
sudo service varnish restart
            
varnish.lo/user-context/ (with your browser)



Wrap-Up

Take-Aways

Outlook: Use libraries

Outlook: Where to go from here

Outlook: There is more than caching

Questions / Input / Feedback ?


Twitter: @dbu

https://joind.in/14893