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. 5.
  2. 6.
  3. 7.
  4. 11.
  5. 13.
  6. 16.
  7. 17.
  8. 19.
  9. 21.
  10. 22.
  11. 25.
  12. 26.
  13. 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
  14. 28.
  15. 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
  16. 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
  17. 34.

    vcl 4.1; import cookieplus; sub vcl_recv { cookieplus.keep("language"); cookieplus.write(); }

    sub vcl_hash { hash_data(cookieplus.get("language")); } VMOD_COOKIE
  18. 36.
  19. 39.
  20. 43.

    ESI ✓ PLACEHOLDER ✓ PARSED BY VARNISH ✓ OUTPUT IS

    A COMPOSITION OF BLOCKS ✓ STATE PER BLOCK ✓ TTL PER BLOCK
  21. 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; } }
  22. 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
  23. 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
  24. 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
  25. 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
  26. 53.

    WHAT DO YOU DO WITH IT? ✓ AJAX CALL ✓

    LOAD VIA ESI AND PROCESS VIA LOCAL JAVASCRIPT ✓ EDGESTASH
  27. 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
  28. 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
  29. 60.
  30. 61.
  31. 64.
  32. 65.

    {{ edgestash('#username', '/session') }} Welcome {{ edgestash('username') }} {{ edgestash('/username')

    }} {{ edgestash('^username') }} Welcome guest {{ edgestash('/username') }
  33. 67.
  34. 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
  35. 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
  36. 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
  37. 73.
  38. 74.