Caching the uncachable

Caching the uncachable

Or how to deliver visitor specific content super fast. #symfony #sulu #cms

D6bc02afa30223e3acd9fdf512d82e2c?s=128

Thomas Schedler

October 26, 2018
Tweet

Transcript

  1. Caching the uncachable Or how to deliver visitor specific content

    super fast.
  2. I'm Thomas Schedler @chirimoya | https://github.com/chirimoya ... head of development

    and technical consultant. Young father trying to master Heston Blumenthal recipes.
  3. Why?

  4. None
  5. Who knows about HTTP Caching?

  6. HTTP Caching Basics A short recap.

  7. Browser Reverse Proxy Symfony Application GET / GET / Browser

    200 OK 200 OK GET / 200 OK
  8. HTTP/1.1 200 OK Cache-Control: public, max-age=3600, s-maxage=86400 <!DOCTYPE html> <html>

    <body> Interesting content </body> </html> GET /
  9. HTTP/1.1 200 OK Expires: Sat, 27 Oct 2015 16:00:00 GMT

    <!DOCTYPE html> <html> <body> More Content </body> </html> GET /
  10. HTTP/1.1 200 OK Last-Modified: Wed, 24 Oct 2018 07:28:00 GMT

    <!DOCTYPE html> <html> <body> Quite recent Content </body> </html> GET / GET / If-Modified-Since: Wed, 24 Oct 2018 07:28:00 GMT HTTP/1.1 304 Not Modified
  11. 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
  12. Custom HTTP Header Pass additional information with the request or

    the response.
  13. Browser Reverse Proxy Symfony Application GET / GET / Browser

    200 OK Cache-Control: max-age=240 200 OK X-Reverse-Proxy-TTL: 3600 GET / GET / 200 OK X-Reverse-Proxy-TTL: 3600 200 OK Cache-Control: max-age=240 PURGE /
  14. Browser Reverse Proxy Symfony Application GET / GET / Browser

    200 OK Cache-Control: max-age=240 200 OK X-Reverse-Proxy-TTL: 3600 X-Cache-Tags: p1,p2,p3 GET / GET / 200 OK X-Reverse-Proxy-TTL: 3600 X-Cache-Tags: p1,p2 200 OK Cache-Control: max-age=240 BAN X-Cache-Tags: p3
  15. “ Caching entire responses isn't always possible for highly dynamic

    sites, or is it?
  16. Partly caching with ESI HTTP Standards FTW!

  17. 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
  18. // app/config/config.yml framework:
 ... esi: { enabled: true } //

    app/Resources/views/Default/index.html.twig
 
 {# you can use a controller reference #}
 {{ render_esi(controller('AppBundle:News:latest', { 'limit': 5 })) }}
 
 {# ... or a URL #}
 {{ render_esi(url('latest_news', { 'limit': 5 })) }}
  19. https://www.kuechengoetter.de

  20. Caching Implementations Symfony HttpCache or Varnish?

  21. Symfony HttpCache – Easy to setup – Implemented in PHP

    – Not feature complete
  22. // src/CacheKernel.php <?php namespace App; use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache; class CacheKernel extends

    HttpCache { } // public/index.php <?php use App\Kernel; use App\CacheKernel; // ... $kernel = new Kernel($env, $debug); $kernel = new CacheKernel($kernel); // ...
  23. // src/Controller/NameController.php <?php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class

    NameController { /** * @Route("/name/{name}") */ public function index(string $name) { $response = new Response('Your name is ' . $name); $response->setSharedMaxAge(3600); return $response; } }
  24. Varnish – Super performant – Feature complete – Configured using

    VCL – Implemented in C – Harder to setup
  25. 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
  26. // public/index.php <?php // ... Request::setTrustedProxies( ['127.0.0.1'], Request::HEADER_X_FORWARDED_ALL ); //

    ...
  27. // src/Controller/NameController.php <?php namespace App\Controller; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class

    NameController { /** * @Route("/name/{name}") */ public function index(string $name) { $response = new Response('Your name is ' . $name); $response->setSharedMaxAge(3600); return $response; } }
  28. # 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"; }
  29. # 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. }
  30. None
  31. FOSHttpCacheBundle Simplifies cache handling and invalidation in Symfony applications.

  32. FOSHttpCacheBundle Features – Use rules to set cache headers –

    CacheManager / Annotations for comfortable Invalidation – Tagged Cache Invalidation – User Context based caching using session
  33. Audience Targeting Show and cache different content for different target

    groups on the same URL.
  34. 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
  35. None
  36. None
  37. None
  38. Who knows the Vary header?

  39. Browser Reverse Proxy Symfony Application GET /en Accept-Encoding: gzip, deflate

    Browser 200 OK
 Content-Encoding: gzip 200 OK
 Vary: Accept-Encoding
 Content-Encoding: gzip GET /en Accept-Encoding: gzip, deflate GET /en Accept-Encoding: gzip 200 OK
 Content-Encoding: gzip GET /en Accept-Encoding: deflate 200 OK
 Content-Encoding: deflate 200 OK
 Vary: Accept-Encoding
 Content-Encoding: deflate GET /en Accept-Encoding: deflate Browser
  40. Reverse Proxy Symfony Application GET /en Browser 200 OK X-Sulu-Target-Group:

    1 GET /_sulu_target_group X-Sulu-Original-Url: /en 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 200 OK Vary: X-Sulu-Target-Group X-Reverse-Proxy-TTL: 3600 GET /en X-Sulu-Target-Group: 1
  41. Reverse Proxy Symfony Application Browser GET /about-us X-Sulu-Target-Group: 1 200

    OK X-Reverse-Proxy-TTL: 3600 GET /about-us Cookie: _svtg=1; _svs=… Browser 200 OK Cache-Control: max-age=0 GET /about-us Cookie: _svtg=1; _svs=… 200 OK Cache-Control: max-age=0
  42. Symfony HttpCache Use extended version of the Symfony HttpCache from

    Sulu to support Audience Targeting.
  43. <?php namespace Sulu\Component\HttpCache; use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache as AbstractHttpCache; use Symfony\Component\HttpKernel\HttpKernelInterface; class

    HttpCache extends AbstractHttpCache { private $hasAudienceTargeting; public function __construct( HttpKernelInterface $kernel, $hasAudienceTargeting = false, $cacheDir = null ) { parent::__construct($kernel, $cacheDir); $this->hasAudienceTargeting = $hasAudienceTargeting; } // ...
  44. // app/WebsiteCache.php <?php use Sulu\Component\HttpCache\HttpCache; class WebsiteCache extends HttpCache {

    }
  45. // web/website.php <?php // ... $kernel = new WebsiteKernel(SYMFONY_ENV, SYMFONY_DEBUG);

    if (SYMFONY_ENV !== 'dev') { $kernel = new WebsiteCache($kernel, true); Request::enableHttpMethodParameterOverride(); } // ...
  46. Varnish VCL to the rescue!

  47. “ I really love the Varnish Configuration Language! Said no

    one, ever.
  48. None
  49. # default.vcl vcl 4.0; import header; backend default { .host

    = ”127.0.0.1”; .port = "8000"; }
  50. 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; }
  51. 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; }
  52. 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; }
  53. 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=/;"); } }
  54. 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; }
  55. 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=/;"); } }
  56. 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=/;"); } }
  57. Sulu Docs

  58. How to reevaluate target groups on cache hit? Good old

    Javascript.
  59. Thanks for watching! sulu.io