Developing cacheable PHP applications - PHP Barcelona 2019

Ca901ddcea38854b9783781c91fc87c9?s=47 Thijs Feryn
November 12, 2019

Developing cacheable PHP applications - PHP Barcelona 2019

See https://feryn.eu/speaking/developing-cacheable-php-applications/ for more information about this presentation.

Ca901ddcea38854b9783781c91fc87c9?s=128

Thijs Feryn

November 12, 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. 4,800,000 WEBSITES 19% OF THE TOP 10K WEBSITES

  19. None
  20. I'M @THIJSFERYN

  21. None
  22. USER SERVER

  23. REVERSE CACHING PROXY

  24. USER PROXY SERVER

  25. None
  26. None
  27. 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
  28. IN AN IDEAL WORLD

  29. ✓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
  30. REALITY SUCKS

  31. None
  32. FOR YOUR EYES ONLY NOT CACHED

  33. TIME TO LIVE

  34. CACHING PURGING LOGIC

  35. CACHE VARIATIONS

  36. LEGACY

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

    IN MIND?
  38. CACHE-CONTROL

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

  40. <?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(); } }
  41. Cache-Control: private, no-store

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

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

  44. ONLY FETCH PAYLOAD THAT HAS CHANGED

  45. HTTP/1.1 200 OK

  46. OTHERWISE: HTTP/1.1 304 NOT MODIFIED

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

    GET / HTTP/1.1 Host: localhost If-None-Match: 7c9d70604c6061da9bb9377d3f00eb27
  49. 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
  50. 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
  51. VARNISH SUPPORTS CONDITIONAL REQUESTS FOR CLIENTS & BACKENDS

  52. AND WILL SERVE STALE DATA WHILE IT ASYNCHRONOUSLY REVALIDATES CONTENT

  53. Cache-Control: public, max-age=100, s-maxage=500, stale-while-revalidate=20

  54. QUICKLY

  55. EARLY

  56. Store & retrieve Etag

  57. <?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
  58. { $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
  59. CONTENT COMPOSITION & PLACEHOLDERS

  60. None
  61. NO CACHE

  62. PLACEHOLDERS

  63. SEPARATE HTTP REQUEST

  64. AJAX

  65. ✓CLIENT-SIDE ✓COMMON KNOWLEDGE ✓PARALLEL PROCESSING ✓GRACEFUL DEGRADATION -PROCESSED BY THE

    BROWSER -EXTRA ROUNDTRIPS -SOMEWHAT SLOWER AJAX
  66. EDGE-SIDE INCLUDES ESI

  67. <esi:include src="/header" />

  68. ESI ✓ PLACEHOLDER ✓ PARSED BY VARNISH ✓ OUTPUT IS

    A COMPOSITION OF BLOCKS ✓ STATE PER BLOCK ✓ TTL PER BLOCK
  69. VARNISH Surrogate-Capability: key="ESI/1.0" Surrogate-Control: content="ESI/1.0" <esi:include src="/header" /> BACKEND Parse

    ESI placeholders VARNISH
  70. WRITE SOME VCL CODE FOR AUTOMATIC ESI DETECTION

  71. sub vcl_recv { set req.http.Surrogate-Capability = "key=ESI/1.0"; } sub vcl_backend_response

    { if (beresp.http.Surrogate-Control ~ "ESI/1.0") { unset beresp.http.Surrogate-Control; set beresp.do_esi = true; } }
  72. 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
  73. ESI VS AJAX

  74. COMPOSITION AT THE VIEW LAYER

  75. /** * @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
  76. <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>
  77. <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>
  78. CACHE VARIATIONS

  79. HOW DO YOU IDENTIFY AN OBJECT IN CACHE?

  80. THE URL IDENTIFIES THE OBJECT IN CACHE

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

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

  84. WHAT IF YOU CAN'T LEVERAGE HTTP?

  85. WRITE VCL CODE

  86. 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
  87. 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); } } } ONLY KEEP CERTAIN COOKIES
  88. NON-CACHEABLE ROUTES WILL STILL BE A TARGET FOR LOAD/LATENCY

  89. CACHING PERSONALIZED DATA

  90. DECISION MAKING AT THE EDGE

  91. SYNTHETIC HTTP HTTP OUTPUT THAT DIDN'T ORIGINATE FROM THE BACKEND

    SERVER
  92. {USERNAME: "THIJS"} GENERATED IN VCL BASED ON REDIS DATA

  93. vcl 4.1; import cookieplus; import redis; import synthbackend; sub vcl_init

    { new db = redis.db( location="redis:6379", type=master, connection_timeout=500, shared_connections=false, max_connections=1); } sub vcl_recv { set req.http.X-Login = "false"; set req.http.x-session = cookieplus.get("PHPSESSID","guest"); if(req.http.x-session != "guest") { db.command("EXISTS"); db.push("sf_s"+cookieplus.get("PHPSESSID")); db.execute(); if(db.get_integer_reply() == 1) { set req.http.X-Login = "true"; } } } SYNTHETIC HTTP
  94. sub vcl_backend_fetch { if(bereq.url == "/session") { if(bereq.http.X-Login != "true")

    { set bereq.backend = synthbackend.from_string("{}"); return(fetch); } db.command("EVAL"); db.push({" local session = redis.call('GET', KEYS[1]) if session == nil then return '{}' end local result = string.gsub(session, '[%c]', '') local username = string.gsub(result,'.+Userusername\";s:[0-9]+:\"([^\"]+)\";.+','%1') if username == nil then return '{}' end return '{"username":"'.. username ..'"}' "}); db.push(1); db.push("sf_s"+cookieplus.get("PHPSESSID")); db.execute(); set bereq.backend = synthbackend.from_string(db.get_string_reply()); } } sub vcl_backend_response { if(bereq.url == "/session") { set beresp.http.Content-Type = "application/json; charset=utf-8"; set beresp.ttl = 3600s; set beresp.http.vary = "x-session"; } } REDIS LUA CODE
  95. EDGESTASH VARNISH MODULE FOR MUSTACHE PROCESSING ON THE EDGE

  96. {{ USERNAME }} VARNISH MODULE FOR MUSTACHE PROCESSING ON THE

    EDGE REPLACES PLACEHOLDERS WITH JSON VALUES
  97. Surrogate-Control: edgestash="EDGESTASH/2.1" Link: </session>; rel=edgestash Welcome {{ username }} Welcome

    Thijs + =
  98. vcl 4.1; import edgestash; import std; backend default { .host

    = "1.1.1.1"; } sub vcl_recv { set req.http.Surrogate-Capability={"edgestash="EDGESTASH/2.1""}; } sub vcl_backend_response { if(beresp.http.Link) { std.collect(beresp.http.Link,","); } if(beresp.http.Link ~ "<([^>]+)>; rel=edgestash") { set beresp.http.x-edgestash-json-urls = regsuball(beresp.http.Link,"(?(?=<[^>]+>; rel=edgestash)<([^>]+)>; rel=edgestash|<([^>]+)>; rel=[a-z]+, )","\1"); } if(beresp.http.Surrogate-Control) { std.collect(beresp.http.Surrogate-Control); } if(beresp.http.Surrogate-Control ~ {".*="EDGESTASH/2\.[0-9]+".*"}) { edgestash.parse_response(); } } EDGESTASH
  99. sub vcl_deliver { if(edgestash.is_edgestash() && resp.http.x-edgestash-json-urls) { edgestash.add_json_url_csv(resp.http.x-edgestash-json-urls); edgestash.execute(); }

    unset resp.http.Link; unset resp.http.x-edgestash-json-urls; unset resp.http.surrogate-control; } EDGESTASH
  100. None
  101. COMPOSER REQUIRE THIJSFERYN/EDGESTASH-TWIG-BUNDLE

  102. <div>{{ edgestash('username','/session') }}</div> <div>{{ username | edgestash('username','/session') }}</div>

  103. None
  104. 14 DAYS FREE TRIAL

  105. None
  106. None
  107. HTTPS://FERYN.EU HTTPS://TWITTER.COM/THIJSFERYN HTTPS://INSTAGRAM.COM/THIJSFERYN