Varnish Caching Proxy


And the FOSHttpCacheBundle


SFUGZH, May 14th 2014
© David Buchmann, Liip AG

Step 1

apt-get update
apt-get install varnish
            

Step 2

service apache2 restart
service varnish restart
            

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

Do not cache

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

Keep variants apart

Vary: Accept, Cookie
            

Cache validation (I)

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

304 Not Modified
            

Cache validation (II)

ETag: 82901821233
            
If-None-Match: 82901821233

304 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

// DefaultController::indexAction
$response = $this->render('::index.html.twig');
$response->setMaxAge(600);
$response->setPublic();

return $response;
            

FOSHttpCacheBundle

fos_http_cache:
  rules:
    # login must not be cached
    -
      match:
        path: ^/(login|login_check|logout)
      controls:
        private: true
        max_age: 0
    # Cache the homepage for 10 minutes
    -
      match:
        path: ^/$
        vary: Cookie
      controls:
        public: true
        max_age: 600
            

FOSHttpCacheBundle

# ...
# Cache everything else for 1 hour
-
  match:
    path: ^/
    vary: Cookie
  controls:
    public: true
    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_pass: request from backend
            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

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

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

fos_http_cache:
    rules:
            #...
            reverse_proxy_ttl: 600
sub vcl_fetch {
  ...
  if (beresp.http.X-Reverse-Proxy-TTL) {
    C{
      char *ttl;
      ttl = VRT_GetHdr(sp, HDR_BERESP, "\024X-Reverse-Proxy-TTL:");
      VRT_l_beresp_ttl(sp, atoi(ttl));
    }C
    unset beresp.http.X-Reverse-Proxy-TTL;
  }
  ...

Cache Invalidation

There are only two hard things in computer science:

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

Cache busting

# app/config.yml
framework:
  templating:
    assets_version: v1
            
<link rel="stylesheet" href="/css/style.css?v1" type="text/css"/>
...
<script src="/js/scripts.js?v1"></script>
            

Explicit cache invalidation

Invalidation flavors

FOSHttpCacheBundle proxy client

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

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
            

Configure Varnish

# default.vcl
sub vcl_recv {
    if (req.request == "PURGE") {
        if (!client.ip ~ invalidators) {
            error 405 "PURGE not allowed";
        }
        return (lookup);
    }
}
sub vcl_hit {
    if (req.request == "PURGE") {
        purge;
        error 200 "Purged";
    }
}
sub vcl_miss { // same as hit
            

Banning

Refresh

Cache Tagging

/** @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)
    {
        // ...

Cache Tagging

ban("obj.http.host ~ " + req.http.x-host
    + " && obj.http.x-url ~ " + req.http.x-url
    + " && obj.http.content-type ~ " + 
                       req.http.x-content-type
    + " && obj.http.x-cache-tags ~ " + 
                         req.http.x-cache-tags
);

    

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

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

Adjust cache settings

fos_http_cache:
  rules:
    # do not apply rules to _internal
    -
      match:
        path: ^/_internal
      # no controls

Fragment controllers

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

return $response;
            

Purge just the comments fragment

$cm = $container->get('fos_http_cache.cache_manager');
$kernel = $this->container->get('http_kernel');
$path = $kernel->generateInternalUri(
    'DbuCoreBundle:Comments:comments'
);
$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/
            
...
<esi:include src="/_internal/secure/DbuCoreBundle%3AUser%3AshowLogin/none.html" onerror="continue" />
...
<esi:include src="/_internal/secure/DbuCoreBundle%3AComments%3Acomments/none.html" onerror="continue" />
...
            

Enable ESI in Varnish

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

Make sure others do not cache

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