Last Updated: December 30, 2020
·
36.58K
· dommmel

1600% faster app requests with Rails on Heroku

Using rainbows! and em-http-request resulted in a 1600% performance increase compared to using unicorn and net_http.
(For the use case described below)

Situation

  • You have an app that does a lot of calls to a 3rd party API. This is often the case with facebook apps for example.
  • You want to host it on heroku (one dyno preferred, so it's free)

Problem

  • You want to call the 3rd party API from your controller and render the response
  • Even if you are using unicorn to forks several worker processes, they will be blocked and waiting for a response from Facebook (or any other potentially slow API) most of the time.

Solution: Use EventMachine and Rainbows!

Here is a quick outline on how to get you up and running with Rainbows! and em-http-requests on heroku.

We will be using the following gems:
Gemfile

gem 'rainbows'
gem 'em-http-request'
gem "faraday"

app/controllers/async_controller.rb

class AsyncController < ApplicationController
  def index
    conn = Faraday.new "http://slowapi.com" do |con|
      con.adapter :em_http
    end
    resp = conn.get '/delay/1'
    resp.on_complete {
      @res = resp.body 
      render
      request.env['async.callback'].call(response)
    }
    throw :async
end

app/views/async/index.html.erb

<%= @res %>

config/rainbows.rb

worker_processes 3
timeout 30
preload_app true

Rainbows! do
  use :EventMachine
end

before_fork do |server, worker|
  # Replace with MongoDB or whatever
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end
end

after_fork do |server, worker|
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end
end

config/routes.rb

match 'async_test' =>   'async#index'

Procfile

web: bundle exec rainbows -p $PORT -c ./config/rainbows.rb

it is also a good idea to set config.threadsafe! in your Rails environment configuration

Finalize

  • Deploy to heroku
  • Run a benchmark, e.g. the following

    ab -n 600 -c 200 http://mycoolasync.herokuapp.com/async_test

  • Enjoy how fast it is

Some results

I set up a test app on heroku and ran
ab -n 600 -c 200 http://mycoolasync.herokuapp.com/async_test

Result:

  • Old-school method: 183 second
  • Evented method: 11 seconds

Detailed results for old-school synchronous requests

Using non-evented http request by settting con.adapter :net_http in app/controllers/async_controller.rb

Document Path:          /async_test
Document Length:        2755 bytes

Concurrency Level:      100
Time taken for tests:   183.373 seconds
Complete requests:      600
Write errors:           0
Non-2xx responses:      516
Total transferred:      652495 bytes
HTML transferred:       541903 bytes
Requests per second:    3.27 [#/sec] (mean)
Time per request:       30562.206 [ms] (mean)
Time per request:       305.622 [ms] (mean, across all concurrent requests)
Transfer rate:          3.47 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      101  175 260.7    109    1239
Processing:  1270 28308 5571.9  30115   30978
Waiting:     1146 28156 5882.4  30115   30665
Total:       1376 28484 5582.8  30224   32203

Detailed Results for new and fancy evented request method

Ensured by settting con.adapter :em_http in app/controllers/async_controller.rb

Document Path:          /async_test
Document Length:        2758 bytes

Concurrency Level:      100
Time taken for tests:   11.118 seconds
Complete requests:      600
Write errors:           0
Total transferred:      1734330 bytes
HTML transferred:       1655686 bytes
Requests per second:    53.97 [#/sec] (mean)
Time per request:       1853.048 [ms] (mean)
Time per request:       18.530 [ms] (mean, across all concurrent requests)
Transfer rate:          152.33 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:      101  194 283.0    114    1263
Processing:  1136 1485 416.1   1341    3558
Waiting:     1135 1453 346.2   1339    3159
Total:       1250 1679 472.5   1505    3669

Some final words

I wanted to have a link-baity title once. So here we are with a very unscientific 1600%. You'll be the judge - I think with the right test setting this number could be much much higher. If you replicated this and achieved a better result - let me know. I'll update this post and give you credit.

Cheers,
your friend
dommmel

7 Responses
Add your response

Using throw :async in a controller action makes me really sad :(
But that's not the point of this tip, sorry...I... couldn't resist.

over 1 year ago ·

This website looks amazing on an iPad. Well done.

over 1 year ago ·

@mattetti Thanks for pointing it out. Would love to know how you would go about it.

From the top of my head: One could use Rack::FiberPool and EM-Synchrony like so

Gemfile

gem 'rack-fiber_pool', :require => 'rack/fiber_pool'
gem 'em-synchrony'

config.ru

...
use Rack::FiberPool
run MyApp::Application

app/controllers/asycn_controller.rb

conn = Faraday::Connection.new(:url => 'http://slowapi.com') do |builder|
   builder.use Faraday::Adapter::EMSynchrony
end

resp = conn.get '/delay/1'
@res = resp.body 
render
over 1 year ago ·

Can you explain this line?
request.env['async.callback'].call(response)

Where are request and response defined?

over 1 year ago ·

@barapa Yeah - that bit is confusing. I am by no way an expert on this kind of stuff - maybe others can chime in and provide more insightful comments than me - anyway here's my answer

response and request objects

The two accessor methods response and request exist in every
Rails controller. Some details can be found in this section of the rails guides.

async.callback

The async.callback is part of a scheme that was first implemented in thin afaik (see this blog post) and has since found its way into some other server software, including Rainsbows!/EventMachine.

over 1 year ago ·

you are my hero <3

over 1 year ago ·

this website is not working....http://mycoolasync.herokuapp.com/async_test

over 1 year ago ·