Successfully reported this slideshow.
Your SlideShare is downloading. ×

Developing cacheable backend applications - Appdevcon 2019

Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad
Ad

Check these out next

1 of 112 Ad

Developing cacheable backend applications - Appdevcon 2019

Download to read offline

This talk explains how you can leverage the power of HTTP to deliver cacheable backend applications.

By using Cache-control headers, you can influence the caching behavior. We'll also talk about cache variations, conditional requests using etags. We'll even cover placeholdering using Edge Side Includes and AJAX.

See https://feryn.eu/speaking/developing-cacheable-backend-applications/ for more information

This talk explains how you can leverage the power of HTTP to deliver cacheable backend applications.

By using Cache-control headers, you can influence the caching behavior. We'll also talk about cache variations, conditional requests using etags. We'll even cover placeholdering using Edge Side Includes and AJAX.

See https://feryn.eu/speaking/developing-cacheable-backend-applications/ for more information

Advertisement
Advertisement

More Related Content

Slideshows for you (20)

Similar to Developing cacheable backend applications - Appdevcon 2019 (20)

Advertisement

Recently uploaded (20)

Advertisement

Developing cacheable backend applications - Appdevcon 2019

  1. 1. DEVELOPING CACHEABLE BACKEND APPLICATIONS Thijs Feryn
  2. 2. Slow websites SUCK
  3. 3. WEB PERFORMANCE IS AN ESSENTIAL PART OF THE USER EXPERIENCE
  4. 4. SLOW~DOWN
  5. 5. THROWING SERVERS ATTHEPROBLEM
  6. 6. MO' MONEY MO' SERVERS MO' PROBLEMS
  7. 7. IDENTIFY SLOWEST PARTS
  8. 8. OPTIMIZE
  9. 9. AFTER A WHILE YOU HIT THE LIMITS
  10. 10. CACHE
  11. 11. HI, I'M THIJS
  12. 12. I'M AN EVANGELIST AT
  13. 13. I'M @THIJSFERYN
  14. 14. DON’T RECOMPUTE IF THE DATA HASN’T CHANGED
  15. 15. REVERSE CACHING PROXY
  16. 16. NORMALLY USER SERVER
  17. 17. WITH REVERSE CACHING PROXY USER PROXY SERVER
  18. 18. HTTP CACHING MECHANISMS Expires: Sat, 09 Sep 2017 14:30:00 GMT Cache-control: public, max-age=3600, s-maxage=86400 Cache-control: private, no-cache, no-store
  19. 19. IN AN IDEAL WORLD
  20. 20. ✓STATELESS ✓WELL-DEFINED TTL ✓CACHE / NO-CACHE PER RESOURCE ✓CACHE VARIATIONS ✓CONDITIONAL REQUESTS ✓PLACEHOLDERS FOR NON-CACHEABLE CONTENT IN AN IDEAL WORLD
  21. 21. REALITY SUCKS
  22. 22. TIME TO LIVE
  23. 23. CACHE VARIATIONS
  24. 24. LEGACY
  25. 25. WHAT IF WE COULD DESIGN OUR SOFTWARE WITH HTTP CACHING IN MIND?
  26. 26. CACHING STATE OF MIND ✓ PORTABILITY ✓ DEVELOPER EMPOWERMENT ✓ CONTROL ✓ CONSISTENT CACHING BEHAVIOR
  27. 27. CACHE-CONTROL
  28. 28. Cache-Control: public, s-maxage=500
  29. 29. <?php namespace AppController; use SymfonyComponentHttpFoundationRequest; use SensioBundleFrameworkExtraBundleConfigurationRoute; use SymfonyBundleFrameworkBundleControllerController; class DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setSharedMaxAge(500) ->setPublic(); } }
  30. 30. Cache-Control: private, no-store
  31. 31. /** * @Route("/private", name="private") */ public function private() { $response = $this ->render('private.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; }
  32. 32. CONDITIONAL REQUESTS
  33. 33. ONLY FETCH PAYLOAD THAT HAS CHANGED
  34. 34. HTTP/1.1 200 OK
  35. 35. OTHERWISE: HTTP/1.1 304 NOT MODIFIED
  36. 36. 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
  37. 37. CONDITIONAL REQUESTS HTTP/1.1 304 Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27 GET / HTTP/1.1 Host: localhost If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  38. 38. 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
  39. 39. 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-Last-Modified: Fri, 22 Jul 2016 10:11:16 GMT
  40. 40. Cache-Control: public, max-age=100, s-maxage=500, stale-while-revalidate=20
  41. 41. QUICKLY
  42. 42. EARLY
  43. 43. Store & retrieve Etag
  44. 44. <?php namespace AppEventListener; use SymfonyComponentHttpFoundationResponse; use SymfonyComponentHttpFoundationRequest; use SymfonyComponentHttpKernelEventGetResponseEvent; use SymfonyComponentHttpKernelEventFilterResponseEvent; use SymfonyBundlesRedisBundleRedisClient as RedisClient; class ConditionalRequestListener { protected $redis; public function __construct(RedisClient $redis) { $this->redis = $redis; } protected function isModified(Request $request, $etag) { if ($etags = $request->getETags()) { return in_array($etag, $etags) || in_array('*', $etags); } return true; } ... src/EventListener/ConditionalRequestListener.php
  45. 45. { $this->redis = $redis; } protected function isModified(Request $request, $etag) { if ($etags = $request->getETags()) { return in_array($etag, $etags) || in_array('*', $etags); } return true; } public function onKernelRequest(GetResponseEvent $event) { $request = $event->getRequest(); $etag = $this->redis->get('etag:'.md5($request->getUri())); if(!$this->isModified($request,$etag)) { $event->setResponse(Response::create('Not Modified',Response::HTTP_NOT_MODIFIED)); } } public function onKernelResponse(FilterResponseEvent $event) { $response = $event->getResponse(); $request = $event->getRequest(); $etag = md5($response->getContent()); $response->setEtag($etag); if($this->isModified($request,$etag)) { $this->redis->set('etag:'.md5($request->getUri()),$etag); } } } src/EventListener/ConditionalRequestListener.php
  46. 46. CONTENT COMPOSITION & PLACEHOLDERS
  47. 47. Shopping cart or account information
  48. 48. SESSION COOKIE NO CACHE
  49. 49. CODE RENDERS SINGLE HTTP RESPONSE
  50. 50. LOWEST COMMON DENOMINATOR: NO CACHE
  51. 51. PLACEHOLDERS
  52. 52. AJAX
  53. 53. Non-cached AJAX call
  54. 54. EDGE SIDE INCLUDES
  55. 55. EDGE SIDE INCLUDES ✓ PLACEHOLDER ✓ W3C STANDARD ✓ PROCESSED "ON THE EDGE" (E.G. VARNISH) ✓ OUTPUT IS A COMPOSITION OF BLOCKS ✓ STATE PER BLOCK ✓ TTL PER BLOCK <esi:include src="/header" />
  56. 56. VARNISH Surrogate-Capability: key="ESI/1.0" Surrogate-Control: content="ESI/1.0" <esi:include src="/header" /> BACKEND Parse ESI placeholdersVARNISH
  57. 57. Non-cached ESI placeholder
  58. 58. ESI VS AJAX
  59. 59. EDGE SIDE INCLUDES ✓ SERVER-SIDE ✓ STANDARDIZED ✓ PROCESSED ON THE “EDGE”, NOT IN THE BROWSER ✓ GENERALLY FASTER - SEQUENTIAL 
 (ONLY PARALLEL IN ENTERPRISE VERSION) - ONE FAILS, ALL FAIL - LIMITED IMPLEMENTATION IN VARNISH
  60. 60. AJAX ✓ CLIENT-SIDE ✓ COMMON KNOWLEDGE ✓ PARALLEL PROCESSING ✓ GRACEFUL DEGRADATION - PROCESSED BY THE BROWSER - EXTRA ROUNDTRIPS - SOMEWHAT SLOWER
  61. 61. COMPOSITION AT THE VIEW LAYER
  62. 62. /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setPublic() ->setSharedMaxAge(500); } /** * @Route("/header", name="header") */ public function header() { $response = $this ->render('header.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; } /** * @Route("/footer", name="footer") */ public function footer() { $response = $this->render('footer.twig'); $response ->setSharedMaxAge(500) ->setPublic(); return $response; } /** * @Route("/nav", name="nav") */ public function nav() { $response = $this->render('nav.twig'); $response ->setVary('X-Login',false) ->setSharedMaxAge(500) ->setPublic(); return $response; } Controller action per fragment
  63. 63. <div class="container-fluid">
 {{ include('header.twig') }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ include('nav.twig') }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ include('footer.twig') }}
 </div> <div class="container-fluid">
 {{ render_esi(url('header')) }}
 <div class="row">
 <div class="col-sm-3 col-lg-2">
 {{ render_esi(url('nav')) }}
 </div>
 <div class="col-sm-9 col-lg-10">
 {% block content %}{% endblock %}
 </div>
 </div>
 {{ render_esi(url('footer')) }}
 </div>
  64. 64. <div class="container-fluid"> <esi:include src="/header" /> <div class="row"> <div class="col-sm-3 col-lg-2"> <esi:include src="/nav" /> </div> <div class="col-sm-9 col-lg-10"> <div class="page-header"> <h1>An example page <small>Rendered at 2017-05-17 16:57:14</small></h1> </div> <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris consequat orci eget libero sollicitudin,…</p> </div> </div> <esi:include src="/footer" /> </div>
  65. 65. CACHE VARIATIONS
  66. 66. HOW DO YOU IDENTIFY AN OBJECT IN CACHE?
  67. 67. THE URL IDENTIFIES THE OBJECT IN CACHE
  68. 68. WHAT IF THE CONTENT OF A URL VARIES BASED ON THE VALUE OF A REQUEST HEADER?
  69. 69. CACHE VARIATIONS HTTP/1.1 200 OK Host: localhost Content-Language: en Content-type: text/html; charset=UTF-8 Hello world output GET / HTTP/1.1 Host: localhost Accept-Language: en, nl, de
  70. 70. Vary: Accept-Language Request header value Response header
  71. 71. CONTENT INVALIDATION
  72. 72. THERE'S ONLY ONE THING WORSE THAN NOT CACHING ENOUGH
  73. 73. IT'S CACHING TOO MUCH OR TOO LONG
  74. 74. PURGING
  75. 75. sub vcl_recv { if (req.method == "PURGE") { if (!client.ip ~ purge) { return (synth(405, "This IP is not allowed to send PURGE.")); } if (req.http.X-Purge-Pattern) { ban("obj.http.X-Req-URL ~ " + req.url + " && obj.http.X-Req-Host == " + req.http.host); return (synth(200, "Purged")); } else { ban("obj.http.x-url == " + req.url + " && obj.http.x-host == " + req.http.host); return (synth(200, "Purged")); } } } sub vcl_backend_response { set beresp.http.x-url = bereq.url; set beresp.http.x-host = bereq.http.host; }
  76. 76. curl -XPURGE -H"X-Purge-Pattern:/products/(.*)" http://localhost
  77. 77. vcl 4.0; import xkey; backend default { .host = "192.0.2.11"; .port = "8080"; } acl purgers { "203.0.113.0"/24; } sub vcl_recv { if (req.method == "PURGE") { if (client.ip !~ purgers) { return (synth(403, "Forbidden")); } set req.http.n-gone = xkey.purge(req.http.xkey-purge); return (synth(200, "Invalidated "+req.http.n-gone+" objects")); } }
  78. 78. HTTP/1.1 OK xkey: 8155054 xkey: 166412 xkey: 234323 BACKEND RESPONSE PURGE / HTTP/1.1 Host: www.example.com xkey-purge: 166412 INVALIDATION REQUEST
  79. 79. HIGH TTL + PURGING
  80. 80. LOW TTL + CONDITIONAL REQUESTS
  81. 81. WHAT IF YOU CAN'T LEVERAGE HTTP?
  82. 82. WRITE VCL CODE
  83. 83. sub vcl_recv { if (req.url ~ "^/status.php$" || req.url ~ "^/update.php$" || req.url ~ "^/admin$" || req.url ~ "^/admin/.*$" || req.url ~ "^/flag/.*$" || req.url ~ "^.*/ajax/.*$" || req.url ~ "^.*/ahah/.*$") { return (pass); } } URL blacklist example
  84. 84. vcl 4.0; sub vcl_recv { if (req.http.Cookie) { set req.http.Cookie = ";" + req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(SESS[a-z0-9]+|SSESS[a-z0-9]+|NO_CACHE)=", "; 1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); if (req.http.Cookie == "") { unset req.http.Cookie; } else { return (pass); } } } Cookie example
  85. 85. WHAT ABOUT API AUTHENTICATION?
  86. 86. AUTHENTICATION VALIDATED AND TERMINATED AT THE PROXY LEVEL CLIENT VARNISH BACKEND
  87. 87. VARNISH MODULE HTTPS://VARNISH-CACHE.ORG/VMODS/
  88. 88. BASIC AUTH
  89. 89. JWT Digest VMOD JSON VMOD
  90. 90. WHAT ABOUT STATIC RESOURCES?
  91. 91. WHAT ABOUT VIDEO?
  92. 92. VARNISH ENTERPRISE ✓ CLIENT SSL TERMINATION ✓ BACKEND SSL CONNECTIONS ✓ PARALLEL ESI ✓ MASSIVE STORAGE ENGINE ✓ ENCRYPTION ✓ THROTTLING ✓ RATE LIMITING ✓ PREFETCHING ✓ GEOLOCATION ✓ AUTHENTICATION ✓ EDGESTASH ✓ CUSTOM STATISTICS ✓ ADMIN MODULE ✓ SUPPORT ✓ HIGH AVAILABILITY
  93. 93. HTTPS://AWS.AMAZON.COM/QUICKSTART/ARCHITECTURE/VARNISH/
  94. 94. HTTPS://FERYN.EU HTTPS://TWITTER.COM/THIJSFERYN HTTPS://INSTAGRAM.COM/THIJSFERYN

×