ESI caching in Ruby on Rails
Caching and performance is something that always lingers at the back of my mind, and the Holy Grail is yet to be found as far as I am concerned. While looking around on various blogs recently, I came across some fantastic articles on caching that I found really exciting. In a nutshell the article introduces the concept of using ESI, or Edge side includes to create huge performance boosts in Rails applications. ESI is a standard that I had never heard of before, though it has been around since 2001. The specification for ESI was written by some interesting people who have great pedigree in the field of advanced scalabilty, Akamai for example.
ESI is very similar to SSI but has a more sophisticated method for including content that can use things like a try-catch type DSL to ask for content and fall back to alternatives if something goes wrong. To put this in context and to highlight why it is really cool, consider Rails’ whole page caching. An app can write an entire action to a static file in the public directory. Your server can pass this file to its clients without touching Rails and its routing system. This has always seemed like a fantastic concept, but one that can hardly ever be used in the wild. Necessary elements in the page such as shopping cart contents, links to your account, user’s status etc., always spoil the fun when you are trying to avoid hitting the app. This forces you to use fragment caching and model caching when caching 99% of the page at the level of Mongrel would be far more ideal.
Enter ESI (SSI will also do, but ESI has some additional nice features). Whole actions can be cached as static content above the level of your app, but embedded in them is markup that the server executes to fetch more content where necessary. A high traffic shopping site could have something along these lines, in place of some content in the page that displays a cart:
... <esi:include src="/cart" max-age="0"/> ...
The users’ session is passed through, and you can retrieve the items in their cart and render it as on a mini action in your Rails app. The server is responsible for inserting it into the markup and returning it to the client. The max-age of 0 means that it will ask for a fresh version from your app everytime it is run. However, setting it to a higher TTL value means that subsequent request will use a cached copy that the server stores and is responsible for. The really cool thing about this is that it puts responsibility of dealing out cached content onto your web server, rather than the actual Rails app. At the moment you need a custom version of mongrel that supports ESI to use it. Also, it is worth noting that the current version of mongrel-esi stores these cached fragments in memory, though the guys are working on a memcached version. Have a look here to get the install details and also a plugin that helps you integrate it into your Rails apps.
This is obviously cool stuff, but there are some other neat things you can add into the mix. The Rails default method of storing the cached pages is on the file system in your public directory, which gets a bit messy. However, in this article
it describes putting them into memcache and then telling nginx to look in memcache before hitting your app to retrieve them. This is pretty fantastic as it makes managing the cache much easier.
Another option to consider for delivering the fragments would be a lovely lightweight merb application, since the src attribute of an esi:include can be another url, it doesn’t just have to be a path on the same server. I haven’t actually tried this, so it should be taken with a pinch of salt, but I think you should be able to do this:
<esi:include src="http://super_merb.my_app.com/cart" max-age="0"/>
So things are looking pretty nifty in this area, and I am definitely going to be keeping an eye on it.