[Home] You are not logged in. You can create an account here or login here

Just my stuff

Benchmarking mod_rails against mongrel

I’d like to use mod_rails instead of the apache->haproxy->mongrel configuration, but before I do I wanted to make sure I don’t lose to much speed, so I decided to benchmark mod_rails against mongrel. mod_rails was already benchmarked, but I don’t believe it if I don’t see it :)
To benchmark it I created a new rails application with a single controller and a hello.html.erb view with only ‘Hello World’ in it.
For the test I took a very low end machine, because the virtual server I rent for the production site is not much better anyway:

s2@fresh:~$ cat /proc/cpuinfo
processor       : 0
vendor_id       : CentaurHauls
cpu family      : 6
model           : 9
model name      : VIA Nehemiah
stepping        : 8
cpu MHz         : 532.000
cache size      : 64 KB
fdiv_bug        : no
hlt_bug         : no
f00f_bug        : no
coma_bug        : no
fpu             : yes
fpu_exception   : yes
cpuid level     : 1
wp              : yes
flags           : fpu vme de pse tsc msr cx8 sep mtrr pge cmov pat mmx fxsr sse rng rng_en ace ace_en
bogomips        : 1067.72
clflush size    : 32

with 483608 bytes of RAM on Ubuntu 7.10 and ruby 1.8.6.
The mongrel setup consists of 3 mongrels running in production mode behind haproxy balancing the requests coming from apache.
This is the apache config:
<VirtualHost *:80>
  ServerName benchmark.fresh

  ProxyRequests Off

  ProxyPass / http://127.0.0.1:8010/
  ProxyPassReverse / http://127.0.0.1:8010/
</VirtualHost>

I keep things simple because I want to test the speed of the app running with mongrel against mod_rails. I don’t care about static stuff, url rewriting and other things in this test.
haproxy is configured like this:
...
listen rails :8010
  server rails-1 localhost:8011 maxconn 1
  server rails-2 localhost:8012 maxconn 1
  server rails-3 localhost:8013 maxconn 1

I left the sessions active, and the configured database is a postrges connection. As this will be the same in the two setups, I hope this will not make any difference.
Now I get the mongrels running and start the benchmark (after a dry run of 1000 requests):
$ ab -n 1000 -c 100 http://benchmark.fresh/hello/hello
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking benchmark.fresh (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Finished 1000 requests

Server Software:        Mongrel
Server Hostname:        benchmark.fresh
Server Port:            80

Document Path:          /hello/hello
Document Length:        12 bytes

Concurrency Level:      100
Time taken for tests:   25.184929 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      480000 bytes
HTML transferred:       12000 bytes
Requests per second:    39.71 [#/sec] (mean)
Time per request:       2518.493 [ms] (mean)
Time per request:       25.185 [ms] (mean, across all concurrent requests)
Transfer rate:          18.58 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0  107 375.1      0    1874
Processing:    47 2272 771.9   2414   11236
Waiting:        5 2264 740.8   2413   11236
Total:        483 2380 711.4   2415   13110

Percentage of the requests served within a certain time (ms)
  50%   2415
  66%   2456
  75%   2484
  80%   2562
  90%   2793
  95%   2863
  98%   3291
  99%   4396
 100%  13110 (longest request)

Now it’s time for mod_rails. I stopped the mongrels and haproxy to free up some precious RAM.
Apache config:
<VirtualHost *:80>
  ServerName benchmark.fresh
  DocumentRoot /home/s2/tmp/benchmark/public/
</VirtualHost>

LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-1.0.1/ext/apache2/mod_passenger.so
RailsSpawnServer /usr/lib/ruby/gems/1.8/gems/passenger-1.0.1/bin/passenger-spawn-server
RailsRuby /usr/bin/ruby1.8
RailsEnv production

and after a dry run of 1000 requests:
$ ab -n 1000 -c 100 http://benchmark.fresh/hello/hello
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.146 $> apache-2.0
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright 2006 The Apache Software Foundation, http://www.apache.org/

Benchmarking benchmark.fresh (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Finished 1000 requests

Server Software:        Apache/2.2.4
Server Hostname:        benchmark.fresh
Server Port:            80

Document Path:          /hello/hello
Document Length:        12 bytes

Concurrency Level:      100
Time taken for tests:   22.329544 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      514001 bytes
HTML transferred:       12000 bytes
Requests per second:    44.78 [#/sec] (mean)
Time per request:       2232.954 [ms] (mean)
Time per request:       22.330 [ms] (mean, across all concurrent requests)
Transfer rate:          22.44 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2   4.9      0      41
Processing:    48 2069 2922.9   1445   22101
Waiting:       45 2064 2922.5   1443   22101
Total:         61 2071 2922.8   1445   22114

Percentage of the requests served within a certain time (ms)
  50%   1445
  66%   1589
  75%   1735
  80%   1808
  90%   2339
  95%   3249
  98%  17180
  99%  19692
 100%  22114 (longest request)

A pretty graph (because every serious benchmark has at least one):

mod_rails can handle 44,78 req/sec, and mongrel 39,71. I think I can ditch the complicated mongrel setup after I make sure mod_rails is as stable as the mongrels are.


Update: Dan and Sean pointed out in the comments that a
RailsMaxPoolSize 3
for mod_rails would be more appropriate. Here you go:
$ ab -n 1000 -c 100 http://benchmark.fresh/hello/hello
...
Concurrency Level:      100
Time taken for tests:   20.94626 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      514000 bytes
HTML transferred:       12000 bytes
Requests per second:    49.76 [#/sec] (mean)
Time per request:       2009.463 [ms] (mean)
Time per request:       20.095 [ms] (mean, across all concurrent requests)
Transfer rate:          24.93 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    8  28.1      0     113
Processing:   131 1902 1518.9   1240    6861
Waiting:      127 1900 1518.9   1238    6860
Total:        149 1910 1515.4   1253    6861

Percentage of the requests served within a certain time (ms)
  50%   1253
  66%   2038
  75%   3334
  80%   3656
  90%   4322
  95%   4644
  98%   5431
  99%   5696
 100%   6861 (longest request)

49.76. It’s actually quicker?! I could not believe this, so I repeated the test twice, with and without RailsMaxPoolSize 3.
With: 46.92, 46.51
Without: 10.22, 22.32
Ok. Without RailsMaxPoolSize set to 3 the box started to swap, that’s why it was slower.

Subdomain routing with rails

s2.diffuse.it, gpichler.diffuse.it, adanti.diffuse.it and so on. There are a few problems you have to overcome if you want your users to have their own subdomain:
1. first of all you need a dns wildcard
2. you need to configure apache
3. you need to differentiate between assets (javascripts, images, stylesheets) that are on the main site, and those from the subdomains
4. cookies
5. links to subdomains
The first problem is easy to solve: just tell your dns provider to route all traffic from *.yourdomain.tld to the same ip.
The second problem is easily solved too, with a catch all vhost in apache:

<VirtualHost *:80>
  ServerName yourdomain.tld
  DocumentRoot /opt/myapp/public/
  RequestHeader set SERVER_NAME yourdomain.tld
  ProxyRequests Off

  ProxyPass / http://127.0.0.1:8000/
  ProxyPassReverse / http://127.0.0.1:8000/
</VirtualHost>

If this is your first vhost in apache, then all requests going to the ip of your web server will end up in that vhost definition, this means that whatever.yourdomain.tld will end up using that vhost definition (I am assuming we have apache in front of a mongrel listening on 8000 here, but of course this will work with fcgi or mod_rails too).
Ok, now we have all requests to whatever subdomain proxyed to our mongrels on 8000. The RequestHeader set SERVER_NAME part is just a variable I added to the request. I will use this later in rails to get the current host name without any subdomain.
Now let’s move to the rails part. Groovie has very good support for subdomains with it’s routes, but rails (to my knowledge) does not, so I had to monkeypatch it a bit.
The first thing I did was to add a before_filter to get the current user by vhost:
class ApplicationController < ActionController::Base
  before_filter :get_appuser
  before_filter :get_vhostuser

  def get_appuser
    @appuser = User.find(session[:user_id])
  end

  def get_vhostuser
    @vhostuser = User.find_by_vhost(request.subdomains.first)
  end
end

This way we know what subdomain the current request is on, and who the logged in user is (if he is).
Next we have to make sure that static assets are served always from the root domain (yourdomain.tld). If we use
stylesheet_link_tag “style”
the generated url will be relative to the current host:
<link href=”/stylesheets/style.css” media=”screen” rel=”stylesheet” type=”text/css” />
but we want it to be always our root domain, so the browser can cache them and does not download the same stylesheets and javascripts each time for each subdomain. To do this we can just use
config.action_controller.asset_host = 'http://yourdomain.tld'

in environments/production.rb. This way all links pointing to static assets will be generated with http://yourdomain.tld in front of them.
Taken care of the static assets, we have to deal with the cookies. If a user logs in on http://yourdomain.tld/login, we want him to be logged in on http://username1.yourdomain.tld/ as well, and on http://username2.yourdomain.tld/ and so on. To archive this we have to set the domain of the session cookie to .yourdomain.tld.
Now we have almost everything in place. What we still need is some helpers to play nice with the subdomains. Wouldn’t it be nice if you could
<%= link_to 'Home', :controller => 'home', :action => 'index', :subdomain => 'user1' %>
?
Yes, it would. And here comes the ugly part. To do this I had to monkeypatch the UrlRewriter
#I took this from somewhere on the net,
#i don't remember who wrote it so I can't
#really give credit, but I did not write it!
module ActionController
  class UrlRewriter
    def rewrite(options = {})
      unless options[:subdomain].nil?
        if options[:subdomain] == false
          newhost = @request.server_name #domain(options[:tld_length] || 1)
        elsif @request.vhost != options[:subdomain]
          newhost = "#{options[:subdomain]}." + @request.server_name
        end
        unless newhost.nil?
          options[:host] = @request.port == @request.standard_port ? "#{newhost}" : "#{newhost}:#{@request.port}" 
          options[:only_path] = false
        end
      end
      options.delete(:subdomain)
      rewrite_url(options)
    end
  end
end

Just make a plugin out of this code, or put it in a file in the inizializers directory. But, where does that @request.server_name and @request.vhost come from I hear you all (the 2 users reading this post) scream? I patched the AbstractRequest too, to add this two methods:
module ActionController
  class AbstractRequest
     def server_name
       @env['HTTP_SERVER_NAME']
     end
     def vhost
       subdomains.first
     end
  end
end

Remember the
RequestHeader set SERVER_NAME yourdomain.tld
part in the apache config file? Here we use it to get the root domain name.
Well, done. Now we should have everything in place to get going.

passenger: mod_rails - first try

Today I thought I give it a try.
I installed the gem an started passenger-install-apache2-module.
Currently I am running apache2 with mod_proxy -> haproxy -> mongrels. A standard setup I think.
After the installer finished I just commented out the two ProxyPass directives (the DocumentRoot was already set, because apache is serving static files anyway)

#    ProxyPass / http://127.0.0.1:8000/
#    ProxyPassReverse / http://127.0.0.1:8000/

I reloaded apache, stopped the mongrels, went to the application url and got a listing of the files in my public folder… uhm, that’s not what I was expecting :)
A bit of poking around and I discovered that a reload was of course not sufficient, but a restart was needed… duh. Stupid me. I restarted apache this time, went to the application url and see there: diffuse.it in all it’s glory. Almost. The images where not displayed. On diffuse.it you can protect your images with a password or show them only to your friends, so they are not served directly by apache, but they are processed by rails first, and then served by apache using the xsendfile header. I had to go to the passenger user guide but there was not really a solution…
“Static assets are accelerated, i.e. they are served directly by Apache and do not go through the Rails stack. There are two reasons why Apache doesn’t serve static assets correctly…”
those images are not “static assets”, I need those urls to go through the application first, and after the app made sure the user viewing them can really view them, serve them (or not).
Well, I have to search a bit more, or maybe mod_rails just can’t do this. I will update this post when I find out.
Update: It was a permission problem. Now the images work, and xsendfile works too. Cool. For some reason the application now was not running as apache2 user, but as the user owning the rails application (?). Well, the migration was a piece of cake. I’ll test it a bit more this days and post back.

in_place_editor on rails 2.0 and rjs templates

All right. The in_place_editor is gone from rails 2.0 as everyone knows, so now we need to install it as a plugin. No problem. But I needed something particular this time: On this site you can upload pictures and make them password protected. So I had the in_place_editor for the password, and right below it a link pointing to the picture with the password (if you sign up you can see it) like this:


[Picture is password protected:] (in_place_editor) verydifficultpassword
[Link to this picture with password:] click


When a user updates the password, I wanted the link to update as well.
To do it I had to call the in_place_editor with :script => true like this
<span>
Picture is password protected: 
<span id="picture_pass_prot">
<%=h @picture.password_protected||'-no-' %>
</span>
</span>

<span id="picture_pass_prot_url">
<% if @picture.password_protected %>
<%= render :partial => 'pic_pass_url' %>
<% end %>
</span>

<%= in_place_editor 'picture_pass_prot', 
    {:url => url_for (:action => 'update_password_protected', 
                      :id => @picture), 
    :script => true} %>

the pic_pass_url partial only renders the url for the password protected image.
The update_password_protected action looks something like this
    @picture = Picture.find(params[:id])
    @picture.password_protected = value
    @picture.save

and the update_password_protected.rjs looks like this
page[:picture_pass_prot].replace_html(@picture.password_protected)
page[:picture_pass_prot_url].replace_html :partial => 'pic_pass_url'

So we did 2 things now: we replaced 2 elements in the html, and one of them with a partial.
Oh… and to make this work on rails 2.0.x you actually have to patch the in_place_editor def in in_place_macros_helper.rb, adding the line
    js_options['clickToEditText'] = %('#{options[:click_to_edit_text]}') if options[:click_to_edit_text]
    #ADD THIS LINE HERE
    js_options['htmlResponse'] = !options[:script] if options[:script]
    function << (', ' + options_for_javascript(js_options)) unless js_options.empty?

because “rails’ “in_place_editor” doesn’t let you specify htmlResponse for the underlying Ajax.InPlaceEditor call, which needs to be set to false.” – see here for the details.
Ok… now finally we have our in_place_editor that updates two (or more) html elements using rjs.

Prototype color picker

I was looking for a color picker, so if you make an account here you can change the color of your subdomain page and feel yourself more at home if you don’t like the blue :)
I found one here.
Thanks Sebo Zoltan for this :)
And it was really easy to integrate this with the current application:

<%= text_field 'layout', 'background',
  :size => 10,
  :style => 'background: ' + @layout.background,
  :onclick => "startColorPicker(this)",
  :onkeyup =>"maskedHex(this)" %>

Setting the domain of the session cookie in rails

If you have multiple vhosts like app1.example.com – app2.example.com, but you don’t want your users to log in for every vhost, you can set the session cookie of the rails application for the domain .example.com, so it will be valid for all subdomains of example.com. You can set the ‘session_domain’ option of the ActionController in environment.rb like this:

  config.action_controller.session = {
    :session_domain => 'example.com',
    :session_key => '_myapp',
    :secret      => 'longstringthatyoubetterkeepsecret'
  }

Running multiple rails apps behind apache in different folders

I just never remember this single line of code:

ActionController::AbstractRequest.relative_url_root = '/app1'

in environment.rb

How to make cool urls for your models

Say you want your url to be something like /blog/show/4_How+to+make+cool+urls+for+your+models instead of /blog/show/4.
Just add

def to_param
  "#{id}_#{CGI::escape(title)}" 
end

to your blog model and you are done.