Rails page caching, nginx, SSI, Ajax and form POSTS 3

Posted by science on February 22, 2008

Fence post

A couple of great blog posts have come out in the last year, dealing with how to squeeze more performance out of Ruby on Rails. These techniques are actually massively improving the performance of Ruby on Rails, so much so that I’m starting to think of Rails less as an Application Framework and more as an Web Page Template Generator.

For background here are two of the best articles on using page caching, nginx and SSI to radically improve performance on Rails while sacrificing nothing:

http://theexciter.com/articles/dynamic-page-caching-with-nginx-ssi

http://blog.kovyrin.net/2007/08/05/using-nginx-ssi…

These techniques are great. They apply to you if your application:

1) Generates lots of pages with unique URL’s and each such page is accessed over and over.

1.a) For example, your site might have a single action which displays information about various regions around the country. It’s a single action which pulls from a database to generate info about Hawaii or the Gulf Coast, FL. Your URL’s look like:

/region/info/Gulf-Coast-Florida

/region/info/Hawaii

2) Each page is relatively slow (> 2s?) and resource consuming to build but doesn’t change all that often. You might assemble this region info from ActiveResources and XML feeds and what not, but once you’ve assembled the content on the page it might be valid for weeks or months.

3) Each page has some amount of dynamic content that prevents you from using page caching.

3.a) Each page might have a “login” link, or if the user is logged in it might be a link to their account saying “Welcome back Science! (account)”

In the traditional Rails model, you might just live with a 4s load time and move on with other projects. But with nginx SSI and page caching you don’t have to. You can build the dynamic content as a partial (like an Ajax request which returns an HTML partial). The rest of the page you build as fully page cached to disk or memcache. Nginx reads your SSI tag and calls back to Rails for only the very lightweight frequently changing dynamic content to assemble the entire page.

That’s pretty cool and the other articles cover this much in greater detail and clarify. Read up!

Science has been working on extending this work ever so slightly. What if your actions are CRUD and not only do they generate the above slowly changing dynamic content for GET requests but they also generate variable search results for form POST’s or AJAX requests (or WSDL or whatever): they behave differently, yielding alternative content depending on the CRUD or request method.

In the caching model above, you’re out of luck. Your nginx server is going to happily serve out your static page to all requests no matter what. A form or ajax post will simply yield the full page content just like before.

One way to solve this is to change your URL’s so that the request method is parsable on the command line:

/ajax/region/info/Hawaii

/post/region/info/Hawaii

But that kind of blows huh?

Another way to handle this is much more in the spirit of CRUD: Configure Nginx to by-pass POST requests and only serve the static page caches on GET requests. This makes perfect sense from an HTTP perspective: GET’s are cachable and POST’s reflect dynamic data. Perfect distinction. Since Ajax sends (generally speaking) its requests as POST’s everything fits together.

So get out there and add code to nginx to force it to skip page caching for POST’s. Here’s some snippets from my nginx.conf to give an idea of one way to solve this problem:

# rewrite any trailing "/" without the trailing "/"
# this is a work around - we must force nginx to treat pages with trailing slashes
# exactly like pages without them, so the page cache detection works correctly below
rewrite ^/(.*)/$ /$1 last;

location / {
  # turn ssi capability on
  ssi on;
  proxy_set_header  X-Real-IP  $remote_addr;
  proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_redirect false;
  proxy_max_temp_file_size 0;

  location ~ ^/(images|javascripts|stylesheets)/ {
    expires 10y;
  }

  # don't page cache any POST requests
  if ($request_method = POST) {
    proxy_pass http://mongrel;
    break;
  }

  # if we find the cached file on disk now (i.e. it's a GET request) serve it
  if (-f $request_filename.html) {
    rewrite (.*) $1.html break;
  }

  # if we find index.html version of request in root of website, it's also a cached file so serve it
  set $index 'index.html';
  if (-f $request_filename$index) {
    rewrite (.*) $1$index break;
  }
  # if we find index.html files anywhere else, serve as cached
  if (-f $request_filename/$index) {
    rewrite (.*) $1/$index break;
  }
  # otherwise if we don't find the file on disk, hand it off to mongrel/Rails to process
  if (!-f $request_filename) {
    proxy_pass http://mongrel;
    break;
  }
}
Trackbacks

Use this link to trackback from your own site.

Comments

Leave a response

  1. science Tue, 08 Apr 2008 08:15:44 EDT
  2. links for 2008-04-10 « Bloggitation Wed, 09 Apr 2008 16:33:30 EDT

    [...] Rails page caching, nginx, SSI, Ajax and form POSTS (tags: ruby rails web nginx sysadmin) [...]

  3. Jauder Ho Thu, 10 Apr 2008 00:45:52 EDT

    Thanks for the article. This is all very useful.

    Have you done any experimentation with Varnish at all? I am considering a setup of varnish >> nginx >> mongrel/thin

Comments