Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Caching the 
Uncacheable

Caching the 
Uncacheable

Or how to deliver content tailored to visitors at lightning speed.

Thomas Schedler

October 12, 2023
Tweet

More Decks by Thomas Schedler

Other Decks in Technology

Transcript

  1. 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
  2. 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
  3. “ 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
  4. 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
  5. 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
  6. 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
  7. 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
  8. HTTP Header Cache-Control The Cache-Control HTTP header field holds directives

    that control caching in browsers and shared caches (e.g. Proxies, CDNs)
  9. 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
  10. 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
  11. 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
  12. HTTP/1.1 200 OK Expires: Sat, 27 Oct 2023 16:00:00 GMT

    <!DOCTYPE html> <html> <body> More Content </body> </html> GET /
  13. 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
  14. 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
  15. 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.
  16. // 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, ]); } }
  17. // 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, ]); } }
  18. <?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, ]); } } !
  19. 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
  20. {# 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')) }}
  21. Cache Invalidation „There are only two hard things in Computer

    Science: cache invalidation and naming things.“ 
 
 Phil Karlton
  22. 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
  23. 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/
  24. 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/
  25. 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/
  26. 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/
  27. HTTP/2 200 Banned <!DOCTYPE html> <html> <head> <title>200 Banned</title> </head>

    <body> … </body> </html> BAN https://sulu.io/ 
 X-Cache-Tags: 22a92d46
  28. FOSHttpCacheBundle This bundle offers tools to improve 
 HTTP caching

    with Symfony. https://github.com/FriendsOfSymfony/FOSHttpCacheBundle
  29. 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
  30. 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
  31. 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
  32. 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
  33. Varnish – High-Performance HTTP Caching – Load Balancing – Feature

    complete – Edge Side Includes (ESI) – Purging and Banning – Grace Mode – … – Flexible Configuration Language
  34. 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.
  35. # /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); } }
  36. # /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; } }
  37. # /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); }
  38. # /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(); } # ... }
  39. 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; } }