1/54

Varnish Caching Proxy


Madison PHP Conference
September 13th, 2014

© David Buchmann, Liip AG

Step 1

1apt-get install varnish

Step 2

  • Edit /etc/default/varnish, replace port 6081 by port 80
  • Edit webserver config to run on port 8080
1service apache2 restart
2service varnish restart

Step 3

What could possibly go wrong?

What is a reverse proxy again?

HTTP is simple

1GET /path
2 
3HTTP/1.1 200 OK
4Content-Type: text/html
5 
6<html>...</html>

HTTP verbs

HTTP response codes

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

Default Varnish behaviour

http://httpstatusdogs.com

Cache control headers

1Cache-Control: s-maxage=3600, max-age=900
2Expires: 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

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

Keep variants apart

Request

1Accept: application/json

Response

1Vary: Accept

Cache validation (the new way)

1ETag: 82901821233
1If-None-Match: 82901821233
2 
3304 Not Modified

Cache validation (the old way)

1Last-Modified: Tue, 13 May 2014 08:13:20 GMT
1If-Modified-Since: Tue, 13 May 2014 08:13:20 GMT
2 
3304 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)

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

Seting cache headers (Symfony2)

1// DefaultController::indexAction
2$response = $this->render('::index.html.twig');
3$response->setSharedMaxAge(600);
4$response->setMaxAge(60);
5$response->setEtag(sha1($response->getContent()));
6 
7return $response;

FOSHttpCacheBundle

1# ...
2# Cache the homepage for 10 minutes
3-
4  match:
5    path: ^/$
6  headers:
7    cache_control:
8      public: true
9      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

1sub vcl_deliver {
2    if (obj.hits > 0) {
3        set resp.http.X-Cache = "HIT";
4    } else {
5        set resp.http.X-Cache = "MISS";
6    }
7}

VCL: Remove cookies

1sub vcl_recv {
2    if (req.http.Cookie) {
3        if (req.url ~ "^/static") {
4            remove req.http.Cookie;
5        }
6    }
7}

VCL: Google analytics cookies

01sub vcl_recv {
02if (req.http.Cookie) {
03  # removes all cookies named __utm? (utma, ...)
04  set req.http.Cookie = regsuball(
05    req.http.Cookie,
06    "(^|; ) *__utm.=[^;]+;? *", "\1"
07  );
08 
09  if (req.http.Cookie == "") {
10    remove req.http.Cookie;
11  }
12}

Cache Invalidation

There are only two hard things in computer science:

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

Cache busting

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

Symfony2 cache busting

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

1# app/config.yml
2framework:
3  templating:
4    assets_version: v1

Explicit cache invalidation

Invalidation flavors

Configure Varnish (I)

01acl invalidators {
02    "localhost";
03}
04 
05sub vcl_recv {
06    if (req.request == "PURGE") {
07        if (!client.ip ~ invalidators) {
08            error 405 "PURGE not allowed";
09        }
10        return (lookup);
11    }
12}
13...

Configure Varnish (II)

01...
02sub vcl_hit {
03    if (req.request == "PURGE") {
04        purge;
05        error 200 "Purged";
06    }
07}
08sub vcl_miss {
09    if (req.request == "PURGE") {
10        purge;
11        error 200 "Purged (not found)";
12    }
13}

FOSHttpCache library

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

FOSHttpCacheBundle

01// CommentsController::postAction
02// page changed, send purge request for this url
03...
04$cacheManager = $container->get(
05    'fos_http_cache.cache_manager'
06);
07$cacheManager->invalidateRoute('page', array(
08    'id' => $post->getPage()->getId()
09);
10...

Configure client

1# app/config.yml
2 
3fos_http_cache:
4  proxy_client:
5    varnish:
6      servers: 4.4.4.11:80, 4.4.4.22:80
7      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

1X-Cache-Tags: comment1, comment2

VCL: Cache Tagging

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

Cache Tagging with FOSHttpCacheBundle

1/** @var $cm CacheManageer */
2$cm->tagResponse($response, array('comment-42'));
3...
4$cm->invalidateTags(array('comment-42'));
01use FOS\HttpCacheBundle\Configuration\Tag;
02 
03class CommentController extends Controller
04{
05    /**
06     * @Tag({"comments", "'comment-'~id"})
07     */
08    public function commentAction($id)
09    {
10        // ...



Edge Side Includes

Use Edge Side Includes

Like server side include, but on Varnish:

ESI HTML

1<html>
2    <body>
3        Main body.
4        <esi:include src="/esi-fragment.php" />
5    </body>
6</html>

Enable ESI in Varnish

01sub vcl_recv {
02  // Announce ESI support to backend.
03  set req.http.Surrogate-Capability = "abc=ESI/1.0";
04}
05 
06sub vcl_fetch {
07  // Check for ESI acknowledgement
08  if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
09    unset beresp.http.Surrogate-Control;
10    set beresp.do_esi = true;
11  }
12}

Symfony has built-in ESI support

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