BY THIJS FERYN
Slow websites
SUCK
WEB PERFORMANCE IS AN
ESSENTIAL PART OF THE
USER EXPERIENCE
SLOW~DOWN
THROWING
SERVERS
ATTHEPROBLEM
MO' MONEY
MO' SERVERS
MO' PROBLEMS
IDENTIFY SLOWEST PARTS
OPTIMIZE
AFTER A WHILE YOU HIT THE LIMITS
CACHING
WHY
RECOMPUTE
IF THE DATA
HASN'T
CHANGED?
HI, I'M THIJS
I'M THE TECH EVANGELIST
AT VARNISH SOFTWARE
I'M @THIJSFERYN
ON TWITTER &
MASTODON
WE BUILD SOFTWARE-DEFINED
WEB ACCELERATION & CONTENT
DELIVERY SOLUTIONS
SERVER
USER
UNDER PRESSURE
SERVER
USER VARNISH SERVER
USER VARNISH SERVER
USER VARNISH SERVER
IMPROVE
THE CACHING
+ =
+ =
1.5 Tbps
per server
1.4 Gbps
per watt
ACHIEVE GROWTH, PERFORMANCE &
SUSTAINABILITY GOALS
✓ ACCELERATE DELIVERY OF
RESULTS FOR MUNICIPAL
ELECTIONS IN BRAZIL
✓ REAL-TIME RESULTS
✓ MULTIPLE POINTS OF
PRESENCE
✓ VERY HIGH CONCURRENCY
✓ KUBERNETES 2 DOZEN PODS
50,000,000 REQ/MINUTE
+ =
+ =
HTTP HAS BUILT-IN
CACHING MECHANISMS
Cache-Control: public, max-age=500
Cache-Control: public, max-age=500,
s-maxage=3600
Cache-Control: public, max-age=3600, stale-
while-revalidate=100
Cache-Control: private, no-cache, no-store
AND VARNISH
COMPLIES TO THOSE
HTTP CACHING
CONVENTIONS
IT'S EASY TO
CACHE THESE
VARNISH
DOESN'T CACHE
WHEN COOKIES
ARE USED
VARNISH
DOESN'T CACHE
WHEN THERE'S AN
AUTHORIZATION
HEADER
FOR YOUR EYES ONLY
NOT CACHED
1. STATEMENT
2. QUESTION
3. CONCLUSION
VARNISH CONFIGURATION LANGUAGE
VCL
✓ DOMAIN-SPECIFIC LANGUAGE
✓ CURLY BRACES
✓ CONTROLS REQUESTS, RESPONSES, BACKENDS &
CACHING BEHAVIOR
✓ TRANSPILED INTO C-CODE AND COMPILED INTO
MACHINE CODE
✓ NOT A TOP-DOWN PROGRAMMING LANGUAGE
✓ HOOKS INTO FINITE STATE MACHINE
vcl 4.1;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
if(req.url ~ "^/admin(/.*|$)") {
return(pass);
}
unset req.http.Cookie;
}
vcl 4.1;
import cookie;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
if (req.http.cookie) {
cookie.parse(req.http.cookie);
cookie.keep("PHPSESSID");
set req.http.cookie = cookie.get_string();
if (req.http.cookie ~ "^s*$") {
unset req.http.cookie;
}
}
}
CACHE-CONTROL
Cache-Control: public, max-age=100, s-maxage=3600
<?php
namespace AppController;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAttributeRoute;
class DefaultController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this
->render('default/index.html.twig')
->setPublic()
->setMaxAge(100)
->setSharedMaxAge(3600);
}
}
Cache-Control: private, no-cache, no-store
#[Route('/private', name: 'private')]
public function private(): Response
{
$response = $this
->render('default/private.html.twig')
->setPrivate();
$response->headers->addCacheControlDirective('no-cache');
$response->headers->addCacheControlDirective('no-store');
return $response;
}
ASYNCHRONOUS
REVALIDATION
USER VARNISH SERVER
ASYNC FETCH
SEND STALE
RESPONSE WHILE
FETCHING
Cache-Control: public, max-age=100,
s-maxage=3600, stale-while-revalidate=7200
<?php
namespace AppController;
use SymfonyBundleFrameworkBundleControllerAbstractController;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingAttributeRoute;
class DefaultController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this
->render('default/index.html.twig')
->setPublic()
->setMaxAge(100)
->setSharedMaxAge(3600)
->setStaleWhileRevalidate(7200);
}
}
OBJECT LIFETIME = TTL + GRACE + KEEP
OBJECT LIFETIME = TTL + GRACE + KEEP
DEFAULT VALUE:
2 MINUTES
OBJECT LIFETIME = TTL + GRACE + KEEP
SERVE STALE
CONTENT, ASYNC
REVALIDATION
DEFAULT VALUE: 10
SECONDS
CAN BE SET BY
"STALE-WHILE-
REVALIDATE"
OBJECT LIFETIME = TTL + GRACE + KEEP
KEEP THE OBJECT
AROUND FOR LATER
USE
DEFAULT VALUE:
0 SECONDS
SYNCHRONOUS
REVALIDATION:
CONDITIONAL
REQUESTS
OBJECT LIFETIME = TTL + GRACE + KEEP
OBJECT LIFETIME = 100 + 10 + 0
OBJECT LIFETIME = 100 + 10 + 0
CACHE HIT
OBJECT LIFETIME = 0 + 10 + 0
EXPIRED
OBJECT LIFETIME = 0 + 10 + 0
STALE
ASYNC
REVALIDATION
OBJECT LIFETIME = -8 + 10 + 0
OBJECT LIFETIME = -8 + 10 + 0
ASYNC
REVALIDATION
OBJECT LIFETIME = -11 + 10 + 0
OBJECT LIFETIME = -11 + 10 + 0
EXPIRED &
OUT OF GRACE
SYNCHRONOUS
REVALIDATION
OBJECT LIFETIME = -11 + 10 + 100
OBJECT LIFETIME = -11 + 10 + 100
EXPIRED &
OUT OF GRACE
SYNCHRONOUS
REVALIDATION
KEEP
OBJECT AROUND
FOR CONDITIONAL
REQUESTS
CONDITIONAL
REQUESTS
ONLY FETCH PAYLOAD THAT HAS CHANGED
DURING REVALIDATION
HTTP/1.1 200 OK
OTHERWISE:
HTTP/1.1 304 NOT MODIFIED
CONDITIONAL REQUESTS
HTTP/1.1 200 OK
Host: localhost
Etag: 7c9d70604c6061da9bb9377d3f00eb27
Content-type: text/html; charset=UTF-8
Hello world output
GET / HTTP/1.1
Host: localhost
CONDITIONAL REQUESTS
HTTP/1.1 304 Not Modified
Host: localhost
Etag: 7c9d70604c6061da9bb9377d3f00eb27
GET / HTTP/1.1
Host: localhost
If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
CONDITIONAL REQUESTS
HTTP/1.1 200 OK
Host: localhost
Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
Content-type: text/html; charset=UTF-8
Hello world output
GET / HTTP/1.1
Host: localhost
CONDITIONAL REQUESTS
HTTP/1.1 304 Not Modified
Host: localhost
Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
GET / HTTP/1.1
Host: localhost
If-Modified-Since: Fri, 22 Jul 2016 10:11:16 GMT
QUICKLY
EARLY
<?php
namespace AppEventListener;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentEventDispatcherAttributeAsEventListener;
use SymfonyComponentHttpKernelEventRequestEvent;
use SymfonyComponentHttpKernelEventResponseEvent;
use SymfonyComponentHttpKernelKernelEvents;
use SymfonyContractsCacheCacheInterface;
use SymfonyContractsCacheItemInterface;
final class EtagListener
{
public function __construct(private CacheInterface $cache)
{
}
...
<?php
namespace AppEventListener;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentEventDispatcherAttributeAsEventListener;
use SymfonyComponentHttpKernelEventRequestEvent;
use SymfonyComponentHttpKernelEventResponseEvent;
use SymfonyComponentHttpKernelKernelEvents;
use SymfonyContractsCacheCacheInterface;
use SymfonyContractsCacheItemInterface;
final class EtagListener
{
public function __construct(private CacheInterface $cache)
{
}
...
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get();
if($etagData !== null) {
$etag = $etagData[0];
$cc = $etagData[1];
} else {
$etag = null;
$cc = null;
}
$response = new Response();
$response->setEtag($etag);
if($cc !== null) {
$response->headers->add(['Cache-Control'=>$cc]);
}
if($response->isNotModified($request)) {
$event->setResponse($response);
}
}
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get();
if($etagData !== null) {
$etag = $etagData[0];
$cc = $etagData[1];
} else {
$etag = null;
$cc = null;
}
$response = new Response();
$response->setEtag($etag);
if($cc !== null) {
$response->headers->add(['Cache-Control'=>$cc]);
}
if($response->isNotModified($request)) {
$event->setResponse($response);
}
}
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get();
if($etagData !== null) {
$etag = $etagData[0];
$cc = $etagData[1];
} else {
$etag = null;
$cc = null;
}
$response = new Response();
$response->setEtag($etag);
if($cc !== null) {
$response->headers->add(['Cache-Control'=>$cc]);
}
if($response->isNotModified($request)) {
$event->setResponse($response);
}
}
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get();
if($etagData !== null) {
$etag = $etagData[0];
$cc = $etagData[1];
} else {
$etag = null;
$cc = null;
}
$response = new Response();
$response->setEtag($etag);
if($cc !== null) {
$response->headers->add(['Cache-Control'=>$cc]);
}
if($response->isNotModified($request)) {
$event->setResponse($response);
}
}
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get();
if($etagData !== null) {
$etag = $etagData[0];
$cc = $etagData[1];
} else {
$etag = null;
$cc = null;
}
$response = new Response();
$response->setEtag($etag);
if($cc !== null) {
$response->headers->add(['Cache-Control'=>$cc]);
}
if($response->isNotModified($request)) {
$event->setResponse($response);
}
}
#[AsEventListener(event: KernelEvents::REQUEST)]
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get();
if($etagData !== null) {
$etag = $etagData[0];
$cc = $etagData[1];
} else {
$etag = null;
$cc = null;
}
$response = new Response();
$response->setEtag($etag);
if($cc !== null) {
$response->headers->add(['Cache-Control'=>$cc]);
}
if($response->isNotModified($request)) {
$event->setResponse($response);
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event)
{
$response = $event->getResponse();
if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) {
$request = $event->getRequest();
$etag = md5($response->getContent());
$cc = $response->headers->get('Cache-Control');
$response->setEtag($etag);
if(!$response->isNotModified($request)) {
$this->cache->get('etag-'.md5($request->getUri()),
function(ItemInterface $item) use($etag, $cc){
$item->expiresAfter(3600);
$item->set([$etag,$cc]);
return [$etag,$cc];
});
}
}
}
CONTENT COMPOSITION
& PLACEHOLDERS
NON-
CACHEABLE
CONTENT
CACHEABLE
CONTENT
CACHEABLE
CONTENT
NO CACHE
USE PLACEHOLDERS
TO SEPARATE
CACHEABLE
& NON-CACHEABLE
CONTENT
AJAX
EDGE-SIDE INCLUDES ESI
<esi:include src="/header" />
ESI
✓ PLACEHOLDER
✓ PARSED BY VARNISH
✓ OUTPUT IS A COMPOSITION OF BLOCKS
✓ STATE PER BLOCK
✓ TTL PER BLOCK
<!DOCTYPE html>
<html>
<body>
<esi:include src="/header" />
<p>Main content</p>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<p>Welcome Thijs</p>
<p>Main Content</p>
</body>
</html>
GET / HTTP/1.1
Host: example.com
User-Agent: Chrome
Accept: */*
GET / HTTP/1.1
Host: example.com
User-Agent: Chrome
Accept: */*
Surrogate-Capability = "key=ESI/1.0"
ANNOUNCE
ESI SUPPORT
HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8
Cache-Control: public, max-age=3600
Surrogate-Control = "key=ESI/1.0"
CONTROL ESI
PARSING
GET /header HTTP/1.1
Host: example.com
Surrogate-Capability = "key=ESI/1.0"
SUBREQUEST
SENT BY
VARNISH
HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8
Cache-Control: no-store
WON'T BE
CACHED
HTTP/1.1 200 OK
Content-type: text/html; charset=UTF-8
Cache-Control: public, max-age=3600
SINGLE
HTTP RESPONSE
TO CLIENT
<!DOCTYPE html>
<html>
<body>
<esi:include src="/header" />
<p>Main content</p>
</body>
</html>
SERVER
RESPONSE
<!DOCTYPE html>
<html>
<body>
<p>Welcome Thijs</p>
<p>Main Content</p>
</body>
</html>
EDGE
RESPONSE
sub vcl_backend_fetch {
set bereq.http.Surrogate-Capability = "key=ESI/1.0";
}
sub vcl_backend_response {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}
sub vcl_backend_fetch {
set bereq.http.Surrogate-Capability = "key=ESI/1.0";
}
sub vcl_backend_response {
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
}
ANNOUNCE
ESI SUPPORT
NEGOTIATE
WITH BACKEND
COMPOSITION AT THE VIEW LAYER
class DefaultController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this
->render('default/index.html.twig')
->setPublic()
->setMaxAge(10)
->setSharedMaxAge(60);
}
#[Route('/header', name: 'header')]
public function header(): Response
{
return $this
->render('default/header.html.twig')
->setPublic()
->setSharedMaxAge(3600);
}
#[Route('/footer', name: 'footer')]
public function footer(): Response
{
return $this
->render('default/footer.html.twig')
->setPublic()
->setSharedMaxAge(86400);
}
}
ROUTE PER
FRAGMENT
class DefaultController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this
->render('default/index.html.twig')
->setPublic()
->setMaxAge(10)
->setSharedMaxAge(60);
}
#[Route('/header', name: 'header')]
public function header(): Response
{
return $this
->render('default/header.html.twig')
->setPublic()
->setSharedMaxAge(3600);
}
#[Route('/footer', name: 'footer')]
public function footer(): Response
{
return $this
->render('default/footer.html.twig')
->setPublic()
->setSharedMaxAge(86400);
}
}
class DefaultController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this
->render('default/index.html.twig')
->setPublic()
->setMaxAge(10)
->setSharedMaxAge(60);
}
#[Route('/header', name: 'header')]
public function header(): Response
{
return $this
->render('default/header.html.twig')
->setPublic()
->setSharedMaxAge(3600);
}
#[Route('/footer', name: 'footer')]
public function footer(): Response
{
return $this
->render('default/footer.html.twig')
->setPublic()
->setSharedMaxAge(86400);
}
}
class DefaultController extends AbstractController
{
#[Route('/', name: 'home')]
public function index(): Response
{
return $this
->render('default/index.html.twig')
->setPublic()
->setMaxAge(10)
->setSharedMaxAge(60);
}
#[Route('/header', name: 'header')]
public function header(): Response
{
return $this
->render('default/header.html.twig')
->setPublic()
->setSharedMaxAge(3600);
}
#[Route('/footer', name: 'footer')]
public function footer(): Response
{
return $this
->render('default/footer.html.twig')
->setPublic()
->setSharedMaxAge(86400);
}
}
<!DOCTYPE html>
<html>
<body>
<div class="container">
{{ include('default/header.html.twig') }}
{% block content %}
{% endblock %}
{{ include('default/footer.html.twig') }}
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div class="container">
{{ render_esi(url('header')) }}
{% block content %}
{% endblock %}
{{ render_esi(url('footer')) }}
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div class="container">
{{ render_esi(url('header')) }}
{% block content %}
{% endblock %}
{{ render_esi(url('footer')) }}
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div class="container">
<esi:include src="/header" />
{% block content %}
{% endblock %}
<esi:include src="/footer" />
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div class="container">
{{ render_esi(url('header')) }}
{% block content %}
{% endblock %}
{{ render_esi(url('footer')) }}
</div>
</body>
</html> FALLS BACK TO
INTERNAL SUBREQUESTS IF
SURROGATE-CAPABILITY
HEADER DOESN'T MATCH
CACHE VARIATIONS
HOW DO YOU IDENTIFY AN
OBJECT IN CACHE?
HOST HEADER + URI
WHAT IF THE CONTENT
OF A URL VARIES
BASED ON THE VALUE
OF A REQUEST
HEADER?
Vary: Accept-Language
USER VARNISH SERVER
GET / HTTP/1.1
Host: example.com
Accept-Language: nl
example.com
/
en
HTTP/1.1 200 OK
Cache-Control: public, max-age=500
Vary: Accept-Language
example.com
/
nl
CLEAN UP ACCEPT HEADERS
vcl 4.1;
import accept;
sub vcl_init {
new format = accept.rule("text/plain");
format.add("text/html");
format.add("application/json");
format.add("text/plain");
new lang = accept.rule("en");
lang.add("en");
lang.add("fr");
lang.add("nl");
lang.add("pt");
}
sub vcl_recv {
set req.http.accept-language = lang.filter(req.http.accept-language);
set req.http.accept= format.filter(req.http.accept);
}
Vary: Accept-Language, X-Forwarded-Proto
<?php
namespace AppEventListener;
use SymfonyComponentEventDispatcherAttributeAsEventListener;
use SymfonyComponentHttpKernelEventResponseEvent;
use SymfonyComponentHttpKernelKernelEvents;
final class VaryListener
{
#[AsEventListener(event: KernelEvents::RESPONSE)]
public function onKernelResponse(ResponseEvent $event): void
{
$event
->getResponse()
->setVary('Accept-Language, X-Forwarded-Proto',false);
}
}
WHAT IF YOU CAN'T
LEVERAGE HTTP?
WRITE VCL CODE
vcl 4.1;
import std;
import cookie;
backend server1 {
.host = "127.0.0.1";
.port = "8080";
.probe = {
.url = "/";
}
}
acl purge {
"localhost";
"127.0.0.1";
"::1";
}
sub vcl_recv {
set req.http.Host = regsub(req.http.Host, ":[0-9]+", "");
unset req.http.proxy;
set req.url = std.querysort(req.url);
set req.url = regsub(req.url, "?$", "");
set req.http.Surrogate-Capability = "key=ESI/1.0";
if (std.healthy(req.backend_hint)) {
set req.grace = 10s;
}
if (!req.http.X-Forwarded-Proto) {
if(std.port(server.ip) == 443 || std.port(server.ip) == 8443) {
set req.http.X-Forwarded-Proto = "https";
} else {
set req.http.X-Forwarded-Proto = "https";
}
}
if (req.http.Upgrade ~ "(?i)websocket") {
return (pipe);
}
...
if (req.url ~ "(?|&)(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof|
siteurl)=") {
set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_content|
gclid|cx|ie|cof|siteurl)=([A-z0-9_-.%25]+)", "");
set req.url = regsuball(req.url, "?(utm_source|utm_medium|utm_campaign|utm_content|
gclid|cx|ie|cof|siteurl)=([A-z0-9_-.%25]+)", "?");
set req.url = regsub(req.url, "?&", "?");
set req.url = regsub(req.url, "?$", "");
}
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return (synth(405, client.ip + " is not allowed to send PURGE requests."));
}
return (purge);
}
if (req.url ~ "^[^?]*.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|
js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|
swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(?.*)?$") {
unset req.http.Cookie;
}
if (req.http.cookie) {
cookie.parse(req.http.cookie);
cookie.keep("PHPSESSID");
set req.http.cookie = cookie.get_string();
if (req.http.cookie ~ "^s*$") {
unset req.http.cookie;
}
}
}
sub vcl_hash {
hash_data(req.http.X-Forwarded-Proto);
}
sub vcl_backend_response {
if (bereq.url ~ "^[^?]*.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg|
js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz|
swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(?.*)?$") {
unset beresp.http.Set-Cookie;
set beresp.ttl = 1d;
}
if (beresp.http.Surrogate-Control ~ "ESI/1.0") {
unset beresp.http.Surrogate-Control;
set beresp.do_esi = true;
}
set beresp.grace = 6h;
}
CACHE INVALIDATION
acl purge {
"localhost";
"127.0.0.1";
"::1";
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return(synth(405,"Method not allowed."));
}
return (purge);
}
}
curl -XPURGE https://example.com/contact
acl purge {
"localhost";
"127.0.0.1";
"::1";
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return(synth(405,"Method not allowed."));
}
if (req.http.X-Purge-Method == "regex") {
ban("obj.http.url ~ " + req.url + " && obj.http.host == " + req.http.host);
return(synth(200, "Purged"));
}
return (purge);
}
}
sub vcl_backend_response {
set beresp.http.url = bereq.url;
set beresp.http.host = bereq.http.host;
}
sub vcl_deliver {
unset resp.http.url;
unset resp.http.host;
}
curl -XPURGE 
-H"X-Purge-Method:regex" https://example.com/catalog/
import ykey;
acl purge {
"localhost";
"127.0.0.1";
"::1";
}
sub vcl_recv {
if (req.method == "PURGE") {
if (!client.ip ~ purge) {
return(synth(405,"Method not allowed."));
}
if (req.http.tags) {
set req.http.n-gone = ykey.purge_header(req.http.tags, sep=",", true);
return (synth(200, "Invalidated " + req.http.n-gone + " objects"))
}
return (purge);
}
}
sub vcl_backend_response {
ykey.add_header(beresp.http.tags, sep=",");
ykey.add_key("all");
if(beresp.http.Content-Type ~ "^image/") {
ykey.add_key("image");
}
}
curl -XPURGE 
-H"tags:cat_1,p_4" https://example.com/
WE HAVE A
LOT OF COOL
MODULES IN OUR
ENTEPRISE
PRODUCT
ACCEPT
ACCOUNTING
ACL (ACLPLUS)
ACTIVEDNS
AKAMAI CONNECTOR
AWS VCL
BODY ACCESS &
TRANSFORMATION
BODYACCESS
BROTLI
COOKIE PLUS
DEVICEATLAS
DIGEST
DYNAMIC BACKENDS
EDGESTASH
FILE
FORMAT
GEOLOCATION
HEADER MANIPULATION
HTTP COMMUNICATION
IMAGE COMPRESSION
JSON PARSING
JWT
KEY VALUE STORAGE
LEAST CONNECTIONS DIRECTOR
MODULE TO CONTROL THE BUILT-IN
HTTP2 H2 TRANSPORT CONTROL
STORAGE CONTROL
PROBE PROXY
PROXYV2 TLV ATTRIBUTE
EXTRACTION
PSEUDO RANDOM NUMBER
GENERATOR
PURGE & SOFT PURGE
REAL-TIME STATUS
REVERSE DNS
REWRITE
S3 VMOD
SESSION
SLICER
SQLITE3
STALE
PROMETHEUS
STRING FUNCTIONS
SYNTHETIC BACKENDS
TAG-BASED INVALIDATION
TCP CONFIGURATION
TLS
TOTAL ENCRYPTION
UNIFIED DIRECTOR OBJECT
URI FUNCTIONS
UNIX DOMAIN SOCKET FUNCTIONS
URL MANIPULATION
UTILS
VSTHROTTLE
EDGESTASH
HEADER MANIPULATION
Developing Cacheable PHP Applications - PHP SP 2024

Developing Cacheable PHP Applications - PHP SP 2024

  • 1.
  • 2.
  • 3.
    WEB PERFORMANCE ISAN ESSENTIAL PART OF THE USER EXPERIENCE
  • 4.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
    AFTER A WHILEYOU HIT THE LIMITS
  • 13.
  • 14.
  • 15.
  • 16.
    I'M THE TECHEVANGELIST AT VARNISH SOFTWARE
  • 17.
  • 21.
    WE BUILD SOFTWARE-DEFINED WEBACCELERATION & CONTENT DELIVERY SOLUTIONS
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
    1.5 Tbps per server 1.4Gbps per watt ACHIEVE GROWTH, PERFORMANCE & SUSTAINABILITY GOALS
  • 31.
    ✓ ACCELERATE DELIVERYOF RESULTS FOR MUNICIPAL ELECTIONS IN BRAZIL ✓ REAL-TIME RESULTS ✓ MULTIPLE POINTS OF PRESENCE ✓ VERY HIGH CONCURRENCY ✓ KUBERNETES 2 DOZEN PODS 50,000,000 REQ/MINUTE
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
    Cache-Control: public, max-age=3600,stale- while-revalidate=100
  • 38.
  • 39.
    AND VARNISH COMPLIES TOTHOSE HTTP CACHING CONVENTIONS
  • 40.
  • 41.
  • 42.
    VARNISH DOESN'T CACHE WHEN THERE'SAN AUTHORIZATION HEADER
  • 43.
    FOR YOUR EYESONLY NOT CACHED
  • 44.
  • 45.
  • 46.
    VCL ✓ DOMAIN-SPECIFIC LANGUAGE ✓CURLY BRACES ✓ CONTROLS REQUESTS, RESPONSES, BACKENDS & CACHING BEHAVIOR ✓ TRANSPILED INTO C-CODE AND COMPILED INTO MACHINE CODE ✓ NOT A TOP-DOWN PROGRAMMING LANGUAGE ✓ HOOKS INTO FINITE STATE MACHINE
  • 48.
    vcl 4.1; backend default{ .host = "127.0.0.1"; .port = "8080"; } sub vcl_recv { if(req.url ~ "^/admin(/.*|$)") { return(pass); } unset req.http.Cookie; }
  • 49.
    vcl 4.1; import cookie; backenddefault { .host = "127.0.0.1"; .port = "8080"; } sub vcl_recv { if (req.http.cookie) { cookie.parse(req.http.cookie); cookie.keep("PHPSESSID"); set req.http.cookie = cookie.get_string(); if (req.http.cookie ~ "^s*$") { unset req.http.cookie; } } }
  • 52.
  • 53.
  • 54.
    <?php namespace AppController; use SymfonyBundleFrameworkBundleControllerAbstractController; useSymfonyComponentHttpFoundationResponse; use SymfonyComponentRoutingAttributeRoute; class DefaultController extends AbstractController { #[Route('/', name: 'home')] public function index(): Response { return $this ->render('default/index.html.twig') ->setPublic() ->setMaxAge(100) ->setSharedMaxAge(3600); } }
  • 55.
  • 56.
    #[Route('/private', name: 'private')] publicfunction private(): Response { $response = $this ->render('default/private.html.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-cache'); $response->headers->addCacheControlDirective('no-store'); return $response; }
  • 57.
  • 58.
    USER VARNISH SERVER ASYNCFETCH SEND STALE RESPONSE WHILE FETCHING
  • 59.
  • 60.
    <?php namespace AppController; use SymfonyBundleFrameworkBundleControllerAbstractController; useSymfonyComponentHttpFoundationResponse; use SymfonyComponentRoutingAttributeRoute; class DefaultController extends AbstractController { #[Route('/', name: 'home')] public function index(): Response { return $this ->render('default/index.html.twig') ->setPublic() ->setMaxAge(100) ->setSharedMaxAge(3600) ->setStaleWhileRevalidate(7200); } }
  • 61.
    OBJECT LIFETIME =TTL + GRACE + KEEP
  • 62.
    OBJECT LIFETIME =TTL + GRACE + KEEP DEFAULT VALUE: 2 MINUTES
  • 63.
    OBJECT LIFETIME =TTL + GRACE + KEEP SERVE STALE CONTENT, ASYNC REVALIDATION DEFAULT VALUE: 10 SECONDS CAN BE SET BY "STALE-WHILE- REVALIDATE"
  • 64.
    OBJECT LIFETIME =TTL + GRACE + KEEP KEEP THE OBJECT AROUND FOR LATER USE DEFAULT VALUE: 0 SECONDS SYNCHRONOUS REVALIDATION: CONDITIONAL REQUESTS
  • 65.
    OBJECT LIFETIME =TTL + GRACE + KEEP
  • 66.
    OBJECT LIFETIME =100 + 10 + 0
  • 67.
    OBJECT LIFETIME =100 + 10 + 0 CACHE HIT
  • 68.
    OBJECT LIFETIME =0 + 10 + 0 EXPIRED
  • 69.
    OBJECT LIFETIME =0 + 10 + 0 STALE ASYNC REVALIDATION
  • 70.
    OBJECT LIFETIME =-8 + 10 + 0
  • 71.
    OBJECT LIFETIME =-8 + 10 + 0 ASYNC REVALIDATION
  • 72.
    OBJECT LIFETIME =-11 + 10 + 0
  • 73.
    OBJECT LIFETIME =-11 + 10 + 0 EXPIRED & OUT OF GRACE SYNCHRONOUS REVALIDATION
  • 74.
    OBJECT LIFETIME =-11 + 10 + 100
  • 75.
    OBJECT LIFETIME =-11 + 10 + 100 EXPIRED & OUT OF GRACE SYNCHRONOUS REVALIDATION KEEP OBJECT AROUND FOR CONDITIONAL REQUESTS
  • 76.
  • 77.
    ONLY FETCH PAYLOADTHAT HAS CHANGED DURING REVALIDATION
  • 78.
  • 79.
  • 80.
    CONDITIONAL REQUESTS HTTP/1.1 200OK Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost
  • 81.
    CONDITIONAL REQUESTS HTTP/1.1 304Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 GET / HTTP/1.1 Host: localhost If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  • 82.
    CONDITIONAL REQUESTS HTTP/1.1 200OK Host: localhost Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost
  • 83.
    CONDITIONAL REQUESTS HTTP/1.1 304Not Modified Host: localhost Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT GET / HTTP/1.1 Host: localhost If-Modified-Since: Fri, 22 Jul 2016 10:11:16 GMT
  • 84.
  • 85.
  • 86.
    <?php namespace AppEventListener; use SymfonyComponentHttpFoundationResponse; useSymfonyComponentEventDispatcherAttributeAsEventListener; use SymfonyComponentHttpKernelEventRequestEvent; use SymfonyComponentHttpKernelEventResponseEvent; use SymfonyComponentHttpKernelKernelEvents; use SymfonyContractsCacheCacheInterface; use SymfonyContractsCacheItemInterface; final class EtagListener { public function __construct(private CacheInterface $cache) { } ...
  • 87.
    <?php namespace AppEventListener; use SymfonyComponentHttpFoundationResponse; useSymfonyComponentEventDispatcherAttributeAsEventListener; use SymfonyComponentHttpKernelEventRequestEvent; use SymfonyComponentHttpKernelEventResponseEvent; use SymfonyComponentHttpKernelKernelEvents; use SymfonyContractsCacheCacheInterface; use SymfonyContractsCacheItemInterface; final class EtagListener { public function __construct(private CacheInterface $cache) { } ...
  • 88.
    #[AsEventListener(event: KernelEvents::REQUEST)] public functiononKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get(); if($etagData !== null) { $etag = $etagData[0]; $cc = $etagData[1]; } else { $etag = null; $cc = null; } $response = new Response(); $response->setEtag($etag); if($cc !== null) { $response->headers->add(['Cache-Control'=>$cc]); } if($response->isNotModified($request)) { $event->setResponse($response); } }
  • 89.
    #[AsEventListener(event: KernelEvents::REQUEST)] public functiononKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get(); if($etagData !== null) { $etag = $etagData[0]; $cc = $etagData[1]; } else { $etag = null; $cc = null; } $response = new Response(); $response->setEtag($etag); if($cc !== null) { $response->headers->add(['Cache-Control'=>$cc]); } if($response->isNotModified($request)) { $event->setResponse($response); } }
  • 90.
    #[AsEventListener(event: KernelEvents::REQUEST)] public functiononKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get(); if($etagData !== null) { $etag = $etagData[0]; $cc = $etagData[1]; } else { $etag = null; $cc = null; } $response = new Response(); $response->setEtag($etag); if($cc !== null) { $response->headers->add(['Cache-Control'=>$cc]); } if($response->isNotModified($request)) { $event->setResponse($response); } }
  • 91.
    #[AsEventListener(event: KernelEvents::REQUEST)] public functiononKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get(); if($etagData !== null) { $etag = $etagData[0]; $cc = $etagData[1]; } else { $etag = null; $cc = null; } $response = new Response(); $response->setEtag($etag); if($cc !== null) { $response->headers->add(['Cache-Control'=>$cc]); } if($response->isNotModified($request)) { $event->setResponse($response); } }
  • 92.
    #[AsEventListener(event: KernelEvents::REQUEST)] public functiononKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get(); if($etagData !== null) { $etag = $etagData[0]; $cc = $etagData[1]; } else { $etag = null; $cc = null; } $response = new Response(); $response->setEtag($etag); if($cc !== null) { $response->headers->add(['Cache-Control'=>$cc]); } if($response->isNotModified($request)) { $event->setResponse($response); } }
  • 93.
    #[AsEventListener(event: KernelEvents::REQUEST)] public functiononKernelRequest(RequestEvent $event): void { $request = $event->getRequest(); $etagData = $this->cache->getItem('etag-'.md5($request->getUri()))->get(); if($etagData !== null) { $etag = $etagData[0]; $cc = $etagData[1]; } else { $etag = null; $cc = null; } $response = new Response(); $response->setEtag($etag); if($cc !== null) { $response->headers->add(['Cache-Control'=>$cc]); } if($response->isNotModified($request)) { $event->setResponse($response); } }
  • 94.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 95.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 96.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 97.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 98.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 99.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 100.
    #[AsEventListener(event: KernelEvents::RESPONSE)] public functiononKernelResponse(ResponseEvent $event) { $response = $event->getResponse(); if($response->getStatusCode() !== Response::HTTP_NOT_MODIFIED) { $request = $event->getRequest(); $etag = md5($response->getContent()); $cc = $response->headers->get('Cache-Control'); $response->setEtag($etag); if(!$response->isNotModified($request)) { $this->cache->get('etag-'.md5($request->getUri()), function(ItemInterface $item) use($etag, $cc){ $item->expiresAfter(3600); $item->set([$etag,$cc]); return [$etag,$cc]; }); } } }
  • 101.
  • 102.
  • 104.
  • 105.
  • 106.
  • 107.
  • 108.
  • 109.
    ESI ✓ PLACEHOLDER ✓ PARSEDBY VARNISH ✓ OUTPUT IS A COMPOSITION OF BLOCKS ✓ STATE PER BLOCK ✓ TTL PER BLOCK
  • 110.
    <!DOCTYPE html> <html> <body> <esi:include src="/header"/> <p>Main content</p> </body> </html>
  • 111.
  • 112.
    GET / HTTP/1.1 Host:example.com User-Agent: Chrome Accept: */*
  • 113.
    GET / HTTP/1.1 Host:example.com User-Agent: Chrome Accept: */* Surrogate-Capability = "key=ESI/1.0" ANNOUNCE ESI SUPPORT
  • 114.
    HTTP/1.1 200 OK Content-type:text/html; charset=UTF-8 Cache-Control: public, max-age=3600 Surrogate-Control = "key=ESI/1.0" CONTROL ESI PARSING
  • 115.
    GET /header HTTP/1.1 Host:example.com Surrogate-Capability = "key=ESI/1.0" SUBREQUEST SENT BY VARNISH
  • 116.
    HTTP/1.1 200 OK Content-type:text/html; charset=UTF-8 Cache-Control: no-store WON'T BE CACHED
  • 117.
    HTTP/1.1 200 OK Content-type:text/html; charset=UTF-8 Cache-Control: public, max-age=3600 SINGLE HTTP RESPONSE TO CLIENT
  • 118.
    <!DOCTYPE html> <html> <body> <esi:include src="/header"/> <p>Main content</p> </body> </html> SERVER RESPONSE
  • 119.
    <!DOCTYPE html> <html> <body> <p>Welcome Thijs</p> <p>MainContent</p> </body> </html> EDGE RESPONSE
  • 120.
    sub vcl_backend_fetch { setbereq.http.Surrogate-Capability = "key=ESI/1.0"; } sub vcl_backend_response { if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; set beresp.do_esi = true; } }
  • 121.
    sub vcl_backend_fetch { setbereq.http.Surrogate-Capability = "key=ESI/1.0"; } sub vcl_backend_response { if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; set beresp.do_esi = true; } } ANNOUNCE ESI SUPPORT NEGOTIATE WITH BACKEND
  • 122.
  • 123.
    class DefaultController extendsAbstractController { #[Route('/', name: 'home')] public function index(): Response { return $this ->render('default/index.html.twig') ->setPublic() ->setMaxAge(10) ->setSharedMaxAge(60); } #[Route('/header', name: 'header')] public function header(): Response { return $this ->render('default/header.html.twig') ->setPublic() ->setSharedMaxAge(3600); } #[Route('/footer', name: 'footer')] public function footer(): Response { return $this ->render('default/footer.html.twig') ->setPublic() ->setSharedMaxAge(86400); } } ROUTE PER FRAGMENT
  • 124.
    class DefaultController extendsAbstractController { #[Route('/', name: 'home')] public function index(): Response { return $this ->render('default/index.html.twig') ->setPublic() ->setMaxAge(10) ->setSharedMaxAge(60); } #[Route('/header', name: 'header')] public function header(): Response { return $this ->render('default/header.html.twig') ->setPublic() ->setSharedMaxAge(3600); } #[Route('/footer', name: 'footer')] public function footer(): Response { return $this ->render('default/footer.html.twig') ->setPublic() ->setSharedMaxAge(86400); } }
  • 125.
    class DefaultController extendsAbstractController { #[Route('/', name: 'home')] public function index(): Response { return $this ->render('default/index.html.twig') ->setPublic() ->setMaxAge(10) ->setSharedMaxAge(60); } #[Route('/header', name: 'header')] public function header(): Response { return $this ->render('default/header.html.twig') ->setPublic() ->setSharedMaxAge(3600); } #[Route('/footer', name: 'footer')] public function footer(): Response { return $this ->render('default/footer.html.twig') ->setPublic() ->setSharedMaxAge(86400); } }
  • 126.
    class DefaultController extendsAbstractController { #[Route('/', name: 'home')] public function index(): Response { return $this ->render('default/index.html.twig') ->setPublic() ->setMaxAge(10) ->setSharedMaxAge(60); } #[Route('/header', name: 'header')] public function header(): Response { return $this ->render('default/header.html.twig') ->setPublic() ->setSharedMaxAge(3600); } #[Route('/footer', name: 'footer')] public function footer(): Response { return $this ->render('default/footer.html.twig') ->setPublic() ->setSharedMaxAge(86400); } }
  • 127.
    <!DOCTYPE html> <html> <body> <div class="container"> {{include('default/header.html.twig') }} {% block content %} {% endblock %} {{ include('default/footer.html.twig') }} </div> </body> </html> <!DOCTYPE html> <html> <body> <div class="container"> {{ render_esi(url('header')) }} {% block content %} {% endblock %} {{ render_esi(url('footer')) }} </div> </body> </html>
  • 128.
    <!DOCTYPE html> <html> <body> <div class="container"> {{render_esi(url('header')) }} {% block content %} {% endblock %} {{ render_esi(url('footer')) }} </div> </body> </html> <!DOCTYPE html> <html> <body> <div class="container"> <esi:include src="/header" /> {% block content %} {% endblock %} <esi:include src="/footer" /> </div> </body> </html>
  • 129.
    <!DOCTYPE html> <html> <body> <div class="container"> {{render_esi(url('header')) }} {% block content %} {% endblock %} {{ render_esi(url('footer')) }} </div> </body> </html> FALLS BACK TO INTERNAL SUBREQUESTS IF SURROGATE-CAPABILITY HEADER DOESN'T MATCH
  • 130.
  • 131.
    HOW DO YOUIDENTIFY AN OBJECT IN CACHE?
  • 132.
  • 133.
    WHAT IF THECONTENT OF A URL VARIES BASED ON THE VALUE OF A REQUEST HEADER?
  • 134.
  • 135.
    USER VARNISH SERVER GET/ HTTP/1.1 Host: example.com Accept-Language: nl example.com / en HTTP/1.1 200 OK Cache-Control: public, max-age=500 Vary: Accept-Language example.com / nl
  • 136.
  • 137.
    vcl 4.1; import accept; subvcl_init { new format = accept.rule("text/plain"); format.add("text/html"); format.add("application/json"); format.add("text/plain"); new lang = accept.rule("en"); lang.add("en"); lang.add("fr"); lang.add("nl"); lang.add("pt"); } sub vcl_recv { set req.http.accept-language = lang.filter(req.http.accept-language); set req.http.accept= format.filter(req.http.accept); }
  • 138.
  • 139.
    <?php namespace AppEventListener; use SymfonyComponentEventDispatcherAttributeAsEventListener; useSymfonyComponentHttpKernelEventResponseEvent; use SymfonyComponentHttpKernelKernelEvents; final class VaryListener { #[AsEventListener(event: KernelEvents::RESPONSE)] public function onKernelResponse(ResponseEvent $event): void { $event ->getResponse() ->setVary('Accept-Language, X-Forwarded-Proto',false); } }
  • 140.
    WHAT IF YOUCAN'T LEVERAGE HTTP?
  • 141.
  • 142.
    vcl 4.1; import std; importcookie; backend server1 { .host = "127.0.0.1"; .port = "8080"; .probe = { .url = "/"; } } acl purge { "localhost"; "127.0.0.1"; "::1"; }
  • 143.
    sub vcl_recv { setreq.http.Host = regsub(req.http.Host, ":[0-9]+", ""); unset req.http.proxy; set req.url = std.querysort(req.url); set req.url = regsub(req.url, "?$", ""); set req.http.Surrogate-Capability = "key=ESI/1.0"; if (std.healthy(req.backend_hint)) { set req.grace = 10s; } if (!req.http.X-Forwarded-Proto) { if(std.port(server.ip) == 443 || std.port(server.ip) == 8443) { set req.http.X-Forwarded-Proto = "https"; } else { set req.http.X-Forwarded-Proto = "https"; } } if (req.http.Upgrade ~ "(?i)websocket") { return (pipe); } ...
  • 144.
    if (req.url ~"(?|&)(utm_source|utm_medium|utm_campaign|utm_content|gclid|cx|ie|cof| siteurl)=") { set req.url = regsuball(req.url, "&(utm_source|utm_medium|utm_campaign|utm_content| gclid|cx|ie|cof|siteurl)=([A-z0-9_-.%25]+)", ""); set req.url = regsuball(req.url, "?(utm_source|utm_medium|utm_campaign|utm_content| gclid|cx|ie|cof|siteurl)=([A-z0-9_-.%25]+)", "?"); set req.url = regsub(req.url, "?&", "?"); set req.url = regsub(req.url, "?$", ""); } if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(405, client.ip + " is not allowed to send PURGE requests.")); } return (purge); }
  • 145.
    if (req.url ~"^[^?]*.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg| js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz| swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(?.*)?$") { unset req.http.Cookie; } if (req.http.cookie) { cookie.parse(req.http.cookie); cookie.keep("PHPSESSID"); set req.http.cookie = cookie.get_string(); if (req.http.cookie ~ "^s*$") { unset req.http.cookie; } } } sub vcl_hash { hash_data(req.http.X-Forwarded-Proto); }
  • 146.
    sub vcl_backend_response { if(bereq.url ~ "^[^?]*.(7z|avi|bmp|bz2|css|csv|doc|docx|eot|flac|flv|gif|gz|ico|jpeg|jpg| js|less|mka|mkv|mov|mp3|mp4|mpeg|mpg|odt|ogg|ogm|opus|otf|pdf|png|ppt|pptx|rar|rtf|svg|svgz| swf|tar|tbz|tgz|ttf|txt|txz|wav|webm|webp|woff|woff2|xls|xlsx|xml|xz|zip)(?.*)?$") { unset beresp.http.Set-Cookie; set beresp.ttl = 1d; } if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; set beresp.do_esi = true; } set beresp.grace = 6h; }
  • 147.
  • 148.
    acl purge { "localhost"; "127.0.0.1"; "::1"; } subvcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge) { return(synth(405,"Method not allowed.")); } return (purge); } }
  • 149.
  • 150.
    acl purge { "localhost"; "127.0.0.1"; "::1"; } subvcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge) { return(synth(405,"Method not allowed.")); } if (req.http.X-Purge-Method == "regex") { ban("obj.http.url ~ " + req.url + " && obj.http.host == " + req.http.host); return(synth(200, "Purged")); } return (purge); } } sub vcl_backend_response { set beresp.http.url = bereq.url; set beresp.http.host = bereq.http.host; } sub vcl_deliver { unset resp.http.url; unset resp.http.host; }
  • 151.
    curl -XPURGE -H"X-Purge-Method:regex"https://example.com/catalog/
  • 152.
    import ykey; acl purge{ "localhost"; "127.0.0.1"; "::1"; } sub vcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge) { return(synth(405,"Method not allowed.")); } if (req.http.tags) { set req.http.n-gone = ykey.purge_header(req.http.tags, sep=",", true); return (synth(200, "Invalidated " + req.http.n-gone + " objects")) } return (purge); } } sub vcl_backend_response { ykey.add_header(beresp.http.tags, sep=","); ykey.add_key("all"); if(beresp.http.Content-Type ~ "^image/") { ykey.add_key("image"); } }
  • 153.
    curl -XPURGE -H"tags:cat_1,p_4"https://example.com/
  • 154.
    WE HAVE A LOTOF COOL MODULES IN OUR ENTEPRISE PRODUCT
  • 155.
    ACCEPT ACCOUNTING ACL (ACLPLUS) ACTIVEDNS AKAMAI CONNECTOR AWSVCL BODY ACCESS & TRANSFORMATION BODYACCESS BROTLI COOKIE PLUS DEVICEATLAS DIGEST DYNAMIC BACKENDS EDGESTASH FILE FORMAT GEOLOCATION HEADER MANIPULATION HTTP COMMUNICATION IMAGE COMPRESSION JSON PARSING JWT KEY VALUE STORAGE LEAST CONNECTIONS DIRECTOR MODULE TO CONTROL THE BUILT-IN HTTP2 H2 TRANSPORT CONTROL STORAGE CONTROL PROBE PROXY PROXYV2 TLV ATTRIBUTE EXTRACTION PSEUDO RANDOM NUMBER GENERATOR PURGE & SOFT PURGE REAL-TIME STATUS REVERSE DNS REWRITE S3 VMOD SESSION SLICER SQLITE3 STALE PROMETHEUS STRING FUNCTIONS SYNTHETIC BACKENDS TAG-BASED INVALIDATION TCP CONFIGURATION TLS TOTAL ENCRYPTION UNIFIED DIRECTOR OBJECT URI FUNCTIONS UNIX DOMAIN SOCKET FUNCTIONS URL MANIPULATION UTILS VSTHROTTLE EDGESTASH HEADER MANIPULATION