Quantcast
skip navigation
Blog About Archives Careers Twitter RSS

Twitter

Rails 3 Performance - Not Good Enough

05/11/2011, 8:53am (CDT)
By Luke Ludwig

A simple reproducible benchmark shows ActiveRecord 3.0.7 to be 1.43 times slower than ActiveRecord 2.3.2.

At TST Media we upgraded our NGIN application from Rails 2.3.2 to Rails 3.0.5 on April 6th. Unfortunately, we immediately saw the average response time of our application double. NGIN running on Rails 2 had an average response time of 300 ms per request with a throughput of around 1750 requests per minute. Running on Rails 3 with similar throughput levels, the average response time was around 650 ms per request. We have since reduced this down to just under 500 ms per request by tuning our garbage collection settings through Ruby Enterprise Edition (REE), the details of which deserve a separate blog post.


Rails 2. April 5th, 8am-11am


Rails 3. April 6th, 8am - 11am

Amount and Type of Traffic

Comparing average response times between days can be tricky since there are many variables. The first one to consider is the amount and type of traffic. As traffic increases, average response time typically increases as well. The following charts show the throughput in requests per minute to be very similar. Throughput levels were around 1500 requests per minute (rpm) at 8 am and rose to 2000 rpm on the 5th, whereas on the 6th it only reached 1800 rpm by 11 am. So the amount of traffic was slightly less on April 6th. The type of traffic NGIN receives can also account for major changes in response time. We carefully considered this as a possibility, but we found there to be no significant change in the type of traffic received.


Rails 2 throughput, April 5th, 8am - 11am


Rails 3 throughput, April 6th, 8am - 11am

Investigating the Performance of Specific Actions

The next thing we looked at is whether or not the change in performance was due to a single feature or a single action. Sometimes a single action can skew an application's average response time. Using New Relic's Web Transactions tab we were able to compare the performance of several key actions within NGIN. The table below shows performance data for four of the more frequently called NGIN requests. It is clear that the performance of each of these was significantly impacted with the upgrade to Rails 3. The performance difference did not appear to be due to a single action, but instead was affecting everything.

Performance of Key Actions

Action Rails 3 Average Response Time Rails 2 Average Response Time Rails 3 Calls Per Minute Rails 2 Calls Per Minute
page/show 740 ms 436 ms 723 cpm 775 cpm
news_article/show 1356 ms 802 ms 59 cpm 57 cpm
roster_player/show 1896 ms 1131 ms 38 cpm 45 cpm
game/show 1321 ms 608 ms 50 cpm 46 cpm

Performance Impact Due to a Gem or Plugin?

Upgrading to Rails 3 was much more than just upgrading Rails. NGIN depends on 60 gems and 9 plugins. Most of these gems and plugins were upgraded during this process as well. With this in mind we were hesitant to immediately blame Rails 3 itself for the performance degradation. However, considering the points made above that show that the performance of everything has degraded, not just a single feature-set, a quick scan of our gems and plugins turned up only two gems that could possibly affect the performance of NGIN across the board. Those gems are mysql2 and multi_db. On Rails 2 we used the mysql gem and during the upgrade to Rails 3 we switched to the mysql2 gem. We use multi_db to spread our sql read requests out to multiple slave databases, and we upgraded the multi_db gem ourselves to work with Rails 3. At this point I was farely confident the performance degradation was due to either Rails 3, multi_db, or mysql2, and to determine which one would require getting my hands dirty and running some simple benchmarks.

Why is No One Else Complaining About Rails 3 Performance?

I'm not sure why the performance of Rails 3 is not a hotter subject. Initially Rails 3 ActiveRecord was 5 times slower than Rails 2, but with some nice work by Aaron Patterson (tenderlove) on optimizing AREL, this difference was improved greatly.

Otherwise the only other relevant post I've found, by Bill Harding, had similar results as NGIN, meaning Rails 3 was twice as slow as Rails 2 until tweaking REE's garbage collection settings.

A Simple Benchmark Points to ActiveRecord

At this point I decided to finally dive in and run some basic benchmarks. The simple benchmark below loads 10,000 User ActiveRecord objects from the database.

Rails 2:
>> Benchmark.measure { 10000.times { |i| u = User.find_by_id(i); u.user_name if u } }
=> #<Benchmark::Tms:0x2ab707b087c0 @label="", @stime=0.47, @total=4.66, @real=6.06405091285706, @utime=4.19, @cstime=0.0, @cutime=0.0>

With Rails 2 it takes about 6 seconds to load the 10,000 user objects. I am saving the user off in a local variable and accessing the user_name field for consistency. This is necessary for the Rails 3 benchmark to guarantee that the database is getting hit by the User.where call, which would otherwise not execute the sql query due to AREL's lazy sql execution.

Running the same benchmark with Rails 3:

Rails 3:
>> Benchmark.measure { 10000.times { |i| u = User.find_by_id(i); u.user_name if u } }
=> #<Benchmark::Tms:0x2aaaacc2d630 @cutime=0.0, @label="", @stime=0.75, @real=12.5903899669647, @utime=10.63, @total=11.38, @cstime=0.0>

The difference here is astounding! With Rails 2 it takes around 6 seconds to load 10,000 User objects, and with Rails 3 it takes around 12.6 seconds, over twice as long! Interestingly, the preferred where syntax with Rails 3 is slightly faster, coming in at 11.5 seconds, which is still roughly twice as slow as Rails 2.

Rails 3, using preferred "where" syntax:
>> Benchmark.measure { 10000.times { |i| u = User.where(:id => i).first; u.user_name if u } }
=> #<Benchmark::Tms:0x2aaaad25bd18 @cutime=0.0, @label="", @stime=0.550000000000001, @real=11.4192109107971, @utime=9.79, @total=10.34, @cstime=0.0>

The performance difference with this simple benchmark, Rails 3 being twice as slow as Rails 2, corresponds very closely to the performance difference we saw with NGIN on upgrading to Rails 3.

To make sure that the change from the mysql gem to mysql2 did not account for the performance degradation, I ran this same simple benchmark with the mysql gem instead of mysql2 gem. There was not a noticeable performance difference. I did the same for multi_db, which also did not have a noticeable performance difference with this simple benchmark.

Reducing the Problem Further

Simplifying a problem down to only what is needed is an extremely useful technique. Getting rid of all the cruft that is not necessary to demonstrate a problem is useful in understanding the root cause of an issue, and is an excellent way of creating something that can be reproduced by anyone.

In this case, the cruft around my simple benchmark above is the NGIN codebase itself. I created two new rails projects, one for Rails 2 and one for Rails 3, which includes an empty User model and some migrations to create the User table and populate the User table with 10,000 users. Clone this repository and follow the steps in the README to run this benchmark yourself: https://github.com/tstmedia/simple_benchmark

Interestingly, running this simple benchmark outside of NGIN in a clean rails project had significantly different results. Rails 2 loaded 10,000 User objects in 3.7 seconds, and Rails 3 took 5.3 seconds.

Rails 2:
$ env RAILS_ENV=production rails2/script/performance/benchmarker "10000.times { |i| u = User.find_by_id(i); u.user_name if u }"
            user     system      total        real
#1      2.810000   0.300000   3.110000 (  3.765559)
Rails 3:
$ env RAILS_ENV=production rails3/script/rails benchmarker "10000.times { |i| u = User.where(:id => i).first; u.user_name if u }"
            user     system      total        real
#1      4.290000   0.380000   4.670000 (  5.383205)

So in this case, Rails 3 is 1.43 times slower than Rails 2. While still significantly slower than Rails 2, it is not as bad as the 1.88 times slower when run within the NGIN codebase. I have yet to figure out what is causing this difference, but I expect it is a combination of things instead of a single culprit.

All of the above benchmarks were ran on our staging environment, using REE 2011.03, which is Ruby 1.8.7 patchlevel 334, and MySQL 5.1.55. Interestingly, when I ran the simple benchmark outside of the NGIN environment on my MacBook, Rails 3 was only 1.2 times slower than Rails 2.

At this point it is clear that the blame for the performance degradation goes to ActiveRecord. In a real-world application, ActiveRecord 3.0.5 is twice as slow as ActiveRecord 2.3.2. In a simple benchmark within a clean rails framework it is 1.43 times slower. Clearly the benefits of Rails 3 pale in comparison to this major performance difference. If you are considering upgrading to Rails 3, I would suggest waiting. 

Comments

Tony ·

Great writeup, Luke.

My only issue is with your statement:

"Clearly the benefits of Rails 3 pale in comparison to this major performance difference"

Really? That's quite a blanket statement. I'd say it depends entirely on your circumstances. If you absolutely can't compromise on speed, then yeah, it makes sense to not upgrade. Then again, Rails 3 might give you other benefits that would outweigh the performance issue.

Luke Ludwig ·

Tony,

Sure, I agree that it depends on your circumstances. There are several great features in Rails 3 that make programmer's lives easier, such as bundler and AREL, and some great rewrites of the underlying architecture (routing, mailers, etc.). Most of this stuff is great and makes programmers like myself happy, but has no affect on end users. All of this great abstraction has apparently come at a price in performance.

Application response time is very important and directly
Read More




Processing…

Tony ·

I should also add, I'd be curious to see the test with the entire Rails stack removed, and done with just ActiveRecord.




Processing…

Heinrich Lee Yu ·

How about using ruby 1.9.2?




Processing…

Michael A. ·

Just as a point of interest, there were some performance regression fixes for ActiveRecord in rails 3.0.7. No idea whether it would affect the results you're seeing.

Luke Ludwig ·

Michael A.,

I have tried ActiveRecord 3.0.7 and I did not see any improvement.

Luke




Processing…

Joe Van Dyk ·

I'm working on converting my 2.3 app to 3.0. I'm sad seeing my test suite take 50% to run.

Ruby 1.9.2 + rails 3 has some slow startup problems, btw. Supposed to be fixed in 1.9.3, but that won't be put for a while.




Processing…

bt ·

@heinrich, if he's using Ruby Enterprise Edition, then his version of ruby 1.8.7 is probably already faster than 1.9.2. My Real Estate app, which has uncached views, benchmarked faster in REE than 1.9.2. I'm not using Active Record.




Processing…

Guillermo ·

Can you test it with Ruby 1.9.2?

I've see a lot of bug reports about performance of Rails 3 under Ruby 1.8.7




Processing…

Daniel Parks ·

I posted this on Hacker News (http://news.ycombinator.com/item?id=2549240), but perhaps it makes more sense to post it here:

The benchmark was done using REE. Out of curiousity, I tried it with MRI 1.8.7, MRI 1.9.2, and REE 1.8.7. Results are best out of three:

user system total real
rails3-1.9.2 2.240000 0.200000 2.440000 ( 3.127072)
rails2-ree 2.530000 0.2900
Read More




Processing…

Aaron T. ·

I have to echo the sentiment about trying with Ruby 1.9.2.

We upgraded a very large rails app from 2.3.5 with REE1.8.7 to Rails 3.0.5 with MRI1.9.2, and and saw a significant *decrease* in our page latency (from approximately 300ms to 180ms using new_relic). Honestly I don't have numbers to back this up, its entirely anecdotal on my part because we didn't research the performance improvement into this detail.

It also might explain why the OP hasn't seen more outcry on this i
Read More




Processing…

brianmario ·

I'd be very curious to see what the performance characteristics are of Rails 3.1 beta1/master - if you have some time would you mind giving that a try?




Processing…

Michael Campbell ·

It would be interesting to see this done with various ruby's; specifically jruby after the jvm JITter got warmed up.




Processing…

Brian Hogan ·

You have a great piece of work here. Now, I'm pretty much one of the biggest Rails fans out there, and I've been pretty unhappy with the performance of Rails 3. But you derailed your own train wit the "Clearly the benefits of Rails 3 pale in comparison to this major performance difference. If you are considering upgrading to Rails 3." statement.

The changes to the ORM allow us to do much more complex queries without writing SQL. I have several apps that won't benefit from thes
Read More




Processing…

Ezequiel ·

Very interesting article, but Luke, Rails 3 is not equal to Active Record




Processing…

Chris Gill ·

my money says the slowness is almost certainly because of arel.

having a DSL to build a SQL AST in slow-@#$% ruby (including allocating tons of new objects and the associated GC hit), to generate a string of SQL, to be sent over the wire, to be turned back into an AST and executed by the database is...stupid. that's great that it makes SQL so much easier for people who don't know SQL, but in my mind that is the completely wrong thing to be optimizing. optimize teaching SQL instead. Read More




Processing…

Jan Faber ·

Could it be that one of your 90 gems in NGIN is injecting something into Active Record? If you are only seeing 1.2 slower performance on a clean system it doesn't seem unlikely that there is a problem there.

Luke Ludwig ·

Jan,

This is a definite possibility and something I've considered. We are actually seeing 1.4 times slower performance on a clean system. I've looked through NGIN's gem dependencies with this thought in mind but haven't found anything yet. I plan on going back through our gems thoroughly soon.

Luke




Processing…

Gleb Arshinov (Acunote) ·

Let me explain why you are getting worse slowdown within the NGIN codebase than in a standalone test.

The difference is that each garbage collection takes much longer within a large codebase. And I have worse news for you. When you run this within your app server in production it will take even longer. That's because you now have even more code loaded -- unicorn/mongrel, New Relic, etc.

You should benchmark it, but if your app is anything like Acunote a single GC run should b
Read More




Processing…

Leif ·

Why did you upgrade before knowing the performance impacts ? Why not test, benchmark then decide not to upgrade.

Luke Ludwig ·

Leif,

That is an easy question to ask in hindsight! While I could have done some benchmarking of a clean rails app with Rails 3, the real test is how it behaves on a real application like NGIN when running live. By writing this post I hope to make more people aware of the performance degradation when they choose to upgrade.

Luke




Processing…

Lonny Eachus ·

I would not be surprised if some people thought I was nitpicking here, but I'm not. If you are going to do something like a semi-scientific measurement of performance, you should get your terminology right.

In particular, the phrase "twice as slow" has no clear meaning. It is possible to measure how fast something is, but not how "slow" it is. Slow is an imprecise term that is only useful in a relative sense.

"Half as fast" is a technically accurat
Read More




Processing…

Luke Ludwig ·

I tried the simple benchmark, loading 10,000 ActiveRecord objects, within the clean rails app using Ruby 1.9.2.... something several of you were asking about. Rails 3 using Ruby 1.9.2 is 1.52 times slower than Rails 2 using Ruby 1.9.2. Here are the results:

Rails 3 with 1.9.2: 3.97 seconds
Rails 2 with 1.9.2: 2.61 seconds

For comparison, here are the results with Ruby 1.8.7 (REE 2011.03) as detailed in the post above:

Rails 3 with 1.8.7: 5.3 seconds
Rails
Read More




Processing…

Ryan Heneise ·

I've had a very similar experience. I struggled for a week with Rails 3 on REE. The performance was absolutely abysmal. Finally, I found that Rails 3 with Ruby 1.9.2 is an order of magnitude faster. Sorry I don't have any benchmarks to share. However, here are my average request times (approximately):

Rails 2.3.11/Ruby 1.8 400ms
Rails 3.0.7/REE 900ms
Rails 3.0.7/Ruby 1.9.2 250ms

HOWEVER! Ruby 1.9 is incredibly slow to start. Every time I deploy it takes ou
Read More




Processing…

Jared Mehle ·

I am attending RailsConf this week and Aaron Patterson's keynote was very relevant to this post. Long story short is the Rails stack in version 3 got a lot bigger than Rails 2 and thus is causing issues with garbage collection. He proposed a solution that if accepted would likely be part of Rails 3.2

At some point the video will be live on the RailsCofn website. I highly recommend watching it.
Read More

Luke Ludwig ·

Jared... that is great to hear that a potential solution is in the works. I look forward to seeing the keynote.




Processing…

Lawrence Pit ·

> At this point it is clear that the blame for the performance degradation goes to ActiveRecord.

ActiveRecord depends on ActiveSupport. I found that ActionMailer in rails3 is more than 10 times slower compared to rails2, and that doesn't use ActiveRecord, but does use ActiveSupport. Not blaming anything yet... ;p




Processing…

Ritesh Agrawal ·

Great post. I also notice some performance issue with rails 3.1.0 + ruby 1.9.2 + thin server. In general, the system took 40% more time as compared to my previous setup which as ruby 1.8.7 + rails 2.1.1 + mongrel. However I can't really assert whether the culprit is ruby, rails, thin or some combination of all




Processing…
Add a New Comment



Processing…

Tag(s): Development