Diving into ruby on rails part 2

The coffee helped!

I’ve followed through the excellent getting started guide with no problems (though my demo site is about britney spears videos, not a blog), nipping out every now and then to check out reference documentation and rails source code (which is pretty tough to follow for now).

I’ve also installed modrails which worked as advertised (gotcha: they forget to mention you should also set up access permissions for your $railsapproot/public).

modrails performance, no database

With the same apache config I settled on for mod_wsgi, out of the box, performance is reasonable:

$ ab -k -n 10000 -c 100 http://127.0.0.1:81/
Requests per second:    508.93 [#/sec] (mean)
Time per request:       196.490 [ms] (mean)
Time per request:       1.965 [ms]
          (mean, across all concurrent requests)

With some very basic tuning:

PassengerHighPerformance on
RailsSpawnMethod smart
PassengerMaxPoolSize 30
PassengerPoolIdleTime 0
PassengerStatThrottleRate 300

I don’t see that much difference:

Requests per second:    533.85 [#/sec] (mean)
Time per request:       187.319 [ms] (mean)
Time per request:       1.873 [ms]
           (mean, across all concurrent requests)

The ruby processes take about 4-5% CPU per process, the httpd ones take about 0.6% per process. So while the overhead of ruby on rails is pretty significant, its really not shocking considering how much .

The built-in mongrel server in development mode does about 40 req/s, so you really don’t want to use that as a guide for performance benchmarking.

modrails performance, with database

Using the sqlite3 database backend with a very simple page:

$ ab -k -n 10000 -c 100 http://127.0.0.1:81/videos/1/comments/3
Requests per second:    256.87 [#/sec] (mean)
Time per request:       389.302 [ms] (mean)
Time per request:       3.893 [ms] (mean, across all concurrent requests)

Let’s try mysql…

production:
  adapter: mysql
  encoding: utf8
  database: britneyweb
  pool: 30
  username: root
  password:
  socket: /tmp/mysql.sock
$ sudo gem install mysql -- \
  --with-mysql-config=/usr/local/mysql/bin/mysql_config
$ RAILS_ENV=production rake db:migrate
$ sudo apachectl restart
# create some sample data in production db...
$ mysql -u root britneyweb
mysql> analyze table comments;
mysql> analyze table tags;
mysql> analyze table videos;
mysql> show create table comments \G
*************************** 1. row ***************************
       Table: comments
Create Table: CREATE TABLE `comments` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `commenterName` varchar(255) DEFAULT NULL,
  `commenterUrl` varchar(255) DEFAULT NULL,
  `commenterEmail` varchar(255) DEFAULT NULL,
  `body` text,
  `video_id` int(11) DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
$ 

Hmm, now the machine is swapping. Tuning down a bit, and then:

Requests per second:    250.00 [#/sec] (mean)
Time per request:       400.004 [ms] (mean)
Time per request:       4.000 [ms]
        (mean, across all concurrent requests)

I can’t really get it to go faster. It seems we are pretty much CPU-bound, with the vast majority of CPU going to ruby processes.

Adding page caching

Adding this tiny bit of code to the comments controller:

class CommentsController < ApplicationController
  ...
  caches_page :show
  ...

Helps a ‘bit’:

Requests per second:    4398.80 [#/sec] (mean)
Time per request:       22.733 [ms] (mean)
Time per request:       0.227 [ms]
        (mean, across all concurrent requests)

Now I need a cache sweeper:

class CommentsController  [:create, :update, :edit, :destroy]
  ...
  caches_page :show
  cache_sweeper :comment_sweeper, :only => [:create, :update, :edit, :destroy]

Enabling memcached cache backend in config/production.rb:

config.cache_store = :mem_cache_store, 'localhost',  \
   '192.168.1.1:11211', { :namespace => 'bwprod' }

…is not much faster (so bottleneck is probably elsewhere):

Requests per second:    4461.97 [#/sec] (mean)
Time per request:       22.412 [ms] (mean)
Time per request:       0.224 [ms]
      (mean, across all concurrent requests)

Lessons learned:

  • raw apache + modrails + rails can do a reasonable 500 req/s on my laptop when not connecting to a database, and a reasonable 400 req/s when connecting to sqlite3 for a simple page.
  • I shouldn’t really attempt to performance-tune modrails.
  • ActiveRecord seems good at chewing up CPU.
  • rails page caching makes things go fast, above 4000 req/s on my laptop.
  • rails + memcached is trivial to set up.