Caching the uncacheable with Varnish - PHP London 2020

Ca901ddcea38854b9783781c91fc87c9?s=47 Thijs Feryn
February 06, 2020

Caching the uncacheable with Varnish - PHP London 2020

This talk was presented at the February 2020 edition of the PHP London meetup group.

This presentation is about caching personalized data in Varnish, while still maintaining a good hit rate.

See https://feryn.eu/speaking/caching-uncacheable-varnish-php-london-meetup-2020/ for more info.

Ca901ddcea38854b9783781c91fc87c9?s=128

Thijs Feryn

February 06, 2020
Tweet

Transcript

  1. CACHING THE UNCACHEABLE BY THIJS FERYN WITH VARNISH

  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. None
  23. NORMALLY USER SERVER

  24. WITH REVERSE CACHING PROXY USER PROXY SERVER

  25. None
  26. None
  27. CONTROLLING THE CACHE WITH HTTP 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 Vary: Accept-Language
  28. STATE

  29. STATE ~ USER SPECIFIC DATA COOKIES AUTH HEADERS

  30. FOR YOUR EYES ONLY NOT CACHED

  31. VARNISH CONFIGURATION LANGUAGE

  32. vcl 4.1; 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
  33. 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, ";(language)=", "; \1="); set req.http.Cookie = regsuball(req.http.Cookie, ";[^ ][^;]*", ""); set req.http.Cookie = regsuball(req.http.Cookie, "^[; ]+|[; ]+$", ""); if (req.http.cookie ~ "^\s*$") { unset req.http.cookie; return(pass); } return(hash); } } sub vcl_hash { hash_data(regsub( req.http.Cookie, "^.*language=([^;]*);*.*$", "\1" )); } Cookie cache variation
  34. vcl 4.1; import cookieplus; sub vcl_recv { cookieplus.keep("language"); cookieplus.write(); }

    sub vcl_hash { hash_data(cookieplus.get("language")); } VMOD_COOKIE
  35. CONTAINS USER INFORMATION

  36. NO CACHE

  37. PLACEHOLDERS

  38. SEPARATE HTTP REQUEST

  39. AJAX

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

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

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

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

    A COMPOSITION OF BLOCKS ✓ STATE PER BLOCK ✓ TTL PER BLOCK
  44. 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; } }
  45. ✓SERVER-SIDE ✓STANDARDIZED ✓PROCESSED ON THE “EDGE”, NOT IN THE BROWSER

    ✓GENERALLY FASTER -SEQUENTIAL* -ONE FAILS, ALL FAIL -LIMITED IMPLEMENTATION IN VARNISH -NOT THAT COMMON ESI PARALLEL IN ENTERPRISE
  46. NON-CACHEABLE ROUTES WILL STILL BE A TARGET FOR LOAD/LATENCY

  47. NOT JUST A CACHE NOT JUST "TAKE IT OR LEAVE

    IT"
  48. AN HTTP LOGIC BOX

  49. DECISION MAKING AT THE EDGE

  50. 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
  51. 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
  52. USER GET /session Cookie: PHPSESSID=7er3hjKal8u235c87u6ih0vz8Y HTTP/1.1 200 OK Content-Length: 31

    Content-Type: application/json; charset=utf-8 X-Varnish: 163854 196622 Age: 27 {"username":"thijs"} VARNISH
  53. WHAT DO YOU DO WITH IT? ✓ AJAX CALL ✓

    LOAD VIA ESI AND PROCESS VIA LOCAL JAVASCRIPT ✓ EDGESTASH
  54. {{ EDGESTASH }} VARNISH MODULE FOR MUSTACHE PROCESSING ON THE

    EDGE
  55. vcl 4.1; import edgestash; backend default { .host = "1.1.1.1";

    .port = "80"; } sub vcl_backend_response { if (bereq.url == "/session") { edgestash.index_json(); } else if (bereq.url == "/") { edgestash.parse_response(); } } sub vcl_deliver { if (req.url == "/" && edgestash.is_edgestash()) { edgestash.add_json_url("/session"); edgestash.execute(); } } EDGESTASH
  56. EDGESTASH {"username":"Thijs"} <div>{{ username }}</div> <div>Thijs</div>

  57. MORE FLEXIBILITY

  58. Surrogate-Control: edgestash="EDGESTASH/2.1" Link: </session>; rel=edgestash

  59. 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
  60. 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
  61. None
  62. COMPOSER REQUIRE THIJSFERYN/EDGESTASH-TWIG-BUNDLE

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

  64. <div> {% if isEdgestash() %} //Edgestash-supported logic {% else %}

    //Regular Twig logic {% endif %} </div>
  65. {{ edgestash('#username', '/session') }} Welcome {{ edgestash('username') }} {{ edgestash('/username')

    }} {{ edgestash('^username') }} Welcome guest {{ edgestash('/username') }
  66. <ul> {{ edgestash('#.', '/users') }} <li>{{ edgestash('username') }}</li> {{ edgestash('/.')

    }} </ul>
  67. None
  68. FROM $0.20/H + 14 DAYS FREE TRIAL

  69. MORE EDGE DECISION MAKING

  70. import deviceatlas; sub vcl_recv { set req.http.MobilePhone = "no"; if

    (deviceatlas.lookup_int(req.http.User-Agent, "isMobilePhone")) { set req.http.MobilePhone = "yes"; } } Device detection
  71. vcl 4.0; import mmdb; backend default { .host = "192.0.2.11";

    .port = "8080"; } # create a database object sub vcl_init { new geodb = mmdb.init("/path/to/db"); } sub vcl_recv { # retrieve the name of the request's origin set req.http.Country-Name = geodb.country_name(client.ip); # if the country doesn't come from Germany or Belgium, deny access if (req.http.Country-Name != "Germany" || req.http.Country-Name != "Belgium") { return (synth(403, "Sorry, only available in Germany and Belgium")); } } Geo blocking
  72. vcl 4.0; import http; backend default { .host = "origin";

    .port = "80"; } sub vcl_recv { set req.http.X-prefetch = http.varnish_url("/"); } sub vcl_backend_response { if (beresp.http.Link ~ "<.+>.*(prefetch|next)") { set bereq.http.X-link = regsub(beresp.http.Link, "^.*<([^>]*)>.*$", "\1"); set bereq.http.X-prefetch = regsub(bereq.http.X-prefetch, "/$", bereq.http.X-link); http.init(0); http.req_copy_headers(0); http.req_set_url(0, bereq.http.X-prefetch); http.req_send_and_finish(0); } } Pre-fetching
  73. None
  74. None
  75. HTTPS://FERYN.EU HTTPS://TWITTER.COM/THIJSFERYN HTTPS://INSTAGRAM.COM/THIJSFERYN