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