Varnish Web Accelerator
Getting going - simplest possible setup
part1_basic.cfm
<CFHEADER NAME="Cache-Control" VALUE="s-maxage=600"> <cfoutput> <html> <body> <strong>Generated by server:</strong> #dateformat(now(),'ddd, mmm d yyyy')# #timeformat(now(),'HH:mm:ss')#<br> <strong>Loaded by browser:</strong> <script type="text/javascript">document.write(new Date());</script> </body> </html> </cfoutput>
- The default varnish installation serves cached content from port 6081 proxied from 127.0.0.1:80.
- that is important - the default varnish config is appended to yours
- Cookies (i.e. sessions) should be disabled, as by default varnish will invalidate caches if cookies are present.
- Setting browser cache separate to varnish cache - you can control varnish cache but not client
Static assets
/etc/varnish/default.vcl
sub vcl_recv { if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset req.http.Accept-Encoding; // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } unset req.http.Cookie; return(lookup); } }
virtual.conf
<VirtualHost *:80> ... # Set up caching on media files for 1 year (forever?) <FilesMatch "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$"> ExpiresDefault A29030400 Header append Cache-Control "public" </FilesMatch> </VirtualHost>
Tips
- Leave the default backend (127.0.0.1) in place.
- ~ is a regex operator for strings
- set and unset update and remove a property
- req.http properties are request headers
- this configuration leaves control of cache timeouts with Apache, but you can force a cache timeout in varnish with a line like "set beresp.ttl = 48h;"
- return(lookup) returns control to varnish, requesting a cache lookup if possible
- if there is not return in your config, the default varnish config is executed
Flush Varnish cache from backend
/etc/varnish/default.vcl
// IPs/domains that can access the purge url acl purge { "localhost"; "117.53.174.178"; "117.53.174.179"; "203.26.11.39"; } sub vcl_recv { // Purge everything url - this isn't the squid way, but works if (req.url ~ "^/varnishpurge") { if (!client.ip ~ purge) { error 405 "Not allowed."; } if (req.url == "/varnishpurge") { ban("req.http.host == " + req.http.host + " && req.url ~ ^/"); error 841 "Purged site."; } else { ban("req.http.host == " + req.http.host + " && req.url ~ ^" + regsub( req.url, "^/varnishpurge(.*)$", "\1" ) + "$" ); error 842 "Purged page."; } } }
- it's a good idea to use an ACL to restrict access to the sensitive functionality like flushing
- notice that for ACLs, the ~ operator is an 'in' check
- this acl sets up a URL that will trigger a flush '/varnishpurge'
- /varnishpurge by itself purges every page on the domain
- /varnishpurge/url/you/want/to/purge purges /url/you/want/to/purge
- 'ban' is for varnish 3 what 'purge' was for 2
- in 3 you can do "string" + myvar, in 2 it was just "string" myvar (implicit concatenation)
- notice the custom error numbers
- bans are stored in memory, and every page request is checked against every ban - there are performance implications
Invalidate cache from client side
By IP
/etc/varnish/default.vcl
// IPs/domains that bypass cache acl bypass { "1.2.3.4"; } sub vcl_recv { if (client.ip ~ bypass) { return(pass); } }
return(pass) passes the request to the backend
By Cookie
/etc/varnish/default.vcl
sub vcl_recv { if (req.http.Cookie ~ "LOGGED-IN=1") { return(pass) } }
bypasses cache if there is a LOGGED-IN cookie with value 1
By Header
/etc/varnish/default.vcl
sub vcl_recv { if (req.http.X-Requested-With == "XMLHttpRequest") { return(pass) } }
ajax requests bypass cache
Caching strategies; you can set, and flush now for real world scenarios
Bomb proof static delivery: fullasagoog.com
/etc/varnish/default.vcl
sub vcl_recv { set req.grace = 300s; } sub vcl_fetch { if (beresp.status == 500) { set beresp.saintmode = 20s; if (req.request != "POST") { return(restart); } else { error 500 "Failed"; } } set beresp.grace = 300s; set beresp.ttl = 5s; }
|
Members vs The great unwashed: adnews
- flat proxy for anonymous users
- live site for logged in users; show VCL looking for loggedin cookies
/etc/varnish/default.vcl
sub vcl_hash { ### these 2 entries are the default ones used for vcl. Below we add our own. set req.hash += req.http.host; set req.hash += req.url; set req.http.X-Varnish-Hashed-By = req.http.host req.url; if (req.http.Cookie ~ "LOGGED-IN=1") { set req.hash += regsub( req.http.Cookie, "^.*?LOGGED-IN=1" ); set req.http.X-Varnish-Hashed-By = req.http.X-Varnish-Hashed-By regsub( req.http.Cookie, "^.*?LOGGED-IN=1" ); } return(hash); } sub vcl_recv { // Let any non "GET" / "HEAD" right through if (req.request != "GET" && req.request != "HEAD"){ return(pass); } // strip cookies for static files if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset req.http.Accept-Encoding; // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } unset req.http.Cookie; return(lookup); } // strip cookies for everything except specific pages if ( !(req.url ~ "Login" || req.url ~ "login" || req.url ~ "logout") ) { // Clean up accept encoding values # Handle compression correctly. Varnish treats headers literally, not # semantically. So it is very well possible that there are cache misses # because the headers sent by different browsers aren't the same. # @see: http://varnish.projects.linpro.no/wiki/FAQ/Compression if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "gzip") { # if the browser supports it, we'll use gzip set req.http.Accept-Encoding = "gzip"; } elsif (req.http.Accept-Encoding ~ "deflate") { # next, try deflate if it is supported set req.http.Accept-Encoding = "deflate"; } else { # unknown algorithm. Probably junk, remove it unset req.http.Accept-Encoding; } } // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } // If the user is logged in, let session cookies through (Note: these values should not be getting included in the cache hash) if (req.http.Cookie ~ "FC-LOGGED-IN=1") { set req.http.Cookie = ";" req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(CFID|CFTOKEN|JSESSIONID|FC-LOGGED-IN|FC-ROLES)=", "; \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; } } return(lookup); } else { return(pass); } } sub vcl_fetch { // strip cookies for static files if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset beresp.http.set-cookie; return(deliver); } // strip cookies for everything except specific pages if ( req.url ~ "Login" || req.url ~ "login" || req.url ~ "logout" ) { set beresp.http.X-Cacheable = "NO:Login or logout page"; return(pass); } else { unset beresp.http.set-cookie; } // varnish determined the object was not cacheable if (!beresp.cacheable){ set beresp.http.X-Cacheable = "NO:Not Cacheable"; } elsif (beresp.http.Cache-Control ~ "private"){ set beresp.http.X-Cacheable = "NO:Cache-Control=private"; return(pass); } else { // Keep stale content for a little while, to serve while varnish gets a fresh copy set beresp.grace = 15s; set beresp.http.X-Cacheable = "YES"; } return(deliver); }
User interactivity: adnews
- form posts are not proxied (only GET); comment forms; show VCL ignoring POST requests
/etc/varnish/default.vcl
sub vcl_hash { ### these 2 entries are the default ones used for vcl. Below we add our own. set req.hash += req.http.host; set req.hash += req.url; set req.http.X-Varnish-Hashed-By = req.http.host req.url; if (req.http.Cookie ~ "LOGGED-IN=1") { set req.hash += regsub( req.http.Cookie, "^.*?LOGGED-IN=1" ); set req.http.X-Varnish-Hashed-By = req.http.X-Varnish-Hashed-By regsub( req.http.Cookie, "^.*?LOGGED-IN=1" ); } return(hash); } sub vcl_recv { // Let any non "GET" / "HEAD" right through if (req.request != "GET" && req.request != "HEAD"){ return(pass); } // strip cookies for static files if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset req.http.Accept-Encoding; // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } unset req.http.Cookie; return(lookup); } // strip cookies for everything except specific pages if ( !(req.url ~ "Login" || req.url ~ "login" || req.url ~ "logout") ) { // Clean up accept encoding values # Handle compression correctly. Varnish treats headers literally, not # semantically. So it is very well possible that there are cache misses # because the headers sent by different browsers aren't the same. # @see: http://varnish.projects.linpro.no/wiki/FAQ/Compression if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "gzip") { # if the browser supports it, we'll use gzip set req.http.Accept-Encoding = "gzip"; } elsif (req.http.Accept-Encoding ~ "deflate") { # next, try deflate if it is supported set req.http.Accept-Encoding = "deflate"; } else { # unknown algorithm. Probably junk, remove it unset req.http.Accept-Encoding; } } // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } // If the user is logged in, let session cookies through (Note: these values should not be getting included in the cache hash) if (req.http.Cookie ~ "FC-LOGGED-IN=1") { set req.http.Cookie = ";" req.http.Cookie; set req.http.Cookie = regsuball(req.http.Cookie, "; +", ";"); set req.http.Cookie = regsuball(req.http.Cookie, ";(CFID|CFTOKEN|JSESSIONID|FC-LOGGED-IN)=", "; \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; } } return(lookup); } else { return(pass); } } sub vcl_fetch { // strip cookies for static files if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset beresp.http.set-cookie; return(deliver); } // Let any non "GET" / "HEAD" right through if (req.request != "GET" && req.request != "HEAD"){ set beresp.http.X-Cacheable = "NO:Not GET or HEAD"; return(pass); } // strip cookies for everything except specific pages elsif ( req.url ~ "Login" || req.url ~ "login" || req.url ~ "logout" ) { set beresp.http.X-Cacheable = "NO:Login or logout page"; return(pass); } else { unset beresp.http.set-cookie; } // varnish determined the object was not cacheable if (!beresp.cacheable){ set beresp.http.X-Cacheable = "NO:Not Cacheable"; } elsif (beresp.http.Cache-Control ~ "private"){ set beresp.http.X-Cacheable = "NO:Cache-Control=private"; return(pass); } else { // Keep stale content for a little while, to serve while varnish gets a fresh copy set beresp.grace = 15s; set beresp.http.X-Cacheable = "YES"; } return(deliver); }
We use the following strategy to have user specific content on a site:
|
Personalized pods: adnews, newspaper works
- Ajax personalized content after proxied page; show VCL ignoring Ajax requests
sub vcl_recv { // strip cookies for static files if (req.url ~ "^/favicon" || req.url ~ "^/cache" || req.url ~ "^/css" || req.url ~ "^/js" || req.url ~ "^/wsimages" || req.url ~ "^/base" || req.url ~ "^/webtop/cffp" || req.url ~ "^/webtop/icons"){ //if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset req.http.Accept-Encoding; // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } unset req.http.Cookie; return(lookup); } // strip cookies for everything except specific pages if ( !(req.url ~ "profile" || req.url ~ "Login" || req.url ~ "login" || req.url ~ "logout") ) { // Clean up accept encoding values # Handle compression correctly. Varnish treats headers literally, not # semantically. So it is very well possible that there are cache misses # because the headers sent by different browsers aren't the same. # @see: http://varnish.projects.linpro.no/wiki/FAQ/Compression if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "gzip") { # if the browser supports it, we'll use gzip set req.http.Accept-Encoding = "gzip"; } elsif (req.http.Accept-Encoding ~ "deflate") { # next, try deflate if it is supported set req.http.Accept-Encoding = "deflate"; } else { # unknown algorithm. Probably junk, remove it unset req.http.Accept-Encoding; } } // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } return(lookup); } else { return(pass); } } sub vcl_fetch { // strip cookies for static files if (req.url ~ "^/favicon" || req.url ~ "^/cache" || req.url ~ "^/css" || req.url ~ "^/js" || req.url ~ "^/wsimages" || req.url ~ "^/base" || req.url ~ "^/webtop/cffp" || req.url ~ "^/webtop/icons"){ //if (req.url ~ "\.(jpg|jpeg|gif|png|ico|css|zip|pdf|txt|tar|wav|bmp|rtf|js|flv|swf|html|htm)$") { unset beresp.http.set-cookie; return(deliver); } // Let any non "GET" / "HEAD" right through if (req.request != "GET" && req.request != "HEAD"){ set beresp.http.X-Cacheable = "NO:Not GET or HEAD"; return(pass); } // strip cookies for everything except specific pages elsif ( req.url ~ "profile" || req.url ~ "Login" || req.url ~ "login" || req.url ~ "logout" ) { set beresp.http.X-Cacheable = "NO:Login or logout page"; return(pass); } else { unset beresp.http.set-cookie; } // varnish determined the object was not cacheable if (!beresp.cacheable){ set beresp.http.X-Cacheable = "NO:Not Cacheable"; } elsif (beresp.http.Cache-Control ~ "private"){ set beresp.http.X-Cacheable = "NO:Cache-Control=private"; return(pass); } else { // Keep stale content for a little while, to serve while varnish gets a fresh copy set beresp.grace = 15s; set beresp.http.X-Cacheable = "YES"; } return(deliver); }
Tips
Normalising user agent and compression headers
Varnish can include the user-agent and accept-encoding headers in cache hash, so normalising them is a good idea:
/etc/varnish/default.vcl
sub vcl_recv { if (req.http.Accept-Encoding) { if (req.http.Accept-Encoding ~ "gzip") { # if the browser supports it, we'll use gzip set req.http.Accept-Encoding = "gzip"; } elsif (req.http.Accept-Encoding ~ "deflate") { # next, try deflate if it is supported set req.http.Accept-Encoding = "deflate"; } else { # unknown algorithm. Probably junk, remove it unset req.http.Accept-Encoding; } } // Remove user agent if (req.http.User-Agent) { set req.http.User-Agent = ""; } }
If your website serves different HTML to mobile users, you might use code like this:
if (req.http.User-Agent ~ "iP(hone|od)" || req.http.User-Agent ~ "Android" || req.http.User-Agent ~ "Symbian" || req.http.User-Agent ~ "^BlackBerry" || req.http.User-Agent ~ "^SonyEricsson" || req.http.User-Agent ~ "^Nokia" || req.http.User-Agent ~ "^SAMSUNG" || req.http.User-Agent ~ "^LG" || req.http.User-Agent ~ " webOS") { set req.http.User-Agent = "mobile"; } else { set req.http.User-Agent = "desktop"; }
Custom cache hash
This VCL adds a cookie value (ROLES) to the cache key. req.http.X-Varnish-Hashed-By is helpful for debugging, as you can inspect the request header to find out what the cache key is.
/etc/varnish/default.vcl
sub vcl_hash { ### these 2 entries are the default ones used for vcl. Below we add our own. set req.hash += req.http.host; set req.hash += req.url; set req.http.X-Varnish-Hashed-By = req.http.host req.url; if (req.http.Cookie ~ "ROLES=1") { set req.hash += regsub( req.http.Cookie, "^.*?ROLES=([^;]*);*.*$", "\1" ); set req.http.X-Varnish-Hashed-By = req.http.X-Varnish-Hashed-By regsub( req.http.Cookie, "^.*?ROLES=([^;]*);*.*$", "\1" ); } return(hash); }
Pass along original IP address as a custom header
/etc/varnish/default.vcl
sub vcl_recv { // Pass along the IP for uncached pages if (req.http.x-forwarded-for) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For ", " client.ip; } else { set req.http.X-Forwarded-For = client.ip; } }
Refining your caching strategy.
Varnish tooling
- showcase varnishhist
- plus other command line utils