In my recent post detailing my impressions of Go (the language), I took the following swipe at Rails:
Independent of ruby, I see Rails as the emperor with no clothes on. A subject for another post, but I will try my damndest to steer clear of it in the future.
I was called out on Hacker News for being so vague, and rightly so. This post is my attempt to be fair, objective, and, by consequence, unrelentingly negative about Rails :)
Of course I don’t literally mean what I’m saying in the title of this post. Rails has its strengths and makes it easy for a total newcomer to glue things together and build an interactive webapp. The reason I think Rails is so dangerous is that it’s like balsa wood: very pleasant to work with, but ultimately not solid, and – unlike a service written in any number of other frameworks – tangible performance concerns appear when there are tens of thousands of users, not tens of millions of users. That is, even small services run into performance problems if they put pressure on the framework (e.g., services for social content or other domains that involve a lot of churn or interactivity).
Edit: this post is getting more attention that I was expecting – great! However, people are not parsing the argument on HN (shocking!), so I will lay it out more succinctly.
- Do you need performance? If so, don’t use Rails.
- You don’t need performance. Do you need to maintain this software long-term? If so, don’t use Rails.
- You don’t need performance and you don’t need to maintain your software. There are countless other web frameworks written on top of dynamic languages which offer comparable benefits to Rails, yet have favorable performance profiles and a better culture of compatibility. Start there.
Well-Intentioned Developer: “I need to develop a webapp and/or a REST API, and everyone and their dog seems to use Rails. I should, too, right?”
No; Rails is fundamentally – and catastrophically – slow.
This well-known set of webapp benchmarks shows that, for a single-database-query workload, Rails offers less than 1/20th the throughput of, say, Go, 25 times the average latency of Go, and more than 6 times the latency variance of Go. And Go isn’t even the most responsive or highest-throughput of the available options. The story is even more alarming for elemental workloads that focus exclusively on the framework internals.
And it’s no wonder. Until Rails 4, the core framework didn’t enable concurrent request handling by default. And even in Rails 4, concurrent request handling is multiplexed on a single CPU core, leaving Rails unable to take advantage of the parallelism of modern architectures. Don’t mistake concurrency for parallelism: any web framework that cannot take advantage of multiple CPU cores will suffer greatly from a throughput standpoint.
At this point it would be reasonable to bring up JRuby. I should admit at this point that I’ve never used JRuby seriously, as the Rails apps I worked on had dependencies on C libraries which of course eliminated JRuby. Nevertheless, a few thoughts:
- Deploying JRuby means entering the dark world of JVM tuning: one of the reasons people want to use frameworks like Rails is to get away from JVM tuning, so it’s unfortunate to drag it back into the picture.
- That said, JRuby sometimes wildly outperforms cRuby on CPU benchmarks… yet it also sometimes wildly underperforms cRuby for those same benchmarks: read more here.
- Although JRuby does allow for parallel processing of requests across CPUs, some gems often (unfortunately) still make the simplifying assumption that there’s only ever a single extant request in the Rails process, and thus they are not safe for use in a concurrent environment. One can avoid those gems once identified, but my experience was to find out about them the hard way. Hopefully Rails 4’s concurrent-by-default mode will put more pressure on gem maintainers, but this is a rocky place to start from. The Rails community hasn’t been thinking about concurrency (much less parallelism) for many years running, and it’s a difficult cultural corner to turn.
Independent of JRuby, the best way to make use of a modern multi-core server in Rails is to run a single instance [of Rails] on each CPU core. Then one factors any large caches out of those Rails processes (via memcached or similar) and – ideally – places them on the same machine in order to make proper use of local memory. (That said, it seems that most people run the cache servers “wherever”, which is obviously problematic from a latency perspective)
Compare that to a system design where the web framework can use all available CPU cores and use a single in-memory cache; latency is now reduced to the time it takes to grab a read lock and fetch from main memory. It’s also a simpler programming model since there are no potential cache-fetch RPC failures to contend with.
Well-Intentioned Developer: “Huh. Well, I am more interested in time-to-market than serving latency, and dynamic languages (and DSLs in those languages) allow me to work more rapidly.”
However, modern dynamic languages (and their incapacity to do the sort of meaningful pre-runtime verification of basic semantic well-being one expects from a compiler) place a heavy burden on test coverage. Of course it is essential in any language to provide test coverage for core algorithms and other subtle aspects of a software module. However, when trying to “move fast” and get to market quickly, one shouldn’t have to write tests for every souped-up accessor method or trivial transformation. In a dynamic language, if you don’t actually have test coverage for every line, you’ll run the risk of trouble with “types and typos.” Either because, e.g., you thought you were accepting a list of strings, but some caller accidentally passes in a single string without the s, or because you fat-fingered a variable name in a rarely-taken conditional branch, or because you were expecting a duck but got a chicken. Aside from not making these [natural] mistakes in the first place, the only cure in modern dynamic languages is utterly comprehensive testing.
These are the situations that concern me in dynamic languages:
u = User.find(...)
If your tests don’t cover that else case, you won’t find out about your typo until your code runs the rarely-taken else block in production. These are the sorts of errors that compilers are great at discovering in languages with stronger typing (and of course there are countless such languages: I’m not picking any one in particular here).
In dynamic languages, then, one either spends much of one’s dev time writing tests to cover things a compiler could otherwise check for you, XOR one is contending with a long-term maintenance problem.
Well-Intentioned Developer: “well, you could say the same thing about Node.js, Django, or any other framework built on a modern dynamic language. I love dynamic languages, and I am willing to accept the dev cost for the tests (or the maintenance costs for the lack of tests).”
Furthermore, the tools available for profiling Ruby are either wildly inaccurate or have such a profound effect on performance as to be misleading. And trying to make software efficient without a profiler is foolish.
Well-Intentioned Developer: “But hiring is so difficult already, and most of the developers I encounter are familiar with Rails. Aren’t people the thing we should optimize for?”
A great engineer will have no trouble learning a new framework. In fact, a great engineer will be excited to learn a new framework. And you should only ever hire great engineers.
Well-Intentioned Developer: “When would Rails be appropriate, then?”
If you already know how to get things done in Rails, you’re in a hurry, you don’t need to maintain what you’re building, and performance is not a concern, it might be a good choice. Otherwise, perhaps never – I wouldn’t, anyway.