This is the second in our series of Rails Performance blog posts, focusing on the Ruby garbage collector. In case you're wondering if this one results in better performance, I present Exhibit A: Our average response time over the last month:
Important Notes: For these tips to work, you need to be using Ruby Enterprise Edition. I believe similar results are available with Ruby 1.9.x, but we haven't tried that out yet. As with all tuning advice, the benefit you see from these changes will vary depending on the specifics of your app, e.g. the change will be much more dramatic for big Rails apps than small Sinatra apps. On with the show...
Ruby is a "Garbage Collected" language, which is one of its many features that results in improved programmer productivity. But GC doesn't make memory management just go away - it merely makes it automatic, and like many automated things it needs tuning by a meat puppet human to achieve optimal performance.
Out of the box, the Ruby Garbage collector is... unambitious. It's extremely well optimised to print "Hello World" and quit. For Ruby on Rails-based apps, which often demand hundreds of megabytes of RAM per process, the Ruby default GC settings leave a lot to be desired.
- Ruby preallocates a small amount of "heap" for dynamic data.
- This heap is periodically grown in response to memory allocation requests by a certain amount. By default, each additional chunk of growth is bigger than the ones before by a factor of 1.5x, multiplied by some preset growth increment.
But how do you go about tuning your GC for best performance? In abstract terms, this requires figuring out how much the minimum memory should be for your Passenger processes and having Ruby pre-allocate everything you already know you need, and then having it grow in ways that reflect the normal distribution of memory use across all your various web requests.
If this sounds complicated, then the good news is this: the default settings are so bad, almost any settings you put in are going to be an improvement. So just try what we have:
export RUBY_HEAP_MIN_SLOTS=600000 export RUBY_GC_MALLOC_LIMIT=59000000 export RUBY_HEAP_FREE_MIN=100000
(check out REE's GC performance tuning page for more examples)
Where do you put these? The easiest place is in an executable script which we will invoke instead of the Ruby intepreter itself. If you're using the Passenger plugin for Apache, it means you look for a line like this in your httpd.conf:
and replace it with this:
Then you need to create the "ruby_optimised" script, which sets those environment variables and calls your original Ruby VM:
exec "/Users/dan/.rvm/wrappers/ree-1.8.7-2011.03@nbuild/ruby" "$@"
You will want to "chmod a+x ruby_optimised" so that your Apache process can execute this script.
Et voila! In production, we immediately saw our GC usage drop from ~50% of request time down to less than 25%, which shaved about 60 - 80ms off our average request time, depending on various other factors (e.g. our cache hit ratio).
If your Rails app is big like ours, tuning your Ruby VM's Garbage Collector is probably the best possible bang-for-buck you can get in terms of raw performance. Let us know how you go tuning your GC in the comments!