Some of you may have heard of Ruby on Rails. It is a web development framework, based on the Ruby language. Last year I developed a Rails web application. I like Ruby on Rails a lot, and I strongly prefer it over PHP for serious web applications. With Rails I was able to develop a robust, stable and maintainable web application in a much shorter time than I could have when I used PHP.
Unfortunately, Rails seems to use a bit more resources than PHP, namely:
- Memory usage.
- CPU usage.
I’m not really concerned about CPU usage – my server has enough CPU to handle the load, but memory usage is more problematic. My web server has “only” 1 GB of RAM. It runs quite a lot of services so it’s a bit short on RAM.
A little introduction on how Ruby on Rails interfaces with the HTTP client
But first, let us take a look at how Rails interfaces with a HTTP client (your web browser). My usual setup is as follows:
- The web browser connects to the web server software (in my case, Lighttpd).
- The web server launches one or more Ruby on Rails FastCGI processes. FastCGI is like CGI, but instead of launching a process every time a HTTP request is made, FastCGI keeps the process in memory, so that it is capable of processing more than one request. Note that Apache 2 doesn’t support FastCGI (its support is broken, last time I checked), so I have to use Lighttpd.
- The web server proxies the HTTP request to one of the Rails FastCGI processes.
This setup is illustrated in the following picture:
There are, of course, different setups. Mongrel – a web server designed to run Ruby on Rails – seems to be becoming more and more popular. Unlike Lighttpd which uses FastCGI to spawn several Rails worker processes, Mongrel embeds Rails directly. Rails is not thread safe, so Mongrel can only process one request at a time (unlike Lighttpd which can proxy requests to one of the many worker processes). So what people generally do is to launch several Mongrel processes, and use Apache 2.2 with mod_proxy_balancer to proxy requests to one of the Mongrel processes.
I have no idea why it’s better than using Lighttpd with FastCGI. I heard that Lighttpd’s mod_proxy module was unstable, but I don’t know whether that’s still the case. My web server runs Lighttpd 1.4 and uses mod_proxy to proxy requests to some backend web servers, and I’ve never had problems with it.
But what about memory usage?
A process which embeds Ruby on Rails (that is, either a Rails FastCGI process or a Mongrel process) uses between 20 MB to 30 MB. It is usually a good idea to launch more than one FastCGI process (or Mongrel process) so that your web server can process more than 1 requests concurrently. But the memory usage quickly adds up. If you load 4 Rails processes then you’re already using about 100 MB of memory. My web server has “only” 1 GB of RAM (it runs quite a lot of services so it’s a bit short on RAM). So I’ve been looking for ways to reduce Rails memory usage.
Luckily, there is a way, and it’s called fork and copy on write.
As you might know, processes’ memory are isolated. That is, one process cannot read or write another process’s memory. On modern Unix operating systems, when a parent process forks a child process, almost all of the memory between the parent and child process is shared. So if your 200 MB bloated application forks a child process, the child process actually only uses a few kilobytes. Only when either the parent process or the child process writes to a piece of memory, that piece of memory is copied, so that the parent process’s memory changes won’t affect the child (and vice versa). This is why it’s called “copy on write”.
We can use this simple fact to save memory. For example, mod_perl uses this to reduce web server Perl scripts’ startup time and memory usage. mod_perl loads all required Perl modules in advance. When the web server forks a new child process to process a HTTP requests, the memory used by the already loaded Perl modules will be shared between the parent and the child process. Because most (or all) of the modules are already loaded, Perl will not load them again, thus significantly reducing loading time.
This technique has only one disadvantage: it doesn’t work on Windows! 🙂 Windows has no fork() system call.
We can use the same technique on Ruby on Rails. Though I find it strange that not many Rails users seem to care – they happily spawn multiple Mongrel processes but I haven’t seen many people asking why Mongrel doesn’t use fork() to save memory. Kirk Haines said that it’s because ActiveRecord doesn’t like fork (the connection to the database seems to break after a fork). But I don’t know why Mongrel doesn’t just fork before any requests are processed.
How much memory does Ruby on Rails use, really?
I decided to measure Rails’s memory usage. My Rails environment was as follows:
config.cache_classes = true config.whiny_nils = true config.breakpoint_server = false config.action_controller.consider_all_requests_local = true config.action_controller.perform_caching = false config.action_view.cache_template_extensions = false config.action_view.debug_rjs = false
This was my memory usage before launching Rails:
total used free shared buffers cached Mem: 1011 365 645 0 1 97 -/+ buffers/cache: 267 744 Swap: 996 0 996
I then proceeded to launch 10 independent Rails FastCGI processes, which do not share memory with each other (other than the memory used by the Ruby interpreter itself). After the launch, the memory usage was as follows:
total used free shared buffers cached Mem: 1011 537 474 0 1 97 -/+ buffers/cache: 438 573 Swap: 996 0 996
The 10 processes used 171 MB in total, or 17.1 MB per process. But we’re not there yet: memory usage is likely to increase when I make a HTTP request. So I used the httperf tool and ran
httperf --uri /rails/ --port 3501 --num-conns 100 --rate 20
This was the memory usage after httperf was done:
total used free shared buffers cached Mem: 1011 551 459 0 1 97 -/+ buffers/cache: 452 558 Swap: 996 0 996
Memory usage was increased by 10 MB. So that’s 1.5 MB extra memory per process, and 186 MB in total for all 10 processes.
I wrote a script which loads all the Ruby on Rails library. It will then fork and spawn x Rails FastCGI processes, where x is a number which you can configure. Each FastCGI process will have its own Unix socket for communication with the web server.
This was my memory usage before I launched the script:
total used free shared buffers cached Mem: 1011 334 676 0 2 95 -/+ buffers/cache: 236 775 Swap: 996 0 996
After instructing the script to launch 10 processes, memory usage become:
total used free shared buffers cached Mem: 1011 364 646 0 3 95 -/+ buffers/cache: 265 745 Swap: 996 0 996
Nice! All 10 processes only used 30 MB in total, which is a far cry from the previously measured 171 MB. Now, let us see what happens after we run httperf:
total used free shared buffers cached Mem: 1011 381 629 0 3 96 -/+ buffers/cache: 282 729 Swap: 996 0 996
Memory usage has gone up by 17 MB. That’s 1.7 MB extra memory per process.
By using preforking I was able to reduce memory usage for 10 Rails processes from 186 to 47 MB. That’s a memory saving of 75%! The Rails application seems to work fine. So far I haven’t been able to detect any strange behavior.
Without preforking, the memory usage, in MB, follows this formula:
memusage(n) = 18.6 * n
…where n is the number of Rails processes. With preforking, the memory usage is:
memusage(n) = 30 + 1.7 * n
The graph looks like this:
The script can be downloaded here. Put it in your ‘scripts’ folder. Launch the script as follows:
…where NUM is the number of Rails processes you want to launch. Each process will be given its own Unix socket, named ‘log/fastcgi.socket-x’, where x is the process’s sequence number (which starts from 0). So if you launch 3 processes, the following Unix sockets will be created:
You must also setup Lighttpd to communicate with Rails through the sockets. I use this configuration:
fastcgi.server = ( ".fcgi" => ( ("socket" => "/path-to-your-rails-root-folder/log/fastcgi.socket-0"), ("socket" => "/path-to-your-rails-root-folder/log/fastcgi.socket-1"), ("socket" => "/path-to-your-rails-root-folder/log/fastcgi.socket-2") ) )