homeASCIIcasts

150: Rails Metal 

(view original Railscast)

One of the new features in Rails 2.3 is Rails Metal. It provides a way to bypass Rails’ routing and request process and go straight from the server to the request’s logic. This can help the performance of your Rails app if you’re trying to make a certain action run as fast as possible.

Below is an application that monitors the processes on a server through a web interface. The list is updated via an AJAX call every few seconds. The update is a fairly simple request that is called often, so it’s a good candidate for optimising with Metal.

The list of processes on our server.

The application shows the processes on our server.

The view code for the page above is straightforward. It uses periodically_call_remote to make a GET request to the processes controller’s list action and updates the contents of the pre element every three seconds.

<h1>Processes List</h1>
<pre id="processes">Gathering processes...</pre>
<%= periodically_call_remote :url => "/processes/list", :update => "processes", :frequency => 3, :method => :get %>

The view code for the index action.

The list action in the controller is also simple. It calls the ps command with a number of parameters and returns its output.

class ProcessesController < ApplicationController
  def index
  end

  def list
    render :text => `ps -axcr -o "pid, pcpu, pmem, time, comm"`
  end
end

The list action calls ps to get a list of running processes.

Generating Metal

To optimise our action we’ll first need to make sure we’re running Rails 2.3 which, at the time of writing, is still at the release candidate stage. Once we’ve done that we can generate our Metal script.

script/generate metal processes_list
    create  app/metal
    create  app/metal/processes_list.rb

The script will generate a metal directory under /app and create a ruby file with the name of our metal script. The file looks like this.

# Allow the metal piece to run in isolation
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)

class ProcessesList
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/processes_list/
      [200, {"Content-Type" => "text/html"}, ["Hello, World!"]]
    else
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end

The first line of the Metal script loads the Rails environment unless it has already been loaded while the rest of it is a class. If you’ve dealt with rack application before then this class should look familiar. As with rack, there’s a method called call that takes a hash of environment variables. The call method returns an array, which contains three elements representing three parts of the response. The first is the HTTP status number; the second is a hash of headers and the third is the response’s body.

Our Metal class is basically a rack application of its own and so it will bypass routing and request process that normal requests to a Rails action go through. The request won’t be logged in Rails’ log file; even that is bypassed.

Our ProcessesList class first uses a regular expression to check that the path begins with process_list. If it does then it will return “Hello, World!”; otherwise it will return a 404 error. If the 404 error is returned it will be detected by Rails, which then takes over the processing of the request.

We’ll now take a look at our Metal action to see if it’s working, but before we do we’ll have to restart the server. Even in development mode changes to a Metal script aren’t picked up so we’ll need to restart every time we make a change. Once the server has restarted we can go to http://localhost:3000/processes_list and we’ll see the “Hello, World!” body content that was in our Metal script returned.

Our processes list action is at /processes/list. If we were to change the line in our Metal script that looks for a matching URL so that it looked to match /processes/list (rather than /processes_list) then the Metal script would pick up the request before Rails had a chance to process it and we’d see “Hello, World!” returned. For now we’ll keep it at /processes_list so that we can compare the speed of each later.

To compare Metal with a normal Rails request we’ll have to have each method do the same thing so we’re going to replace the “Hello, World!” text with the same call to ps that the list action makes.

class ProcessesList
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/processes_list/
      [200, {"Content-Type" => "text/html"}, [`ps -axcr -o "pid, pcpu, pmem, time, comm"`]]
    else
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end

Our Metal script now returns the same list of processes as the list action.

Next we’ll update the index view so that periodically_call_remote calls the URL for our Metal script.

<h1>Processes List</h1>
<pre id="processes">Gathering processes...</pre>
<%= periodically_call_remote :url => "/processes_list", :update => "processes", :frequency => 3, :method => :get %>

If we look at our process index page again we’ll see the same output we saw earlier. The only difference is that the AJAX request is being made to our Metal script, rather than through the processes controller.

If we look at the development log, we can see that while a call is being made to the index action, there is no record of the calls to list. This is because the Metal script handles the request before Rails has a chance to log it. Metal requests happen outside of Rails’ logging process so they won’t appear in the log file.

Benchmarking

To see if using Metal has sped our request up we’ll run some benchmarks to compare each request. We’ll need to use the production environment to run our benchmarks so we’ll stop the server and restart it in production mode.

script/server -e production -d

We’ll do our benchmarking with the ab command. We’ll call the Rails request first, then the Metal one and compare the results.

Rails Request Metal Request
ab -n 100 http://127.0.0.1:3000/processes/list          
Requests per second:    20.03 [#/sec] (mean)
Time per request:       49.924 [ms] (mean)
Time per request:       49.924 [ms] 
(mean, across all concurrent requests)
        
ab -n 100 http://127.0.0.1:3000/processes_list 
Requests per second:    36.94 [#/sec] (mean)
Time per request:       27.073 [ms] (mean)
Time per request:       27.073 [ms] 
(mean, across all concurrent requests)         
        

Comparing the Two Requests

The ab command returns a lot of information, but the parts we’re interested in are shown above. We can see that we’re getting almost twice as many requests per second from the Metal request as from the Rails one. Of course the figures will vary depending on the machine you’re running the benchmark on and on the complexity of the request you’re making.

Despite this speed increase we don’t really want to be using Metal to replace every action in our Rails applications. It is best used for requests that are called frequently, or ones that have been optimised as much as possible but which still need an extra speed boost. If you’re not sure if you need to be using Metal for a certain request it’s best to try using a traditional Rails action and optimising that first.