SlideShare a Scribd company logo
Or how to make visitor specific
content super fast.
Caching the
uncachable
I'm Daniel Rotter
@danrot90 | https://github.com/danrot
core developer and
support guru.
Passionate traveler and
soccer player.
Who knows about
HTTP Caching?
HTTP Caching Basics
A short recap
Browser
Reverse
Proxy
Symfony
Application
GET / GET /
200 OK 200 OK
Browser
GET /
200 OK
GET /
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600, s-maxage=86400
<!DOCTYPE html>
<html>
<body>
Interesting content
</body>
</html>
GET /
HTTP/1.1 200 OK
Last-Modified: Sun, 4, Feb 2018 20:18:00 GMT
<!DOCTYPE html>
<html>
<body>
Quite recent Content
</body>
</html>
GET /
If-Modified-Since: Sun, 4 Feb 2018 20:18:00 GMT
HTTP/1.1 304 Not Modified
Apply more logic in the reverse proxy layer
Custom HTTP Header
200 OK
Cache-Control: max-age=240
Browser
Reverse
Proxy
Symfony
Application
GET / GET /
200 OK
Cache-Control: max-age=240
200 OK
X-Reverse-Proxy-TTL: 2400
Browser
GET /
200 OK
Cache-Control: max-age=240
200 OK
X-Reverse-Proxy-TTL: 2400
200 OK
Cache-Control: max-age=240
Browser
Reverse
Proxy
Symfony
Application
GET / GET /
Browser
GET /
PURGE /
GET /
200 OK
X-Reverse-Proxy-TTL: 2400
Caching
Implementations
Symfony Cache or Varnish?
Symfony Cache
– Easy to setup
– Implemented in PHP
– Not feature complete
// src/CacheKernel.php
<?php
namespace App;
use SymfonyBundleFrameworkBundleHttpCacheHttpCache;
class CacheKernel extends HttpCache {}
// public/index.php
<?php
use AppKernel;
use AppCacheKernel;
// ...
$kernel = new Kernel($env, $debug);
$kernel = new CacheKernel($kernel);
// ...
// src/Controller/NameController.php
<?php
namespace AppController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
class NameController {
/**
* @Route("/name/{name}")
*/
public function index(string $name) {
$response = new Response('Your name is ' . $name);
$response->setSharedMaxAge(3600);
return $response;
}
}
– Harder to setup
– Implemented in C
– Super performant
– Many more features
– Configured using VCL
Varnish
Varnish Installation
1. Actually install varnish
2. Start varnish on port 80
3. Start your application on a different port
4. Tell varnish on which port your application is running
5. Add varnish as a trusted proxy in Symfony
6. Add cache headers to your responses
7. Configure the cache with VCL in more detail
# default.vcl
vcl 4.0;
# Default backend definition. Set this to point to your content server.
backend default {
.host = "127.0.0.1";
.port = "8080";
}
4
// public/index.php
// ...
Request::setTrustedProxies(['127.0.0.1'], Request::HEADER_X_FORWARDED_ALL);
// ...
5
// src/Controller/NameController.php
<?php
namespace AppController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAnnotationRoute;
class NameController {
/**
* @Route("/name/{name}")
*/
public function index(string $name) {
$response = new Response('Your name is ' . $name);
$response->setSharedMaxAge(3600);
return $response;
}
} 6
# default.vcl
sub vcl_recv {
# Happens before we check if we have this in cache already.
#
# Typically you clean up the request here, removing cookies you don't need,
# rewriting the request, etc.
}
sub vcl_deliver {
# Happens when we have all the pieces we need, and are about to send the
# response to the client.
#
# You can do accounting or modifying the final object here.
}
7
Caching the Uncacheable
Audience Targeting
Show (and cache!) different content for different target
groups on the same URL
Audience Targeting
Goals
– Differentiate between different visitor target groups
– Target groups are evaluated by a ruleset
– first visit
– each browser session
– each hit
– Do not start a session on the server
– Cache the response per target group
Caching the Uncacheable
Caching the Uncacheable
Caching the Uncacheable
Who knows the Vary
header?
Browser
Reverse
Proxy
Symfony
Application
GET /en
Accept-Encoding: gzip
200 OK
Vary: Accept-Encoding
GET /en
Accept-Encoding: identity
200 OK
Cache-Control: max-age=0
GET /en
Accept-Encoding: gzip
200 OK
Vary: Accept-Encoding
GET /en
Accept-Encoding: gzip
200 OK
Vary: Accept-Encoding
Browser
200 OK
Vary: Accept-Encoding
GET /en
Accept-Encoding: identity
Browser
Reverse
Proxy
Symfony
Application
GET /en
GET /_sulu_target_group
X-Sulu-Original-Url: /en
GET /en
X-Sulu-Target-Group: 1
200 OK
X-Sulu-Target-Group: 1
200 OK
Vary: X-Sulu-Target-Group
X-Reverse-Proxy-TTL: 2400
200 OK
Cache-Control: max-age=0
Set-Cookie: _svtg=1;
Set-Cookie: _svs=...;
GET /en
Cookie: _svtg=1; _svs=..
200 OK
Cache-Control: max-age=0
Reverse
Proxy
Symfony
Application
GET /about-us
Cookie: _svtg=1; _svs=..
GET /about-us
X-Sulu-Target-Group: 1
200 OK
X-Reverse-Proxy-TTL: 2400200 OK
Browser
Browser
GET /about-us
Cookie: _svtg=1; _svs=..
200 OK
Symfony Cache
Use extended version of the SymfonyCache from Sulu to support
Audience Targeting
<?php
namespace SuluComponentHttpCache;
use SymfonyBundleFrameworkBundleHttpCacheHttpCache as AbstractHttpCache;
use SymfonyComponentHttpKernelHttpKernelInterface;
class HttpCache extends AbstractHttpCache
{
public function __construct(HttpKernelInterface $kernel, $hasAudienceTargeting =
false, $cacheDir = null)
{
parent::__construct($kernel, $cacheDir);
$this->hasAudienceTargeting = $hasAudienceTargeting;
}
}
// app/WebsiteCache.php
<?php
use SuluComponentHttpCacheHttpCache;
class WebsiteCache extends HttpCache
{
}
// web/website.php
<?php
// ...
$kernel = new WebsiteKernel(SYMFONY_ENV, SYMFONY_DEBUG);
$kernel->loadClassCache();
if (SYMFONY_ENV != 'dev') {
$kernel = new WebsiteCache($kernel, true);
Request::enableHttpMethodParameterOverride();
}
// ...
Varnish
VCL to the rescue!
“I really love VCL!
Said no one,
ever.
Caching the Uncacheable
vcl 4.0;
import header;
backend default {
.host = "127.0.0.1";
.port = "8000";
}
sub vcl_recv {
if (req.http.Cookie ~ "_svtg" && req.http.Cookie ~ "_svs") {
set req.http.X-Sulu-Target-Group = regsub(req.http.Cookie, ".*_svtg=([^;]+).*", "1");
} elseif (req.restarts == 0) {
set req.http.X-Sulu-Original-Url = req.url;
set req.http.X-Sulu-Target-Group = regsub(req.http.Cookie, ".*_svtg=([^;]+).*", "1");
set req.url = "/_sulu_target_group";
} elseif (req.restarts > 0) {
set req.url = req.http.X-Sulu-Original-Url;
unset req.http.X-Sulu-Original-Url;
}
unset req.http.Cookie;
}
sub vcl_deliver {
if (resp.http.X-Sulu-Target-Group) {
set req.http.X-Sulu-Target-Group = resp.http.X-Sulu-Target-Group;
set req.http.Set-Cookie = "_svtg=" + resp.http.X-Sulu-Target-Group + "; expires=Tue, 19 Jan 2038
03:14:07 GMT; path=/;";
return (restart);
}
if (resp.http.Vary ~ "X-Sulu-Target-Group") {
set resp.http.Cache-Control = regsub(resp.http.Cache-Control, "max-age=(d+)", "max-age=0");
set resp.http.Cache-Control = regsub(resp.http.Cache-Control, "s-maxage=(d+)", "s-maxage=0");
}
if (req.http.Set-Cookie) {
set resp.http.Set-Cookie = req.http.Set-Cookie;
header.append(resp.http.Set-Cookie, "_svs=" + now + "; path=/;");
}
}
zz
Sulu
Docs
Good ol’ Javascript
How to reevaluate
target groups on
cache hit?
Thanks for watching!
sulu.io

More Related Content

Caching the Uncacheable