jhollinger

code et al
jordan at jordanhollinger dot com
powered by eridu

Deploying with Thin

December 19, 2011 | nginx, thin

I've written on how to properly install and configure Thin, how to use Thin to run multi-threaded Rails apps, and even how to get Thin and Bundler to play nicely together. But I just realized that I've never discussed an actual deployment, complete with hookup to Nginx, how to cleanly restart the app, pros, cons, etc. I do not claim to be an expert; this is merely my experience. If you have suggestions, please let me know in the comments.

Firstly, this post assumes you will at this point go back and read (at least) the first link above. Secondly, this post is about using Thin behind Nginx. That appears to be the most common setup, and I like it.

Is Thin a good choice for production?

Sure. Here's one biased benchmark by the Thin guy, and here's another biased benchmark by a Passenger guy. At worst, it's on par with its competitors. At best, it blows them away. My experience is somewhere between the two. And it seems to use less RAM than Passenger. I use it for Rails, Sinatra, and a few straight-up Rack apps.

The most important piece is, however, what your app is doing during those requests. Only your code can prevent forest fires determine if Thin will get 10s, 100s, or 1000 reqs/sec.

But why should I use it instead of say, Passenger?

I'll admit that Passenger is a simpler set up. It takes care of setting up a reverse proxy from your Web server, clean app restarts, process monitoring, and starting and stopping new app instances in response to load. Why give all that up? For me, two reasons. 1) Too much RAM and 2) it didn't perform very well for large apps with critical yet small to medium load. Our primary Rails app was so huge it took 30-40 seconds to boot up. We eventually broke it into modular, engine-powered apps, but some of those still took 20-30 seconds. It may go hours without being used, then see a flury of activity, then a drop off, then another increase. Regardless of config options designed to help, Passenger would kill of all or some workers at low periods. When usage picked up again, for too many users would be stuck for ~30 seconds waiting for workers to spin back up.

Not a deal-breaker, but it became an itch in the middle of our backs that we couldn't reach. Eventually I began to wonder why Passenger couldn't handle more than one request at a time per-worker (e.g. multi-threading). It would eliminate this problem, be a much more efficient use of our resources, and I knew that Rails had the ability. But it is foreign to Passenger's philosophy. Thin on the other hand, has a threaded option, and it works very well. So we switched.

Following is the setup I used. It is more complicated, but I like the fine-grained control that it offers.

Restarting Thins

You'll want at a minimum two Thin instances running for your app. That way they can be restarted sequentially, meaning at least one is always running. Add the following to your app's Thin config:

servers: 2
onebyone: true

Now you (or your deployment script) can restart your app with thin restart -C /etc/thin/app.yml and your thins will be restarted one-by-one.

Thin socket config

Thin can communicate with the Web server using either tcp or sockets. Happily, Nginx can also communicate with an upstream server over either tcp or sockets. Since sockets are faster, we'll use them. Add the following to your Thin config file (remove the port setting if it's present):

socket: tmp/sockets/thin.sock

Nginx upstream server config

The docs are quite thorough, but I'll distill them a bit. In short, this tells Nginx to balance between the two Thins. If one Thin goes down (e.g. a one-by-one restart), requests will be routed to the other. This is a pretty barebones config, but it is functional.

upstream my_app {
  server unix:/var/www/my_app/tmp/sockets/thin.0.sock max_fails=1 fail_timeout=15s;
  server unix:/var/www/my_app/tmp/sockets/thin.1.sock max_fails=1 fail_timeout=15s;
}

server {
  listen 80;
  server_name my_app.com www.my_app.com;

  # An alias to your upstream app
  location @my_app {
    http://my_app;
    # Define what a "failure" is, so it can try the next server
    proxy_next_upstream error timeout http_502 http_503;
    # If the upstream server doesn't respond within n seconds, timeout
    proxy_read_timeout 60s;
  }

  # Set your doc root for static files
  root /var/www/my_app/public;

  # See if the request matches a static file. If not, pass it on to your app.
  try_files $uri @my_app;
}

For a an excellent explanation and example of a very robust Nginx setup, read calomel.org/nginx.html.

Things to watch out for

"Leaking" RAM

This is not unique to Thin, and in fact is a problem with your code itself. Poorly-written Ruby code can chew up tremendous amounts of RAM which are used for moments, then rarely released. (This isn't "leaking" per-say, but the effect is the same.) If unchecked, this can bring your system to its knees. A good rule of thumb in Ruby is, create as few objects as possible at a time. And remember - everything in Ruby is an object! If you're using Rails and ActiveRecord, look into the find_each method, which loads records in batches, rather than all at once.

Monitoring

Passenger has a master process that monitors, spawns, and kills its worker processes. Thin's design allows for no such system, so you may want to look into Monit. It can restart processes if they die, monitor their memory usage, and tons more.

Yeah, so like I said it's more complicated than Passenger. But as a result, I believe I have a faster, leaner and more transparent deployment system. I may add things occasionally as I think of them. Let me know about your experiences and if I can improve my setup in any way.

comments powered by Disqus