#150 Rails Metal
- Download:
- source codeProject Files in Zip (82.4 KB)
- mp4Full Size H.264 Video (13.6 MB)
- m4vSmaller H.264 Video (9.57 MB)
- webmFull Size VP8 Video (25.4 MB)
- ogvFull Size Theora Video (19.1 MB)
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 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.