memcaches_page plugin for Rails

Filed under linux, memcached, plugin, programming, rails, ruby, and web.

What is it?

This plugin is very similar to the built-in Rails ‘caches_page’ functionality, except it caches to memcached rather than a file. It relies on the ‘memcached_pass’ nginx directive to serve pages directly from memory if possible, and only passes to rails if necessary. On my server I’ve seen a 75% reduction in Ruby memory usage using this technique.

Disclaimer

This approach is quite heavy-handed, and works best when the content you are serving changes rarely. If you have highly dynamic content, you’re probably better off developing your own, more finely-grained caching. It won’t work if the pages you are serving have some kind of user-specific info in the page
(eg. the logged-in user name in the page header).

Installation

  1. Install a couple of memcache gems
    > sudo gem install memcache-client
    > sudo gem install Ruby-MemCache
    
  2. Copy memcaches_page.rb into the ‘lib’ directory of your rails app.
  3. Add the following to the bottom of your environment.rb :
    require 'memcaches_page'
    memcache_options = {
    	:c_threshold => 10_000,
    	:compression => true,
    	:debug => false,
    	:namespace => 'code.recurser.com',
    	:readonly => false,
    	:urlencode => false
    }
    MemcachedPageKeyPrefix = '/code'
    MemcachedPageTtl = 604800
    Cache = MemCache.new memcache_options
    Cache.servers = 'localhost:11211'
    
    A few things to note :
    • Set the namespace to something appropriate for your app – you’ll need it later when you set up nginx.
    • The MemcachedPageKeyPrefix should be set if you run your Rails app from a subfolder – leave blank otherwise.
    • The MemcachedPageTtl is the time-to-live (in seconds) in the cache – I set it for a week which is fairly excessive – you probably only need a few hours, depending on what you’re trying to achieve.
    • Point Cache.servers at the server/port you are running memcached on.
  4. To cache every action in a controller, add the following filter to any controllers you want to cache :
    after_filter :memcache_page
    
    Alternatively, add the following line near the top of your controller to cache specific actions only (where ‘view’ and ‘list’ are actions you want to cache :
    memcaches_page :view, :list
    
    When the page is rendered, the memcaches_page plugin will kick in, and save a copy of the page to your memcache. Rails never uses this cached version directly – in section (7), you’ll configure nginx to check the cache before passing requests to Apache.
  5. Install phusion passenger
    > sudo gem install passenger
    > passenger-install-apache2-module
    
  6. Configure apache – For example, I run wordpress on my main domain (recurser.com), and a rails app (Redmine) in recurser.com/code/. Apache is configured with the following settings for the recurser.com domain :
    RailsBaseURI /code
    PassengerMaxPoolSize 3
    PassengerMaxInstancesPerApp 2
    PassengerPoolIdleTime 120
    
    For the complete config, see apache_example.conf
  7. Configure nginx – for the complete config, see nginx_example.conf . The important sections for our purposes are :
    • Set up a ‘backend’ service called ‘apache’ to pass requests to:
      upstream apache {
      	server 127.0.0.1:8080;
      }
      
    • Catch requests to the ‘/code’ subdirectory, and try to serve them from the cache:
      location /code {
      	if ($request_method = POST) {
      		proxy_pass http://apache;
      		break;
      	}
      	default_type  "text/html; charset=utf-8";
      	set $memcached_key  "code.recurser.com:$uri";
      	memcached_pass      127.0.0.1:11211;
      	error_page          404 502 = @backend;
      }
      
      This does a couple of things :
      1. If the request is a POST, serve from Apache and ignore the cache
      2. Set the memcached key to lookup, and serve the page directly from memcached if possible (memcached_pass directive).
      3. If the page is not in the cache, pass the request back to the ‘backend’. Make sure the code.recurser.com part of the key matches what you set as the :namespace in step (3)!
    • Set up the ‘backend’ proxy that passes to apache:
      location @backend {
      	proxy_pass         http://apache;
      }
      

If you enjoyed this post, please follow us on twitter or subscribe to our feed!