How to deploy a multi-threaded Rails app

Multi-threaded Rails apps have to be the best kept secret in the Rails community. “What do you mean? It’s been discussed at length for years!” Yes, the benefits, drawbacks, and limitations have been. But, JRuby aside, I have never ever seen someone saying “This is how you do it.”

It’s easy to find instructions for turning threaded mode on in Rails. Just comment out config.threadsafe! in config/initializers/production.rb . But that’s only half the story. Normally, Rails has a mutex around request handling, allowing only one at a time. threadsafe! removes that, telling Rails to handle requests concurrently as they come in. But your app server (Passenger, Mongrel, etc.) is what needs to send your app those concurrent requests in threads. Most never will.

Are you sure?

Go ahead, turn threadsafe! on in one of your apps. Then create an action called foo .

  ...
  def foo
    n = params[:n].to_i
    sleep n
    render :text => "I should have taken #{n} seconds!"
  end
  ..

Open a broswer window and pass it ?n=15. Quickly open another window and pass it ?n=2. Window 1 will render in 15 seconds. With config.threadsafe! on, you’d expect Window 2 to finish well before Window 1. But no, Window 2 takes 17 seconds, because it’s waiting on the first request, which is taking 15 seconds. You may have told Rails it’s safe to be threaded, but your app server isn’t threading .

An aside

I will note, as have many others, that Rails thread-safety means little if your gems are not thread-safe or are doing lots of blocking. You’ll have to deal with that on your own. My understanding is that the mysql2 gem used in Rails 3 took care of this for the average app.

Most Rails deployments can’t go multi-threaded, no matter what

Passenger and Unicorn definitely can’t. Most Mongrel deployments can’t or won’t (not sure which). These are all process-based request servers, like Apache, where a process handles only one request at a time. If you want to handle n concurrent requests, you need n copies of your app running. Rainbows! and Zbatery I believe have event-based concurrency, but that is not the same as multi-threading. So what does that leave us?

I got Thin

The only Rack server I’ve found that mentions multi-threading is Thin . It uses EventMachine to handle concurrent requests. Yay! Everything solved, right? Not quite. As is the case with Rainbows! and Zbatery, event-based processing is not the same as multi-threading, and buys config.threadsafed!‘d Rails apps nothing. But Thin does have a threaded mode which can be enabled by passing “—threaded” on the command line or by setting “threaded: true” in your YAML config file. That’s it! Try the above test now, and Window 2 will finish long before Window 1.

Follow this simple bug work-around if you’re using Ruby 1.9.2. Otherwise all your requests could take almost a minute to complete!

You also may want to read my post on Thin config and managment .

Is Thin the only way?

I hope not. If so, that means Rails has an incredibly powerful features that everyone’s excited about, but at the same time, that no one really cares about. Thin is the only multi-threaded Rack server on which I can find any meaningful discussion or documentation (and even that is scant). If you know of another, please let me and everyone else know!

Look into Nginx

To really take advantage of all these multi-threading and asynchronous goings-ons, you should look into dropping Apache and switching to Nginx. In fact if you’re trying to run as efficiently as possible on a VPS, I insist you look into it! There’s plenty out there, but I’d start with a good comparison of the two.

  1. Gary Watson says:

    My experience when testing thin in multi-threaded mode was that it's performance was terrible. This was a long time ago, so you should do your own testing, I was using apache bench.

    On the bright side, both mongrel and webrick are multi-threaded, so try apache-bench against them as well. Rainbows has a threaded mode too.

    allot has changed since I initially did my research. 1.9.3 is out now, and has some threading goodness that didn't exist before.

    But to answer your question about which ruby webservers are threaded, the ones I mentioned above are, (which is funny since they (with the exception of rainbows) are the oldest). I think it is because in many cases they were written before the threading issues in ruby were widely understood, (and now largely gone in all current versions of the major implementations including mri if you are on the 1.9 series [admittedly you have to pull tricks with 1.9.x like spinning up as many processes as you have cores to get 100% cpu utilization, but this isn't so terrible])

  2. Philippe says:

    Great article. Thank you.
    I was experimenting with this. It works fine when I direct my request directly to thin but thin is behind nginx there is no concurrency anymore. It's like nginx won't send more than 1 request to a thin socket at a time.
    Is there an nginx configuration option that must be enabled so that thin gets sent more than 1 requests at once?

  3. Jordan Hollinger says:

    @Philippe I just tired to duplicate your concurrency issue and wasn't able to. As far as I know, I haven't enabled any config options for this behavior.

    So while connection 1 is running, connection 2 just hangs?

  4. Philippe says:

    @Jordan, correct, the 2nd connection hangs if I go through nginx.

    When I was experimenting a couple of weeks ago, I had this in my nginx config:
    upstream thin_cluster {
    server unix:/tmp/uupdates.0.sock;
    }

    I'm wondering if this is right, how does nginx know that it can push more than 1 request at the same time on the same socket? Should there be more than 1 server line in that block?

  5. Jordan Hollinger says:

    @Philippe That looks similar to my config. My understanding is that Nginx sends requests to the server whenever they come in. It is up the app receiving those requests whether it handles them synchrynously or asynchronously.

    You shouldn't need more than one server line unless you have multiple upstream servers (for high load). I have noticed that idential requests from the same browser aren't always handled async, but that happens regardless of Nginx.

Post a comment


(lesstile enabled - surround code blocks with ---)