Varnish Caching Proxy


Madison PHP Conference
September 13th, 2014

© David Buchmann, Liip AG

Step 1

apt-get install varnish
            

Step 2

service apache2 restart
service varnish restart
            

Step 3

What could possibly go wrong?

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

https://twitter.com/stevelosh/status/372740571749572610

Default Varnish behaviour

http://httpstatusdogs.com

Cache control headers

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
  4. Default to default_ttl if nothing specified

Do not cache

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

Keep variants apart

Request

Accept: application/json
            

Response

Vary: Accept
            

Cache validation (the new way)

ETag: 82901821233
            
If-None-Match: 82901821233

304 Not Modified
            

Cache validation (the old way)

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

304 Not Modified
            

Know your tools

Debug what is going on

                wget -Sq --spider http://localhost/path.html
                curl -o /dev/null -sD - http://cmf.lo/app_dev.php
            

Setting cache headers (plain PHP)

// setting headers is no longer possible 
// when output started!
header('Cache-Control: s-maxage=600, max-age=60');
header('Etag: ' . sha1($data));
            

Seting cache headers (Symfony2)

// DefaultController::indexAction
$response = $this->render('::index.html.twig');
$response->setSharedMaxAge(600);
$response->setMaxAge(60);
$response->setEtag(sha1($response->getContent()));

return $response;
            

FOSHttpCacheBundle

# ...
# Cache the homepage for 10 minutes
-
  match:
    path: ^/$
  headers:
    cache_control:
      public: true
      max_age: 600
        

VCL

            vcl_recv: entry point, lookup or pass
            vcl_fetch: receive from backend, cache or not
            vcl_deliver: remove headers not for client
            
            vcl_hash: determine cache key
            vcl_hit: found in cache, deliver or pass
            vcl_miss: pass or fetch
            vcl_pipe: do not alter request
            vcl_error: define error page
            

Warning!


Varnish does what you tell it


Think carefully and test thoroughly

Restart Varnish like a pro

VCL: Debug hit or miss

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

VCL: Remove cookies

sub vcl_recv {
    if (req.http.Cookie) {
        if (req.url ~ "^/static") {
            remove req.http.Cookie;
        }
    }
}

VCL: Google analytics cookies

sub vcl_recv {
if (req.http.Cookie) {
  # removes all cookies named __utm? (utma, ...)
  set req.http.Cookie = regsuball(
    req.http.Cookie,
    "(^|; ) *__utm.=[^;]+;? *", "\1"
  ); 

  if (req.http.Cookie == "") {
    remove req.http.Cookie;
  }
}
            

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>
            

Symfony2 cache busting

Built-in support for cache busting with Symfony & Assetic:

# app/config.yml
framework:
  templating:
    assets_version: v1
            

Explicit cache invalidation

Invalidation flavors

Configure Varnish (I)

acl invalidators {
    "localhost";
}

sub vcl_recv {
    if (req.request == "PURGE") {
        if (!client.ip ~ invalidators) {
            error 405 "PURGE not allowed";
        }
        return (lookup);
    }
}
...
            

Configure Varnish (II)

...
sub vcl_hit {
    if (req.request == "PURGE") {
        purge;
        error 200 "Purged";
    }
}
sub vcl_miss {
    if (req.request == "PURGE") {
        purge;
        error 200 "Purged (not found)";
    }
}
            

FOSHttpCache library

$cacheInvalidator->invalidatePath('/my/path');
...
$cacheInvalidator->flush();
            

FOSHttpCacheBundle

// CommentsController::postAction
// page changed, send purge request for this url
...
$cacheManager = $container->get(
    'fos_http_cache.cache_manager'
);
$cacheManager->invalidateRoute('page', array(
    'id' => $post->getPage()->getId()
);
...
            

Configure client

# app/config.yml

fos_http_cache:
  proxy_client:
    varnish:
      servers: 4.4.4.11:80, 4.4.4.22:80
      base_url: yourwebsite.com
            

Banning

Refresh

Conclusions

Outlook: Varnish is a powerful tool

Varnish 4

Outlook: FOSHttpCache

Outlook: Where to go from here

Questions / Input / Feedback ?


https://joind.in/11751

Twitter: @dbu

Cache Context


Group based caching

Cache Tagging

X-Cache-Tags: comment1, comment2

VCL: Cache Tagging

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

Cache Tagging with 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="/esi-fragment.php" />
    </body>
</html>
            

Enable ESI in Varnish

sub vcl_recv {
  // Announce ESI support to backend.
  set req.http.Surrogate-Capability = "abc=ESI/1.0";
}

sub vcl_fetch {
  // Check for ESI acknowledgement
  if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
    unset beresp.http.Surrogate-Control;
    set beresp.do_esi = true;
  }
}
            

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( 'DbuCoreBundle:Comments:comments', {'param': 42 })) %}