Rails Performance Needs an Overhaul
igvita.com
igvita.com
I get a metric truckload more than $2k of developer productivity out of using Rails instead of Java, though.
If my traffic increased such that I needed 40 Mongrels and 20 Delayed Job workers and I was spending $20k a year on hosting, I'd... I'd... I'd really, really like if that happened, actually.
As much as the extra $2k of hosting is worth the productivity boost, wouldn't you love it if you could have the Rails productivity and save that $2k/year compared to current Rails performance?
Also, development of Ruby + Glassfish almost always meant Rails + Glassfish. I prefer Ramaze, and had to hack a few things (in warbler, for example, and the glassfish3 gem) to get around some things that that assumed you were using Rails.
If you are using one of the anointed frameworks (i.e. Rails or Sinatra) you're probably OK. If you are coloring outside the lines you may have issues. But I've not looked into it much since I got my site running (though now I'm just running it as a rackup app through jruby)
At this point though I'm looking at using Haskell when speed is a concern. But that's a whole other thread ...
See discussion here: http://news.ycombinator.com/item?id=1380732
And if they do, why not use the Ruby app of choice under JRuby? Pretty much all of the dozen+ Ruby Web frameworks can be bundled up as WAR files and dropped off for Glassfish (or tomcat or whatever) to handle.
Almost missed seeing the Snap and Play stats because the preformatted text was getting formated into a side-scrolling pane.
Not that I want to code in Java, but the speed is impressive.
Still, I have to wonder what the JRuby speed would be for some of the other Ruby frameworks that are faster than Rails, such as Ramaze and Sinatra. (I doubt they would beat those Play numbers, but they would still do better than Rails. A Duby version of Ramaze might rock.)
Maybe if they open-sourced this "Goliath" framework they created we'd be a little closer.
Instead of complain about how slow it is, why not attempt making an "evented" version of Rails? Enable thread-safety for Rails to get a speedy application, even on a shared server.
Frankly, I can't write code for EventMachine. Just looking at its style makes me cringe: there's just something about the design of EM apps that I can't compromise with (and it ain't the fact that it's evented).
It's a start.
As for Goliath - we will. Time is the scarcest resource in a startup, and that's why haven't opened it up already. We need to get some documentation in place, and remove a few dependencies.
Point is though, there is nothing inherently novel in our stack! All off the shelf pieces: thin, async-rank, async middleware, em drivers + em-synchrony.
But in the context of this post, it just came off as a little bit "holier-than-thou" to hear you say more-or-less: jeez everybody, what's your problem? We've solved this so why is it taking everyone else so long to catch up?
Now I believe you didn't mean it that way, but it seems like a helpful way to push things forward is to release bits and pieces of what you have, or at least write up a bit more detailed descriptions of the various pieces. That would be very helpful :)
I really like the idea of using Ruby's continuations to avoid the need to write all of your logic in CPS (ala node.js.)
From the memory standpoint, doesn't this just move your memory overhead from the machine stack to continuations on the heap? I do realize there are some advantages to heap over stack, especially on 32-bit architectures.
And in addition, what kind of overhead do you get when creating a continuation for the kind of deep call stacks you're dealing with?
That said continuations don't necessarily involve stack copying. The implementations of continuations that don't do make normal procedure calls slower as far as I know.
So while I think you're correct, pragmatically I suspect this approach has more chance of being implemented.
And it appears that Ilya (the OP) has been looking at this approach using fibers too http://www.igvita.com/2010/03/22/untangling-evented-code-wit... - and the comment thread on the OP suggests that this is in use at Postrank.
Coroutines don't require copying anywhere. Creating a coroutine can be done in a couple of instructions (allocate a new stack and set the top of the stack to the code of the coroutine), and yielding is also just a few instructions (mov a new stack pointer and return).
Essentially if you view coroutines and continuations as data structures then continuations are a functional / non destructive version of coroutines, and like other functional data structures they are slower.
I don't know how Ruby fibers are implemented but they seem to have coroutine semantics, so they are probably very efficient as well.
Adapting a callback library to coroutines is very simple. You have a user coroutine that runs the user code, and you have a system coroutine that manages the resource that calls you back when it's ready (e.g. downloading a web page). When the user code requests something from the resource it yields to the system coroutine. When the system has the response ready the system yields to the user code again.
This is nearly the same for continuations, the difference is that yielding involves copying the stack of the continuation that is yielded to.
The catch is that it spends 96% of it's time waiting for network responses. 10ms in the application code, 289ms waiting.
So now I either re-write the application in a language more suited to the task (node.js) or I work to build async capability into the current app. Personally, I'll take the second if it's not stupidly hard.
In my case the performance issues really have nothing to do with Rails and everything to do with blocking I/O. Rails 3 and Ruby 1.9 may be faster, but that doesn't help you at all if you do something like this:
class ExampleController < ActionController::Base
def index
sleep(1) # contrived example, imagine a slow API call here
render :text => "Completed!"
end
end
Because Rails is setup to be run in a blocking fashion my example will only be able to serve 1 request per second. The fix here is non-blocking languages (like Node.js) or programming in an event-driven fashion (by using EventMachine or the like). For example (in pseudocode): when( request.received ) do
register slow_api_request, :callback => :answer
end
def answer do
response.send( api_result )
end
Ideally the framework should do this by itself. Standard libraries that are blocking, like Net:Http, should continue to appear to the developer like they are blocking, but in reality would pass back control to other instructions while they're just sitting around waiting for the remote server to respond.This is totally possible, and getting more feasible by the day. Thin (the application server) and Rack both allow applications to run asynchronously, Neverblock provides a cleaner way for providing pools of Ruby Fibers to an application to allow implicit scheduling, in time Rails will hopefully build upon this. I've spent several hours this morning doing research into how I can fix this problem for my application, some of the stuff I've dug-up is here:
http://github.com/igrigorik/em-http-request
http://github.com/rkh/async-rack
http://www.espace.com.eg/neverblock/
http://macournoyer.com/blog/2009/06/04/pusher-and-async-with-thin/
It looks like the solution in my case is going to be something like creating a Rails Metal endpoint for Rack, that completes the Net:Http requests using em-http-request. I'm not sure that this is actually possible yet, but it'll be something close to that.I am able to cache the results (and I already am) but I don't want to keep the cache too long (it's setup to expire after 30 minutes at the moment and that's as high as I would like to go). I'm running on Heroku (which uses Varnish for HTTP cacheing) so once the cache is populated I can handle tons of traffic.
Ideally I would like to combine non-blocking HTTP requests with HTTP cacheing. I think the final goal should be the capacity to handle around 50 uncached requests per second.
If you're curious, the application is: http://ajax-gist.com/ And the source code is here: http://github.com/aarongough/ajax-gist
Why not just handle expensive parts of one's app with a simple rack handler? Do you really need form helpers, etc. in every part of the app?
If the whole thing were handled asynchronously though you would be able to handle dozens of requests (if not hundreds) in the same time that blocking NET:Http.get_request would take to complete.
But when it comes down to it, hacking Rails to do async requests is going to teach me a lot about how Rack and Thin work, so that's why I'm continuing to push in that direction.