Dave Perrett

Memcaches_page Plugin for Rails

linux, memcached, plugin, programming, rails, ruby, web

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

  • Install a couple of memcache gems
1
2
> sudo gem install memcache-client
> sudo gem install Ruby-MemCache
  • Copy memcaches_page.rb into the ‘lib’ directory of your rails app.
  • Add the following to the bottom of your environment.rb :
1
2
3
4
5
6
7
8
9
10
11
12
13
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. * To cache every action in a controller, add the following filter to any controllers you want to cache :

1
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 :

1
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. * Install phusion passenger

1
2
> sudo gem install passenger
> passenger-install-apache2-module
  • 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 :
1
2
3
4
RailsBaseURI /code
PassengerMaxPoolSize 3
PassengerMaxInstancesPerApp 2
PassengerPoolIdleTime 120

For the complete config, see apache_example.conf * 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:

1
2
3
upstream apache {
  server 127.0.0.1:8080;
}

** Catch requests to the ‘/code’ subdirectory, and try to serve them from the cache:

1
2
3
4
5
6
7
8
9
10
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 :

If the request is a POST, serve from Apache and ignore the cache

Set the memcached key to lookup, and serve the page directly from memcached if possible (memcached_pass directive).

* 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:

1
2
3
location @backend {
  proxy_pass         http://apache;
}