$30 off During Our Annual Pro Sale. View Details »

Caching the uncacheable with Varnish - PHP UG FFM 19

Thijs Feryn
November 28, 2019

Caching the uncacheable with Varnish - PHP UG FFM 19

This talk is about caching personalized data using Varnish. It was presented at the PHP UG FFM in Frankfurt.

See https://feryn.eu/speaking/caching-uncacheable-varnish-phpugffm19/ for more information

Thijs Feryn

November 28, 2019
Tweet

More Decks by Thijs Feryn

Other Decks in Technology

Transcript

  1. CACHING THE UNCACHEABLE
    BY THIJS FERYN
    WITH VARNISH

    View Slide

  2. Slow websites
    SUCK

    View Slide

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

    View Slide

  4. SLOW ~ DOWN

    View Slide

  5. View Slide

  6. View Slide

  7. View Slide

  8. THROWING
    SERVERS
    AT THE PROBLEM

    View Slide

  9. MO' MONEY
    MO' SERVERS
    MO' PROBLEMS

    View Slide

  10. IDENTIFY SLOWEST PARTS

    View Slide

  11. OPTIMIZE

    View Slide

  12. AFTER A WHILE YOU HIT THE LIMITS

    View Slide

  13. CACHE

    View Slide

  14. HI, I'M THIJS

    View Slide

  15. I'M AN
    EVANGELIST
    AT

    View Slide

  16. View Slide

  17. View Slide

  18. 4,800,000 WEBSITES
    19% OF THE TOP 10K WEBSITES

    View Slide

  19. View Slide

  20. I'M @THIJSFERYN

    View Slide

  21. View Slide

  22. View Slide

  23. NORMALLY
    USER SERVER

    View Slide

  24. WITH REVERSE CACHING PROXY
    USER PROXY SERVER

    View Slide

  25. View Slide

  26. View Slide

  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

    View Slide

  28. STATE

    View Slide

  29. STATE
    ~
    USER SPECIFIC DATA
    COOKIES
    AUTH
    HEADERS

    View Slide

  30. FOR YOUR EYES ONLY
    NOT CACHED

    View Slide

  31. VARNISH CONFIGURATION LANGUAGE

    View Slide

  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

    View Slide

  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

    View Slide

  34. vcl 4.1;
    import cookieplus;
    sub vcl_recv {
    cookieplus.keep("language");
    cookieplus.write();
    }
    sub vcl_hash {
    hash_data(cookieplus.get("language"));
    }
    VMOD_COOKIE

    View Slide

  35. CONTAINS
    USER
    INFORMATION

    View Slide

  36. NO CACHE

    View Slide

  37. PLACEHOLDERS

    View Slide

  38. SEPARATE
    HTTP REQUEST

    View Slide

  39. AJAX

    View Slide

  40. ✓CLIENT-SIDE
    ✓COMMON KNOWLEDGE
    ✓PARALLEL PROCESSING
    ✓GRACEFUL
    DEGRADATION
    -PROCESSED BY THE
    BROWSER
    -EXTRA ROUNDTRIPS
    -SOMEWHAT SLOWER
    AJAX

    View Slide

  41. EDGE-SIDE INCLUDES ESI

    View Slide


  42. View Slide

  43. ESI
    ✓ PLACEHOLDER
    ✓ PARSED BY VARNISH
    ✓ OUTPUT IS A COMPOSITION OF BLOCKS
    ✓ STATE PER BLOCK
    ✓ TTL PER BLOCK

    View Slide

  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;
    }
    }

    View Slide

  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

    View Slide

  46. NON-CACHEABLE ROUTES WILL STILL BE A
    TARGET FOR LOAD/LATENCY

    View Slide

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

    View Slide

  48. AN HTTP
    LOGIC BOX

    View Slide

  49. DECISION MAKING AT THE EDGE

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  53. WHAT DO YOU DO WITH IT?
    ✓ AJAX CALL
    ✓ LOAD VIA ESI AND PROCESS VIA LOCAL JAVASCRIPT
    ✓ EDGESTASH

    View Slide

  54. {{ EDGESTASH }}
    VARNISH
    MODULE FOR MUSTACHE
    PROCESSING ON THE
    EDGE

    View Slide

  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

    View Slide

  56. EDGESTASH
    {"username":"Thijs"}
    {{ username }}
    Thijs

    View Slide

  57. MORE FLEXIBILITY

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  61. View Slide

  62. COMPOSER REQUIRE THIJSFERYN/EDGESTASH-TWIG-BUNDLE

    View Slide

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

    View Slide


  64. {% if isEdgestash() %}
    //Edgestash-supported logic
    {% else %}
    //Regular Twig logic
    {% endif %}

    View Slide

  65. {{ edgestash('#username', '/session') }}
    Welcome {{ edgestash('username') }}
    {{ edgestash('/username') }}
    {{ edgestash('^username') }}
    Welcome guest
    {{ edgestash('/username') }

    View Slide


  66. {{ edgestash('#.', '/users') }}
    {{ edgestash('username') }}
    {{ edgestash('/.') }}

    View Slide

  67. View Slide

  68. FROM $0.20/H + 14 DAYS FREE TRIAL

    View Slide

  69. MORE EDGE DECISION MAKING

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  73. View Slide

  74. View Slide

  75. HTTPS://FERYN.EU
    HTTPS://TWITTER.COM/THIJSFERYN
    HTTPS://INSTAGRAM.COM/THIJSFERYN

    View Slide