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
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
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.
| 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 |
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.
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.
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.
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.
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
I should also add, I'd be curious to see the test with the entire Rails stack removed, and done with just ActiveRecord.
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.
Michael A.,
I have tried ActiveRecord 3.0.7 and I did not see any improvement.
Luke
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.
@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.
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
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
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
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?
It would be interesting to see this done with various ruby's; specifically jruby after the jvm JITter got warmed up.
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
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
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.
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
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
Why did you upgrade before knowing the performance impacts ? Why not test, benchmark then decide not to upgrade.
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
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
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
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
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
Jared... that is great to hear that a potential solution is in the works. I look forward to seeing the keynote.
> 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
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
Tag(s): Development
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.
Reply