1/55

Varnish Caching Proxy


And the FOSHttpCacheBundle


Webmardi Fribourg, June 3rd 2014
© David Buchmann, Liip AG

Step 1

1apt-get update
2apt-get install varnish

Step 1

1apt-get update
2apt-get install varnish

Step 2

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

Do not cache

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

Keep variants apart

1Vary: Accept, Cookie

Cache validation (I)

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

Cache validation (II)

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











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

Know your tools

Tools and helpers

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

Default Symfony headers

HTTP/1.1 200 OK
Date: Wed, 12 May 2014 08:20:06 GMT
Cache-Control: no-cache
Vary: Accept-Language,Accept-Encoding
Content-Type: text/html; charset=UTF-8

Set Symfony cache headers

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

FOSHttpCacheBundle

01fos_http_cache:
02  rules:
03    # login must not be cached
04    -
05      match:
06        path: ^/(login|login_check|logout)
07      controls:
08        private: true
09        max_age: 0
10    # Cache the homepage for 10 minutes
11    -
12      match:
13        path: ^/$
14        vary: Cookie
15      controls:
16        public: true
17        max_age: 600

FOSHttpCacheBundle

1# ...
2# Cache everything else for 1 hour
3-
4  match:
5    path: ^/
6    vary: Cookie
7  controls:
8    public: true
9    max_age: 3600
Clear the Symfony cache whenever you change values here

Lets look into Varnish

Restart Varnish like a pro

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

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: 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: Custom TTL

1fos_http_cache:
2    rules:
3            #...
4            reverse_proxy_ttl: 600
01sub vcl_fetch {
02  ...
03  if (beresp.http.X-Reverse-Proxy-TTL) {
04    C{
05      char *ttl;
06      ttl = VRT_GetHdr(sp, HDR_BERESP, "\024X-Reverse-Proxy-TTL:");
07      VRT_l_beresp_ttl(sp, atoi(ttl));
08    }C
09    unset beresp.http.X-Reverse-Proxy-TTL;
10  }
11  ...

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# app/config.yml
2framework:
3  templating:
4    assets_version: v1
1<link rel="stylesheet" href="/css/style.css?v1" type="text/css"/>
2...
3<script src="/js/scripts.js?v1"></script>

Explicit cache invalidation

Invalidation flavors

FOSHttpCacheBundle proxy client

1// CommentsController::postAction
2// the page changed, send a purge request for this url
3...
4$path = $this->generate('page', array(
5    'id' => $post->getPage()->getId())
6);
7$cacheManager = $container->get('fos_http_cache.cache_manager');
8$cacheManager->invalidatePath($path);
9...

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

Configure Varnish

01# default.vcl
02sub vcl_recv {
03    if (req.request == "PURGE") {
04        if (!client.ip ~ invalidators) {
05            error 405 "PURGE not allowed";
06        }
07        return (lookup);
08    }
09}
10sub vcl_hit {
11    if (req.request == "PURGE") {
12        purge;
13        error 200 "Purged";
14    }
15}
16sub vcl_miss { // same as hit

Banning

Refresh

Cache Context


Group based caching

Cache Tagging

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        // ...

Cache Tagging

1ban("obj.http.host ~ " + req.http.x-host
2    + " && obj.http.x-url ~ " + req.http.x-url
3    + " && obj.http.content-type ~ " +
4                       req.http.x-content-type
5    + " && obj.http.x-cache-tags ~ " +
6                         req.http.x-cache-tags
7);

Conclusions

Outlook: FOSHttpCacheBundle

Outlook: Varnish is a powerful tool

Varnish 4

Outlook: Where to go from here

Questions / Input / Feedback ?


Twitter: @dbu

Use Edge Side Includes

Enable ESI in Symfony

1# app/config/config.yml
2framework:
3  esi: { enabled: true }
4# app/config/routing.yml
5_internal:
6  resource: "@FrameworkBundle/Resources/config/routing/internal.xml"
7  prefix:   /_internal
Make sure either your webserver is only reachable by Varnish or add access restrictions for /_internal
1{# index.html.twig #}
2{% render 'DbuCoreBundle:Comments:comments'
3      with {}, {'standalone': true} %}

Adjust cache settings

1fos_http_cache:
2  rules:
3    # do not apply rules to _internal
4    -
5      match:
6        path: ^/_internal
7      # no controls

Fragment controllers

1// UserController::showBoxAction
2$response = $this->render(
3    'DbuCoreBundle:User:box.html.twig');
4$response->setVary('Cookie', false);
5$response->setMaxAge(0);
6$response->setPrivate();
7return $response;
1// CommentsController::commentsAction
2$response = $this->render(
3    'DbuCoreBundle:Comments:comments.html.twig',
4    array('comments' => $this->getComments()));
5$response->setMaxAge(3600);
6$response->setPublic();
7 
8return $response;

Purge just the comments fragment

1$cm = $container->get('fos_http_cache.cache_manager');
2$kernel = $this->container->get('http_kernel');
3$path = $kernel->generateInternalUri(
4    'DbuCoreBundle:Comments:comments'
5);
6$cm->invalidatePath($path);

Note: generateInternalUri is helpful but does not check if controller exists

ESI response

$ app/console cache:clear --env=prod --no-debug
$ curl -H "Surrogate-Capability: abc=ESI/1.0" \
    http://performance.lo/
1...
2<esi:include src="/_internal/secure/DbuCoreBundle%3AUser%3AshowLogin/none.html" onerror="continue" />
3...
4<esi:include src="/_internal/secure/DbuCoreBundle%3AComments%3Acomments/none.html" onerror="continue" />
5...

Enable ESI in Varnish

1sub vcl_recv {
2  set req.http.Surrogate-Capability = "abc=ESI/1.0";
3  # try to lookup even if there is a cookie
4  return (lookup);
5}
1sub vcl_fetch {
2  if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
3    unset beresp.http.Surrogate-Control;
4    set beresp.do_esi = true;
5  }
6  if (beresp.http.Vary) {
7    return (pass);
8  }
9}

Make sure others do not cache

1sub vcl_deliver {
2  if (! req.url ~ ".*\.(css|js|png)(\?.*)?$") {
3    set resp.http.Vary = "Cookie";
4    unset resp.http.Last-Modified;
5  }
6}