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

Caching the uncachable

Caching the uncachable

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

Thomas Schedler

October 26, 2018
Tweet

More Decks by Thomas Schedler

Other Decks in Technology

Transcript

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

    View Slide

  2. I'm Thomas Schedler
    @chirimoya | https://github.com/chirimoya
    ... head of development
    and technical consultant. Young
    father trying to master Heston
    Blumenthal recipes.

    View Slide

  3. Why?

    View Slide

  4. View Slide


  5. View Slide

  6. Who knows about HTTP
    Caching?

    View Slide

  7. HTTP Caching Basics
    A short recap.

    View Slide

  8. Browser
    Reverse
    Proxy
    Symfony
    Application
    GET / GET /
    Browser
    200 OK 200 OK
    GET /
    200 OK

    View Slide

  9. HTTP/1.1 200 OK
    Cache-Control: public, max-age=3600, s-maxage=86400



    Interesting content


    GET /

    View Slide

  10. HTTP/1.1 200 OK
    Expires: Sat, 27 Oct 2015 16:00:00 GMT



    More Content


    GET /

    View Slide

  11. HTTP/1.1 200 OK
    Last-Modified: Wed, 24 Oct 2018 07:28:00 GMT



    Quite recent Content


    GET /
    GET /
    If-Modified-Since: Wed, 24 Oct 2018 07:28:00 GMT
    HTTP/1.1 304 Not Modified

    View Slide

  12. HTTP/1.1 200 OK
    ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"



    Still more Content


    GET /
    GET /
    If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
    HTTP/1.1 304 Not Modified

    View Slide

  13. Custom HTTP Header
    Pass additional information with the request or the response.

    View Slide

  14. 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 /

    View Slide

  15. 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

    View Slide


  16. Caching entire responses isn't always possible for
    highly dynamic sites, or is it?

    View Slide

  17. Partly caching with ESI
    HTTP Standards FTW!

    View Slide

  18. ESI - Edge Side Includes
    – The ESI specification describes tags to communicate with the
    gateway cache
    – In Symfony the is implemented
    – If the response contains ESI tags, the cache either requests the page
    fragment from the backend or embeds the fresh cache entry

    View Slide

  19. // 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 })) }}

    View Slide

  20. https://www.kuechengoetter.de

    View Slide

  21. Caching Implementations
    Symfony HttpCache or Varnish?

    View Slide

  22. Symfony HttpCache
    – Easy to setup
    – Implemented in PHP
    – Not feature complete

    View Slide

  23. // src/CacheKernel.php
    namespace App;
    use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;
    class CacheKernel extends HttpCache { }
    // public/index.php
    use App\Kernel;
    use App\CacheKernel;
    // ...
    $kernel = new Kernel($env, $debug);
    $kernel = new CacheKernel($kernel);
    // ...

    View Slide

  24. // src/Controller/NameController.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;
    }
    }

    View Slide

  25. Varnish
    – Super performant
    – Feature complete
    – Configured using VCL
    – Implemented in C
    – Harder to setup

    View Slide

  26. 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

    View Slide

  27. // public/index.php
    // ...
    Request::setTrustedProxies(
    ['127.0.0.1'],
    Request::HEADER_X_FORWARDED_ALL
    );
    // ...

    View Slide

  28. // src/Controller/NameController.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;
    }
    }

    View Slide

  29. # 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";
    }

    View Slide

  30. # 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.
    }

    View Slide

  31. View Slide

  32. FOSHttpCacheBundle
    Simplifies cache handling and invalidation in Symfony applications.

    View Slide

  33. FOSHttpCacheBundle Features
    – Use rules to set cache headers
    – CacheManager / Annotations for comfortable Invalidation
    – Tagged Cache Invalidation
    – User Context based caching using session

    View Slide

  34. Audience Targeting
    Show and cache different content for different
    target groups on the same URL.

    View Slide

  35. 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

    View Slide

  36. View Slide

  37. View Slide

  38. View Slide

  39. Who knows the Vary header?

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. Symfony HttpCache
    Use extended version of the Symfony HttpCache
    from Sulu to support Audience Targeting.

    View Slide

  44. 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;
    }
    // ...

    View Slide

  45. // app/WebsiteCache.php
    use Sulu\Component\HttpCache\HttpCache;
    class WebsiteCache extends HttpCache
    {
    }

    View Slide

  46. // web/website.php
    // ...
    $kernel = new WebsiteKernel(SYMFONY_ENV, SYMFONY_DEBUG);
    if (SYMFONY_ENV !== 'dev') {
    $kernel = new WebsiteCache($kernel, true);
    Request::enableHttpMethodParameterOverride();
    }
    // ...

    View Slide

  47. Varnish
    VCL to the rescue!

    View Slide


  48. I really love the Varnish Configuration Language!
    Said no one, ever.

    View Slide

  49. View Slide

  50. # default.vcl
    vcl 4.0;
    import header;
    backend default {
    .host = ”127.0.0.1”;
    .port = "8000";
    }

    View Slide

  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;
    }

    View Slide

  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;
    }

    View Slide

  53. 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;
    }

    View Slide

  54. 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=/;");
    }
    }

    View Slide

  55. 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;
    }

    View Slide

  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=/;");
    }
    }

    View Slide

  57. 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=/;");
    }
    }

    View Slide

  58. Sulu Docs

    View Slide

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

    View Slide

  60. Thanks for watching!
    sulu.io

    View Slide