AsyncRecord: Non-blocking database access for Ruby

Two weeks ago I developed my first event-driven web framework for Ruby, Fastr. It helped me understand why running a web framework in an event loop is so natural.

As I continued to tackle more features in Fastr, it was time to tackle persistence — notably, database access.

AsyncRecord is/will be an ORM, similar to ActiveRecord — with one major difference — it doesn’t block. AsyncRecord currently uses em-mysql to access a MySQL database.

How it usually works

In most ORMs, when you attempt to access the database, everything in that thread will block until a response is received. This means that you waste time — just waiting. The CPU may be idle, but you cannot handle any more requests. (Typically you start multiple instances of your application to get past this, unfortunately each instance requires more resources on your server)

How AsyncRecord works

When you access something in the database with AsyncRecord, the request is sent to the database server, but control returns to the application immediately after the packet(s) are sent. When the server responds, which could be 20ms or 200ms later, a callback that you specify is invoked.

One important thing about accessing a database asynchronously, especially in web frameworks, is the ability to defer a response. Fastr has built-in support for deferred responses, a-la EventMachine/Thin.

A deferred response is when you tell the web server that you will send data to the client some time in the future, and the server is free to handle more requests until you are ready to respond.

Benchmarking

As I was implementing AsyncRecord, I knew it would be faster — but I wasn’t sure by how much. I setup a very simple Rails 2.3.5 application, as well as a Fastr application (from the latest source).

My goal was to make an application that has a single page, which shows 5 usernames from the database.

Rails controller:

class MainController < ApplicationController
  def index
    users = []
    
    User.all(:limit => 5).each do |user|
      users << user.username
    end
    
    headers['Content-Type'] = 'text/plain'
    render(:text => users.join("\n"))
  end
end

Fastr controller:

class MainController < Fastr::Controller  
  def index
    defer_response(200, {"Content-Type" => "text/plain"}) do |response|
      
      User.all(:limit => 5) do |users|
        users.each do |user|
          response.send_data("#{user.username}\n")
        end
        response.succeed
      end

    end
  end
end

The Numbers

Fastr

Average Latency: 123ms Requests per second: 385 r/s

Rails

Average Latency: 2040ms Requests per second: 42 r/s

The tests were performed using JMeter. 100 concurrent requests (10 requests per connection).

I also ran some tests using apache bench, here are the results:

ab -c 100 -n 1000 http://127.0.0.1:5000/

Fastr

Average Latency: 90ms Requests per second: 1100 r/s

Rails

Average Latency: 2235ms Requests per second: 44 r/s

Conclusion

I am extremely happy with what AsyncRecord can do — and I hope to make it even better. I will be moving it out of Fastr and into its own project soon.

Fastr GitHub: http://github.com/chrismoos/fastr


comments powered by Disqus