Vanilla and Varnish Cache - some lessons learned
Per @Todd's suggestion, I wanted to post some info on what I've learned so far with configuring Varnish Cache with Vanilla. I've spent many an evening over the past couple of weeks frantically googling to try to run down issues and I'm pretty happy with the functionality, so the config is probably in a state to share. I'm working on a big blog post about the experience, and I'll add a link to this thread when I've got it done, but I can trim out just the Vanilla bits for you guys and put them here.
Some caveats:
1) I use Nginx. If you're using Apache, then there might be additional stuff you have to do. I don't know, because my life has been better since I got Apache out of it and I don't intend to ever go back.
2) The Nginx config I'm using with Vanilla can be found here.
3) I'm also using Vanilla 2.1, so this might or might not work with 2.0.
What gets cached?
Rather than dive into exactly how to cache full pages, I decided to sort of cheat and cache everything but full pages. The config below lets Varnish cache CSS files, javascript, @font-face files, and any images (including userpics and sprite pngs).
Here are the relevant bits out of my Varnish default.vcl file. Yes, I know I should keep stuff like this in a separate vcl file, but I am lazy. I'm leaving out the backend and other fancy stuff and just posting the stuff from the vcl_ subs. I'll go through the entire big-ass config on the blog post, when I finish that.
sub vcl_recv { # We don't care about POSTs, so pass them if (req.request == "POST") { return (pass); } # Ignore WhosOnline plugin if (req.url ~ "/plugin/imonline") { return (pass); } # Ignore Vanilla analytics if (req.url ~ "/settings/analyticstick.json") { return (pass); } # Ignore Vanilla notifications if (req.url ~ "/dashboard/notifications/inform") { return (pass); } # Remove cookies from things that should be static, if any are set if (req.url ~ "(?i)\.(png|gif|jpeg|jpg|ico|swf|css|js|html|htm|woff|ttf|eot|svg)(\?[a-zA-Z0-9\=\.\-]+)?$") { remove req.http.Cookie; } # Strip & re-set X-Forwarder-For header, so that Nginx sees # actual client IP addresses and not localhost for everything remove req.http.X-Forwarded-For; set req.http.X-Forwarded-For = req.http.rlnclientipaddr; # Remove Google Analytics cookies so caching functions if (req.http.Cookie) { set req.http.Cookie = regsuball(req.http.Cookie, "(^|;\s*)(__[a-z]+|has_js)=[^;]*", ""); } if (req.http.Cookie == "") { remove req.http.Cookie; } } sub vcl_pass { # Ensure connections are closed and not reused set bereq.http.connection = "close"; # Ensure even passed/piped requests still carry a good X-Forwarded-For header if (req.http.X-Forwarded-For) { set bereq.http.X-Forwarded-For = req.http.X-Forwarded-For; } else { set bereq.http.X-Forwarded-For = regsub(client.ip, ":.*", ""); } } sub vcl_pipe { # Ensure connections are closed and not reused set bereq.http.connection = "close"; # Ensure even passed/piped requests still carry a good X-Forwarded-For header if (req.http.X-Forwarded-For) { set bereq.http.X-Forwarded-For = req.http.X-Forwarded-For; } else { set bereq.http.X-Forwarded-For = regsub(client.ip, ":.*", ""); } } sub vcl_fetch { # Strip cookies before static items are inserted into cache. if (req.url ~ "\.(png|gif|jpg|swf|css|js|ico|html|htm|woff|eof|ttf|svg)$") { remove beresp.http.set-cookie; } # Adjusting Varnish's caching - hold on to all cachable objects fo 24 hours. # Objects declared explicibly as uncacheable are held for 60 seconds, which # helps in the event of a sudden ridiculous rush of traffic. if (beresp.ttl < 24h) { if (beresp.http.Cache-Control ~ "(private|no-cache|no-store)") { set beresp.ttl = 60s; } else { set beresp.ttl = 24h; } } } sub vcl_deliver { # Display hit/miss info if (obj.hits > 0) { set resp.http.X-Cache = "HIT"; } else { set resp.http.X-Cache = "MISS"; } }
The comments shoudl explain most of this. Starting at the top, we automatically pass on all HTTP POST requests, ignore Vanilla's chattering, and most importantly strip cookies off of everything that might even be remotely static. The regex on the end of that line ensures that we're caching all versions of the JS & CSS that Vanilla produces (and you can always manually curl -x PURGE things that are known-stale). Then there's a great deal of fiddling with the X-Forwarded-For header, which is obviously extremely important so that your logs aren't filled with 127.0.0.1 from end to end, and a short regex to remove Google Analytics cookies. Then more X-Forwarded-For massaging, to ensure PASS and PIPE exceptions have correct IP addresses, and then some more cookie fiddling for when objects are fetched. The penultimate bit ensures that even objects with "NO DON'T CACHE ME PLEASE!" settings are held for at least a little bit, since you never know when that Slashdotting might come crashing down on your head, and finally we close with a quick extra header which helps tell you on view whether or not each object being served is served from cache or from the backend.
As I said, I'll have more of an explanation when I get around to actually writing the blog post about this, but this should be enough to nudge any brave souls who want to try it out in the right direction
Comments
This is awesome stuff. I'm wondering if you can explain the following a bit further:
Does varnish deliver the uncacheable stuff if nginx times out or something?
The other thing that I was thinking would be a huge performance gain would be to cache pages for a short time for guests. Is there a way to test for the existence of a cookie and then set the ttl for that session?
Haven't had a chance to digest all of this yet, but this looks like some awesome work, and is going to be extremely useful for my project. Thank you for sharing @LeeH!
@Todd -
The logic from that section is lifted in part from here, which explains some of the difficulty in an RFP-compliant cache. The crux of their solution is this:
...
Varnish leaves Expires: and Cache-Control: headers intact, and sets the Age: header with the number of seconds the object have been cached and therefore, any RFC2616 client will do the right thing by default.
A web server can obviously also declare objects uncacheable, but what shouldn't be cacheable by a client and what shouldn't be cacheable by an actual web server object cache might be two different things; consequently, I wanted to make sure to ferret out things which might have a no-cache or private attribute and which would otherwise be ignored by Varnish and make sure we hang onto them for 60 seconds. In regular day-to-day serving this probably isn't a big deal, but a badly-coded web app or a web designer not paying attention might declare a big fat background image or something as no-cache, which might screw us over the day that Reddit comes calling.
There is also a separate Varnish function called "Grace Mode" which does indeed serve stale cached content past its ttl if the backend is unreachable or busy; that's something you'd definitely want on, but it only applies to stuff in the cache. Overriding no-cache ensures that stuff is in the cache in the first place.
You can absolutely test for the presence of cookies and do some per-session caching—in fact, there's a somewhat outdated example of this on the Varnish site, demonstrating how content can be cached for logged-in users. The example sort of demonstrates how to incorporate a user's session ID into the hash that Varnish uses to identify content, thus giving each logged in user a unique content store. The immediate problem is that this could generate a tremendously large cache, since every session would get its own set of cached objects.
It should also be possible to set conditions based on a cookie's name or contents, using a regex in sub rcl_recv, but I'm not sure exactly how to do it—I'd need to read the docs a bit harder and puzzle it out. I tend to figure this stuff out by screwing around with it and breaking it
Blog post is up, for anyone wanting to know more about the Varnish Cache setup I'm using in production:
http://blog.bigdinosaur.org/adventures-in-varnish/
@LeeH
Thank you! Great blog post; I read it this morning. Also, thank you for including the VCL file. Awesome job, and thanks for sharing your experience with us.
@LeeH Awesome work. This was on my list of things to do, and you've saved me a lot of time with a great head start
Vanilla Forums COO [GitHub, Twitter, About.me]
Great work http://codecanyon.net/forums/thread/varnish/70540
There was an error rendering this rich post.
Glad to do it. It was a fun way to spend my vacation, which says a lot about my mental health
I'm sure there's room for improvement, since I was reading the docs & making stuff up as I went along, so when you do get around to implementing a "recommended" config or at least a starting template, I'd love to see it!
You could easily make full pages cacheable, too, with some thought about cookies, I believe. Alternately, it would take a lot of rework, but you could use Varnish's edge-side includes (ESIs) to chop up pages into static and dynamic portions, in order to make the entire forum more cache-friendly.
You could also benefit from having the web server send a PURGE for the current view's page whenever a discussion is updated—see here for how to do this with PHP. That way, every time a post is made, the discussion's page (and perhaps also the parent category's page) is purged and recreated.
I dunno—there's definitely lots of room for improvement, since caching images & css & JS gets rid of the bulk of the bits that have to be moved by the web server, but won't help you hit MySQL any less.
@422 - thanks
We're currently offloading all of our static content: CSS, JS, Images, Avatars, etc onto a cluster of nginx servers that only server static content. This dropped the load on our app servers by a ton. I think we could easily add Varnish in front of those servers without much configuration and see some benefit.
The harder part is the cookie inspect and PURGE stuff, so I'll see about that. It could probably be done with plugin hooks, so there could be a Varnish plugin.
Vanilla Forums COO [GitHub, Twitter, About.me]
@Tim - That's definitely nginx's strong suit, though personally I think it's a good fit everywhere, including serving at the application layer (unless you go specialized with node.js or some other application server/framework Frankenstein thing). And Nginx does obviously cache content using the host file system, but tossing one or more Varnish boxes in the very front as a load balancer might actually let you retask or even eliminate some content servers, since you're at minimum eliminating a lot of file system IO with Varnish serving objects out of its store. Your bandwidth is still obviously a limitation, but as long as your cache hit rate is relatively high, you can service more IOs with less hardware.
With your hosted solution bring your obvious money-maker, I can see how you've got some challenges to overcome: how to leverage Varnish as more than just a css/js/image cache in a way that benefits your service offerings without complicating or breaking them. That's totally over my head and it's why they pay you guys the big bucks, so good luck with that ;-)
We're now using Varnish on our hosted clusters to great effect! Thanks for the initial research you did @LeeH, it was very helpful.
Vanilla Forums COO [GitHub, Twitter, About.me]
That's awesome And I'm sure that using Varnish with Vanilla for the hosted clusters will have trickle-down benefits for us self-hosters, since Vanilla's code will no doubt evolve now with Varnish compatibility in mind.
I'm going to be doing a bit of server re-architecture in the next couple weeks. I think varnish will find it's way into the wiring diagram. I'll post my results.
Vanilla Wiki, Tastes Great! 31,000 viewers can't be wrong. || Plugin Development Explained
Tried this but in headers it says:
X-Varnish: 850271844
Age: 0
Via: 1.1 varnish
Connection: keep-alive
X-Cache: MISS
So nothing is caching. I'm also on Nginx
HITS always = 0 hits. I'm sure its something simple in the above config I need to change. So I have rewritten to:
I will fix any issues one by one and add ignore stuff one by one.
TTFB down big time!!!
EDIT: not sure how to make my code user-friendly sorry.
it seems in the varnish above I'm guessing the following rules mean to ignore files coming from these two areas. If this statement is correct.
what would be the equivalent .htaccess rule for this
# Ignore WhosOnline plugin
if (req.url ~ "/plugin/imonline") {
return (pass);
# Ignore Vanilla notifications
if (req.url ~ "/dashboard/notifications/inform") {
return (pass);
}
I ask because I am trying to resolve an issue here and provide some insights for this.
http://vanillaforums.org/discussion/21977/anyone-using-wordpress-plugin-w3-total-cache-with-whos-online-plugin#latest
I may not provide the completed solution you might desire, but I do try to provide honest suggestions to help you solve your issue.
Hello! Can You help me with next thing: Is there any opportunity to count number of hits and misses for each backend separately? varnishstat and varnishlog does not give this info...
Sorry for resurrecting this post, but I was struggling with cache not working with LeeH setup. Was getting the response X-Cache as MISS.
Just added
return(delivery);
to the last line of vcl_fetch() and it started to work.
May be useful for somebody.
Good question Damir, Ive had to solve this recently and one way to do it is to log HIT/MISS status in varnishncsa log. This lets you then work on log files and group them as needed.
Otherwise you can just deploy a Varnish solution that gives you metrics and logs out of the box - www.section.io
Let me know if any additional help needed,
Matt
Even though this topic goes pretty deep into Varnish, I seem to run into some low level issues with Varnish.
My provider offers a Varnish cloud and the only control you have is: ON or OFF, and the choice between static, dynamic and cache_all. So I can't customize the handling for various filetypes, for example.
My first tests with Dynamic are terrible: Users can't login anymore ("Please try again.") and also registering is impossible. I'm about to find out if it will work again with setting static, but each change on Varnish settings take approx 15 minutes to reload ...
In the mean time: Any other advice or best practices regaring Vanilla + Varnish where it's no option to change the Varnish config yourself? Are there settings that can be place in htaccess or the Vanilla config.php?
Thanks!