Share
Embed
Caching the Uncacheable
Or how to deliver content tailored to visitors at lightning speed.
Thomas Schedler
October 12, 2023
Transcript
55 slides
-
Caching the Uncacheable Or how to deliver content tailored
to visitors at lightning speed. -
Hi, I’m Thomas Schedler @chirimoya – Co-founder & CEO of
Sulu GmbH – More than 20 years of experience in web technologies & development – PHP, Symfony, React, SQL, Redis, Elasticsearch, Varnish, … – Open source enthusiast – Loves cooking and mountains [email protected] https://github.com/chirimoya -
Why HTTP Caching? – Improves web performance by reducing server
load and response times – Enhances user experience by delivering content faster – Saves costs by reducing the resources needed to handle high traffic volumes -
“ HTTP caching is a technique that allows web resources
to be temporarily stored on a client's device or an intermediary server, reducing the need for requests to the original server and improving web performance. ChatGPT -
HTTP Cache – Caches entire HTTP responses – Controlled using
HTTP headers (e.g., Cache-Control, ETag, …) – Examples: browser caches, CDNs, reverse proxy caches – Best for static content or content that doesn't change frequently -
Layers of HTTP-Caching – Browser Cache – Shared Proxies –
Reverse Proxies -
Layers of HTTP-Caching – Browser Cache – Stores web content
locally on the user's device – Provides instant access to previously visited content without re-fetching from the server – Controlled by cache headers like Cache-Control and Expires – Shared Proxies – Reverse Proxies -
Layers of HTTP-Caching – Browser Cache – Shared Proxies –
Distributed systems that cache content closer to the user's location – Enhance content delivery speed by serving cached content from a nearby location – Useful for serving static assets like images, scripts and stylesheets to a global audience – Reverse Proxies -
Layers of HTTP-Caching – Browser Cache – Shared Proxies –
Reverse Proxies – Sit between client and web server, caching responses from the server – Examples include Varnish, Nginx, and Symfony HTTP-Cache – Can cache dynamic content and provide load balancing – Controlled by cache headers and can be configured for specific caching rules -
HTTP Header Cache-Control The Cache-Control HTTP header field holds directives
that control caching in browsers and shared caches (e.g. Proxies, CDNs) -
Cache-Control Directives – public — Indicates that the response can
be stored in any cache, including shared and private caches – private — Indicates that the response can be stored only in a private cache (e.g. local caches in browsers) – no-cache — Indicates that the response can be stored in caches, but the response must be validated with the origin server before each reuse – no-store — Indicates that any caches of any kind (private or shared) should not store this response -
Cache-Control Directives – max-age=[N] — Indicates that the response remains
fresh until N seconds after the response is generated – s-maxage=[N] — Indicates how long the response is fresh for (similar to max-age) — but it is specific to shared caches, and they will ignore max-age when it is present – must-revalidate — Indicates that the response can be stored in caches and can be reused while fresh. If the response becomes stale, it must be validated with the origin server before reuse -
Cache-Control Directives – stale-while-revalidate — Indicates that the cache could
reuse a stale response while it revalidates the response in the background – stale-if-error — Indicates that the cache can reuse a stale response when an upstream server generates an error – … https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control -
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 Expires: Sat, 27 Oct 2023 16:00:00 GMT
<!DOCTYPE html> <html> <body> More Content </body> </html> GET / -
HTTP/1.1 200 OK Last-Modified: Wed, 24 Oct 2023 07:28:00 GMT
<!DOCTYPE html> <html> <body> Quite recent Content </body> </html> GET / GET / If-Modified-Since: Wed, 24 Oct 2023 07:28:00 GMT HTTP/1.1 304 Not Modified -
HTTP/1.1 200 OK ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" <!DOCTYPE html> <html> <body> Still
more Content </body> </html> GET / GET / If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" HTTP/1.1 304 Not Modified -
Symfony Reverse Proxy Symfony comes with a reverse proxy written
in PHP. It's not a fully-featured reverse proxy cache like Varnish, but it is a great way to start. -
// src/Controller/LuckyController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route;
class LuckyController extends AbstractController { #[Route('/lucky/number')] public function number(): Response { $number = random_int(0, 100); return $this->render('lucky/number.html.twig', [ 'number' => $number, ]); } } -
// src/Controller/LuckyController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\Cache;
use Symfony\Component\Routing\Annotation\Route; class LuckyController extends AbstractController { #[Route('/lucky/number')] #[Cache(public: true, maxage: 3600, mustRevalidate: true)] public function number(): Response { $number = random_int(0, 100); return $this->render('lucky/number.html.twig', [ 'number' => $number, ]); } } -
<?php // src/Controller/LuckyController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use
Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\Cache; use Symfony\Component\Routing\Annotation\Route; class LuckyController extends AbstractController { #[Route('/lucky/number')] #[Cache(public: true, maxage: 3600, mustRevalidate: true)] public function number(Request $request): Response { $session = $request->getSession(); $number = $session->get('number', random_int(0, 100)); $session->set('number', $number); return $this->render('lucky/number.html.twig', [ 'number' => $number, ]); } } ! -
“ Caching entire responses isn't always possible for highly dynamic
sites, or is it? -
ESI - Edge Side Includes – The ESI specification describes
tags to communicate with the gateway cache – In Symfony the <esi:include/> is implemented – If the response contains ESI tags, the cache either requests the page fragment from the backend or embeds the fresh cache entry -
{# templates/pages/homepage.html.twig #} {# you can use a controller reference
#} {{ render_esi(controller('App\\Controller\\WeatherController::forecast' })) }} {# ... or a URL #} {{ render_esi(url('weather_forecast')) }} -
Cache Invalidation „There are only two hard things in Computer
Science: cache invalidation and naming things.“ Phil Karlton -
Cache Invalidation – Removing or marking cached resources as stale
or outdated – Using mechanisms like cache control headers, conditional requests – Or by explicitly purging cached entries -
HTTP/2 200 OK … Cache-Control: public, max-age=240, s-maxage=240 … <!DOCTYPE
html> <html> <body> Deliver awesome, robust, reliable websites with Sulu CMS </body> </html> GET https://sulu.io/ -
HTTP/2 200 OK … Cache-Control: public, max-age=240, s-maxage=600 X-Cache: HIT
… <!DOCTYPE html> <html> <body> Deliver awesome, robust, reliable websites with Sulu CMS </body> </html> GET https://sulu.io/ -
HTTP/2 200 OK … Cache-Control: public, max-age=240, s-maxage=240 X-Cache: HIT
X-Reverse-Proxy-TTL: 86400 … <!DOCTYPE html> <html> <body> Deliver awesome, robust, reliable websites with Sulu CMS </body> </html> GET https://sulu.io/ -
HTTP/2 200 OK … Cache-Control: public, max-age=240, s-maxage=240 X-Cache: HIT
X-Reverse-Proxy-TTL: 86400 X-Cache-Tags: 22a92d46, cf4a07fe … <!DOCTYPE html> <html> <body> Deliver awesome, robust, reliable websites with Sulu CMS </body> </html> GET https://sulu.io/ -
HTTP/2 200 Purged <!DOCTYPE html> <html> <head> <title>200 Purged</title> </head>
<body> … </body> </html> PURGE https://sulu.io/ -
HTTP/2 200 Banned <!DOCTYPE html> <html> <head> <title>200 Banned</title> </head>
<body> … </body> </html> BAN https://sulu.io/ X-Cache-Tags: 22a92d46 -
FOSHttpCacheBundle This bundle offers tools to improve HTTP caching
with Symfony. https://github.com/FriendsOfSymfony/FOSHttpCacheBundle -
Vary HTTP Header The Vary HTTP header is used to
create cache variations -
Vary HTTP Header – The Vary HTTP response header describes
the parts of the request message aside from the method and URL that influenced the content of the response – Most often, this is used to create a cache key when content negotiation is in use – Vary: Accept-Encoding, <header-name>, ... https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary -
Facts & Figures – GU recipe portal – Visits: ~6,8
m – PIs: ~12,6 m – Recipes: ~40.000 API – GU Recipes Küchengötter https://www.kuechengoetter.de -
Facts & Figures – GU recipe portal – Visits: ~6,8
m – PIs: ~12,6 m – Recipes: ~40.000 API – GU Recipes Küchengötter https://www.kuechengoetter.de -
Facts & Figures – GU recipe portal – Visits: ~6,8
m – PIs: ~12,6 m – Recipes: ~40.000 API – GU Recipes Küchengötter https://www.kuechengoetter.de -
Varnish The free, open source software that enables super fast
delivery of HTTP based content. -
Varnish – High-Performance HTTP Caching – Load Balancing – Feature
complete – Edge Side Includes (ESI) – Purging and Banning – Grace Mode – … – Flexible Configuration Language -
Varnish Con fi guration Language The Varnish Configuration Language (VCL)
is a domain- specific programming language used by Varnish to control request handling, routing, caching, and several other aspects. -
# /etc/varnish/default.vcl vcl 4.1; acl invalidators { "localhost"; } backend
default { .host = "host.docker.internal"; .port = "8000"; } sub vcl_recv { if (req.method == "PURGE") { if (!client.ip ~ invalidators) { return (synth(405, "Not allowed")); } return (purge); } } -
# /etc/varnish/default.vcl # … sub vcl_backend_response { set beresp.grace =
24h; if (beresp.http.X-Reverse-Proxy-TTL) { set beresp.ttl = std.duration(beresp.http.X-Reverse-Proxy-TTL + "s", 0s); unset beresp.http.X-Reverse-Proxy-TTL; } } sub vcl_deliver { if (resp.http.X-Cache-Debug) { if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } } else { unset resp.http.X-Reverse-Proxy-TTL; unset resp.http.X-Cache-Tags; } } -
# /etc/varnish/default.vcl vcl 4.1; # see docs about redis here
https://github.com/carlosabalde/libvmod-redis import redis; # see docs about cookie here https://github.com/varnish/varnish-modules import cookie; # ... sub vcl_init { # VMOD configuration: simple case, keeping up to # one Redis connection per Varnish worker thread. new db = redis.db( location="127.0.0.1:6379", type=master, connection_timeout=500, shared_connections=false, max_connections=1); } -
# /etc/varnish/default.vcl sub vcl_recv { # ... // Extract SESSIONID
Header, validate it and if its valid get cache group from redis set req.http.X-ABO = ""; cookie.parse(req.http.cookie); set req.http.X-SESSION-ID = cookie.get("PHPSESSID"); if (req.http.X-SESSION-ID ~ "[0-9a-zA-z,-]+") { db.command("GET"); db.push("SESSION_ID_" + req.http.X-SESSION-ID); db.execute(); set req.http.X-ABO = db.get_string_reply(); } # ... } -
class RecipeController extends AbstractController { public function detail(Request $request): Response
{ // ... $response = $this->render('recipe.html.twig', [ 'recipe' => $recipe, ]); if ($recipe->isPlus()) { $response->setVary(‚X-ABO'); } return $response; } }