memcaches_page plugin for Rails
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
- Install a couple of memcache gems
> 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 :A few things to note :
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'- 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 :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 :
after_filter :memcache_pageWhen 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.memcaches_page :view, :list - Install phusion passenger
> 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 :For the complete config, see apache_example.conf
RailsBaseURI /code PassengerMaxPoolSize 3 PassengerMaxInstancesPerApp 2 PassengerPoolIdleTime 120 - 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:This does a couple of things :
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; }- 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:
location @backend { proxy_pass http://apache; }
- Set up a ‘backend’ service called ‘apache’ to pass requests to: