Developing cacheable PHP applications

Developing cacheable PHP applications

This talk was delivered at the Drupal NYC meetup on July 22nd.

See https://feryn.eu/speaking/developing-cacheable-php-applications-2/ for more details

Ca901ddcea38854b9783781c91fc87c9?s=128

Thijs Feryn

July 22, 2019
Tweet

Transcript

  1. DEVELOPING CACHEABLE PHP APPLICATIONS Thijs Feryn

  2. Slow websites SUCK

  3. WEB PERFORMANCE IS AN ESSENTIAL PART OF THE USER EXPERIENCE

  4. SLOW ~ DOWN

  5. None
  6. None
  7. None
  8. THROWING SERVERS AT THE PROBLEM

  9. MO' MONEY MO' SERVERS MO' PROBLEMS

  10. IDENTIFY SLOWEST PARTS

  11. OPTIMIZE

  12. AFTER A WHILE YOU HIT THE LIMITS

  13. CACHE

  14. HI, I'M THIJS

  15. I'M AN EVANGELIST AT

  16. None
  17. None
  18. I'M @THIJSFERYN

  19. None
  20. WE'RE HOSTING A MEETUP ✓ THURSDAY JULY 25TH ✓ 6PM

    - 9PM ✓ BARCADE, CHELSEA, NYC ✓ RSVP ! https://info.varnish-software.com/varnish-nyc-meetup-2019
  21. None
  22. DON’T RECOMPUTE IF THE DATA HASN’T CHANGED

  23. None
  24. None
  25. REVERSE CACHING PROXY

  26. NORMALLY USER SERVER

  27. WITH REVERSE CACHING PROXY USER PROXY SERVER

  28. None
  29. None
  30. None
  31. HTTP CACHING MECHANISMS Expires: Sat, 23 July 2019 00:30:00 GMT

    Cache-control: public, max-age=3600, s-maxage=86400 Cache-control: private, no-cache, no-store
  32. IN AN IDEAL WORLD

  33. ✓STATELESS ✓WELL-DEFINED TTL ✓CACHE / NO-CACHE PER RESOURCE ✓CACHE VARIATIONS

    ✓CONDITIONAL REQUESTS ✓PLACEHOLDERS FOR NON-CACHEABLE CONTENT ✓EDGE-SIDE LOGIC FOR PERSONALIZED CACHING IN AN IDEAL WORLD
  34. REALITY SUCKS

  35. None
  36. None
  37. TIME TO LIVE

  38. CACHE VARIATIONS

  39. LEGACY

  40. WHAT IF WE COULD DESIGN OUR SOFTWARE WITH HTTP CACHING

    IN MIND?
  41. CACHING STATE OF MIND ✓ PORTABILITY ✓ DEVELOPER EMPOWERMENT ✓

    CONTROL ✓ CONSISTENT CACHING BEHAVIOR
  42. None
  43. CACHE-CONTROL

  44. Cache-Control: public, s-maxage=500

  45. <?php namespace App\Controller; use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Symfony\Bundle\FrameworkBundle\Controller\Controller; class

    DefaultController extends Controller { /** * @Route("/", name="home") */ public function index() { return $this ->render('index.twig') ->setSharedMaxAge(500) ->setPublic(); } }
  46. Cache-Control: private, no-store

  47. /** * @Route("/private", name="private") */ public function private() { $response

    = $this ->render('private.twig') ->setPrivate(); $response->headers->addCacheControlDirective('no-store'); return $response; }
  48. CONDITIONAL REQUESTS

  49. ONLY FETCH PAYLOAD THAT HAS CHANGED

  50. HTTP/1.1 200 OK

  51. OTHERWISE: HTTP/1.1 304 NOT MODIFIED

  52. 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
  53. CONDITIONAL REQUESTS HTTP/1.1 304 Not Modified Host: localhost Etag: 7c9d70604c6061da9bb9377d3f00eb27

    GET / HTTP/1.1 Host: localhost If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  54. 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
  55. 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
  56. Cache-Control: public, max-age=100, s-maxage=500, stale-while-revalidate=20

  57. QUICKLY

  58. EARLY

  59. Store & retrieve Etag

  60. <?php namespace App\EventListener; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\GetResponseEvent; use

    Symfony\Component\HttpKernel\Event\FilterResponseEvent; use SymfonyBundles\RedisBundle\Redis\Client 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
  61. { $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
  62. CONTENT COMPOSITION & PLACEHOLDERS

  63. None
  64. Shopping cart or account information

  65. SESSION COOKIE NO CACHE

  66. CODE RENDERS SINGLE HTTP RESPONSE

  67. LOWEST COMMON DENOMINATOR: NO CACHE

  68. PLACEHOLDERS

  69. AJAX

  70. Non-cached AJAX call

  71. EDGE SIDE INCLUDES

  72. 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" />
  73. VARNISH Surrogate-Capability: key="ESI/1.0" Surrogate-Control: content="ESI/1.0" <esi:include src="/header" /> BACKEND Parse

    ESI placeholders VARNISH
  74. Non-cached ESI placeholder

  75. ESI VS AJAX

  76. 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
  77. AJAX ✓ CLIENT-SIDE ✓ COMMON KNOWLEDGE ✓ PARALLEL PROCESSING ✓

    GRACEFUL DEGRADATION - PROCESSED BY THE BROWSER - EXTRA ROUNDTRIPS - SOMEWHAT SLOWER
  78. COMPOSITION AT THE VIEW LAYER

  79. /** * @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
  80. <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>
  81. <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>
  82. CACHE VARIATIONS

  83. HOW DO YOU IDENTIFY AN OBJECT IN CACHE?

  84. THE URL IDENTIFIES THE OBJECT IN CACHE

  85. WHAT IF THE CONTENT OF A URL VARIES BASED ON

    THE VALUE OF A REQUEST HEADER?
  86. 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
  87. Vary: Accept-Language Request header value Response header

  88. WHAT IF YOU CAN'T LEVERAGE HTTP?

  89. WRITE VCL CODE

  90. 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
  91. 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
  92. None
  93. 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
  94. EDGE CONTENT TRANSFORMATION

  95. HTTP/1.1 200 OK Accept-Ranges: bytes Age: 11 Cache-Control: max-age=30, public,

    s-maxage=100 Content-Type: text/html; charset=UTF-8 Date: Thu, 11 Jul 2019 13:47:57 GMT X-Varnish: 262420 262415 Hello Thijs Hello {{username}} USER VARNISH ENTERPRISE ORIGIN GET /liked HTTP/1.1 Cookie:PHPSESSID=21779b3a7a75fca1851954b9810faa26 GET sf_s21779b3a7a75fca1851954b9810faa26
  96. HTTPS://AWS.AMAZON.COM/QUICKSTART/ARCHITECTURE/VARNISH/

  97. None
  98. None
  99. WE'RE HOSTING A MEETUP ✓ THURSDAY JULY 25TH ✓ 6PM

    - 9PM ✓ BARCADE, CHELSEA, NYC ✓ RSVP ! https://info.varnish-software.com/varnish-nyc-meetup-2019
  100. None
  101. HTTPS://FERYN.EU HTTPS://TWITTER.COM/THIJSFERYN HTTPS://INSTAGRAM.COM/THIJSFERYN