How to get 4x the performance out of Heroku with Unicorn
This is the first of a two part series on how we set up Railsonfire with Heroku. The second part will deal with Assets, Sprites and Amazon Cloudfront.
This guide is only relevant and tested with the Heroku Cedar stack.
I have had several debates over the last couple of months whether Heroku is the way to go and especially if it is expensive or not. They provide a great service, but their documentation makes them look pretty bad when it comes to price.
$35 for basically one single concurrent request seems very expensive, especially when starting out with a new project (although of course the first dyno is free).
Heroku provides plenty of resources, but as it only allows to listen on one port you can run only one thin instance (as recommended by their documentation).
What we need here is a webserver that listens on one port, but can work through several concurrent requests. Sounds like a job for Unicorn.
What is Unicorn
Unicorn is a ruby http server that starts one master process listening on one port and forks several worker processes. Every incoming client request is handed to a worker by the master and when finished the master returns the result to the client. Thus it only needs to listen on one port, but can work on several concurrent requests.
Defunkt wrote a nice blogpost about unicorn some time ago that goes into more detail how GitHub uses it.
Setup
To start using Unicorn all you have to do is:
Add the Gem to your Gemfile
Create a Procfile
Add unicorn config in config/unicorn.rb
Set the default Logger in production.rb to STDOUT, otherwise logging doesn't work.
Unicorn config
You can find all config parameters in the unicorn documentation.
Let's go quickly through the configuration we use
worker_processes: Setting the number of worker processes
timeout: Time after which a worker is restarted if unresponsive
preload_app: Load the application before forking workers. Set to true if you use NewRelic (which you should) or you won't see any data
before_fork/after_fork: Disconnect in before_fork and reconnect in after_fork for your Database, Resque or other services. Without those handlers there will be regular database errors.
NewRelic
Go to the NewRelic Page of your Heroku application and check your dynos and memory consumption.
Average Memory Consumption
Check your Memory Consumption in NewRelic and set the worker_processes accordingly. The average consumption is shown on the Dyno tab of your heroku dashboard.
One Heroku Dyno has 512Mb of Memory, so make sure your combined workers do not exceed that maximum amount, or your dyno will be shut down.
On the right hand side of that same tab you can see the number of dynos. Make sure it is the same as you set in worker_processes.
Benchmarks
I ran several tests with ApacheBench to determine how much the performance improved.
I ran 1000 Requests with 100 concurrent connections against the landing page of our staging application. The following graph shows the time the requests took combined with 1-4 workers.
Going from one process to several increases performance drastically, from then on it is still a boost to your application, but not as drastically. However you have to find the right spot on how many workers you want unicorn to fork depending on your application. Having too many may shut down your dyno due to memory constraints.
Conclusion
So in closing using Unicorn as your Heroku Webserver not only pays off, but should be put into the Heroku documentation at least as advanced information.
I actually talked to people and showed them our Unicorn setup, which convinced them that Heroku is not as expensive as it seems and especially when starting your project is a very viable alternative to having your own Server. By starting up one more dyno you can have several more concurrent requests.
If you have any questions regarding the setup or anything else you can send an email to flo@railsonfire.com, a Tweet to @Railsonfire or use the Olark Chat Box in the right hand corner.
Thanks
This post is very much built on Michael van Rooijen's Blogpost. Gists that helped with the setup were by leshill and jamiew