Friday, January 14, 2011

Varnish configuration to only cache for non-logged in users

I have a Ruby on Rails application fronted by varnish+nginx. As most of the sites content is static unless you are a logged in user, I want to cache the site heavily with varnish when a user is logged out but only to cache static assets when they are logged in.

When a user is logged in they will have the cookie 'user_credentials' present in their Cookie: header, in addition I need to skip caching on /login and /sessions in order that a user can get their 'user_credentials' cookie in the first place.

Rails by default does not set a cache friendly Cache-control header, but my application sets a "public,s-max-age=60" header when a user is not logged in. Nginx is set to return 'far future' expires headers for all static assets.

The configuration I have at the moment is totally bypassing the cache for everything when logged in, including static assets — and is returning cache MISS for everything when logged out. I've spent hours going around in circles and here is my current default.vcl

    director rails_director round-robin {
  { 
    .backend = { 
      .host = "xxx.xxx.xxx.xxx"; 
      .port = "http";
      .probe = {
        .url = "/lbcheck/lbuptest";
        .timeout = 0.3 s;
        .window = 8;
        .threshold = 3;
      }
    } 
  }
}

sub vcl_recv {

  if (req.url ~ "^/login") {
    pipe;
  }

  if (req.url ~ "^/sessions") {
    pipe;
  }
  # The regex used here matches the standard rails cache buster urls
  # e.g. /images/an-image.png?1234567
  if (req.url ~ "\.(css|js|jpg|jpeg|gif|ico|png)\??\d*$") {
    unset req.http.cookie;
    lookup;
  } else {
    if (req.http.cookie ~ "user_credentials") {
      pipe;
    }
  }

  # Only cache GET and HEAD requests
  if (req.request != "GET" && req.request != "HEAD") {
    pipe;
  }

}

sub vcl_fetch {

  if (req.url ~ "^/login") {
    pass;
  }

  if (req.url ~ "^/sessions") {
    pass;
  }

  if (req.http.cookie ~ "user_credentials") {
    pass;
  } else {
    unset req.http.Set-Cookie;
  }

  # cache CSS and JS files
  if (req.url ~ "\.(css|js|jpg|jpeg|gif|ico|png)\??\d*$") {
    unset req.http.Set-Cookie;
  } 

  if (obj.status >=400 && obj.status <500) {
    error 404 "File not found";
  }

  if (obj.status >=500 && obj.status <600) {
    error 503 "File is Temporarily Unavailable";
  }

}

sub vcl_deliver {
  if (obj.hits > 0) {
          set resp.http.X-Cache = "HIT";
  } else {
          set resp.http.X-Cache = "MISS";
  }
}
  • Ok, in the end I managed to solve this using the following vcl file. Note that I added a couple of extra bits to allow cache expiration grace when the backend has died.

    It seems my main failure was using unset req.http.Set-Cookie; when I should have been using unset obj.http.Set-Cookie; in the vcl_fetch section. (obj in vcl_fetch and req in vcl_recv section).

    director rails_director round-robin {
      { 
        .backend = { 
          .host = "xxx.xxx.xxx.xxx"; 
          .port = "http";
          .probe = {
            .url = "/lbcheck/lbuptest";
            .timeout = 0.3 s;
            .window = 8;
            .threshold = 3;
          }
        } 
      }
    }
    
    sub vcl_recv {
    
      if (req.backend.healthy) {
        set req.grace = 30s;
      } else {
        set req.grace = 1h;
      }
    
      if (req.url ~ "^/login") {
        pipe;
      }
    
      if (req.url ~ "^/sessions") {
        pipe;
      }
    
      if (req.url ~ "\.(css|js|jpg|jpeg|gif|ico|png)\??\d*$") {
        unset req.http.cookie;
        lookup;
      } else {
        if (req.http.cookie ~ "user_credentials") {
          pipe;
        } else {
          unset req.http.cookie;
        }
      }
    
      # Only cache GET and HEAD requests
      if (req.request != "GET" && req.request != "HEAD") {
        pipe;
      }
    
    }
    
    sub vcl_fetch {
    
      set obj.grace = 1h;
    
      if (req.url ~ "^/login") {
        pass;
      }
    
      if (req.url ~ "^/sessions") {
        pass;
      }
    
      if (req.http.cookie ~ "user_credentials") {
        pass;
      } else {
        unset obj.http.Set-Cookie;
      }
    
      # cache CSS and JS files
      if (req.url ~ "\.(css|js|jpg|jpeg|gif|ico|png)\??\d*$") {
        unset obj.http.Set-Cookie;
      } 
    
      if (obj.status >=400 && obj.status <500) {
        error 404 "File not found";
      }
    
      if (obj.status >=500 && obj.status <600) {
        error 503 "File is Temporarily Unavailable";
      }
    
    }
    
    sub vcl_deliver {
      if (obj.hits > 0) {
              set resp.http.X-Cache = "HIT";
      } else {
              set resp.http.X-Cache = "MISS";
      }
    }
    
  • I can't comment so I'm posting this as an answer.

    Attention: As of 2.1.0, obj.* is called beresp.* in vcl_fetch, and obj.* is now read-only.

    These also seem better placed in vcl_recv instead of vcl_fetch:

      if (req.url ~ "^/login") {
        pipe;
      }
    
      if (req.url ~ "^/sessions") {
        pipe;
      }
    

    Lastly, in you backend definition, I would add

    .max_connections = 32;
    

    Adjust to the number of passenger backends you have allowed nginx/apache to create. If you don't set this limit you should keep an eye on you passenger global queue (assuming you're using passenger).

    From XCondE

0 comments:

Post a Comment