Adding manpower to a late software project makes it later
Filed under: ruby on rails,thin — Tags: , , , , , — Michael Glauche @ 00:10

Nothing spirtual here, but the cool monitoring framework god together with thin. You’ve probably read a lot of benchmarks about thin, but whats the fastest webserver when you can’t ensure they are up and running ? Right here is where god comes into play. God is (beside the silly name) a nice process monitoring framework with a ruby configuration script. You can easily start/stop/monitor daemon processes and put advanced flags on them, like CPU or memory usage.

The default god startup file looks quite neat if you only run one site, and is easily converted to work together with thin, as thin and mongrel share the same parameters. Now i don’t have the luxury of only supporting one site but rather quite a buch of (mostly small and inactive) sites which share quite a lot of critera. Creating this kind of config file for each of them seems quite exessive and against the rails DRY principle.

Now, with a recent thin version (I did try 0.6.3) you can have the configuration for each site in a seperate YAML file, there’s even a small god startup script included in the examples directory.

Now first, lets take a look at the thin yaml config of a test ruby installation, in this case test.yml in /etc/thin :

servers: 3
user: www-data
group: www-data
chdir: /var/www/rails
pid: tmp/pids/test
port: 8000
address: 0.0.0.0
log: log/thin.log

Most is pretty self explainig, the pid and logfile is relative to the startdir, and the uid/gid is set to www-data (the default on ubuntu machines)
Unfortunately the thin.god startup file in thin 0.6.3 seems to have a small bug when it comes to allocating ports (or i’m doing something wrong, but the config file works flawlessly with just thin), so here’s a fixed version of the thin.god file:

# == God config file
# http://god.rubyforge.org/
# Author: Gump
#
# fixed thin ports <michael@glauche.de>
#
# Config file for god that configures watches for each instance of a thin server for
# each thin configuration file found in /etc/thin.

require 'yaml'

config_path = "/etc/thin"

Dir[config_path + "/*.yml"].each do |file|
  config = YAML.load_file(file)
  num_servers = config["servers"] ||= 1
  for i in 0...num_servers
    God.watch do |w|
      w.group = "thin-" + File.basename(file, ".yml")
      port = config["port"] + i

      w.name = w.group + "-#{port}"

      w.interval = 30.seconds

      w.uid = config["user"]
      w.gid = config["group"]

      w.start = "thin start -C #{file} -o #{port}"
      w.start_grace = 10.seconds

      w.stop = "thin stop -C #{file} -o #{port}"
      w.stop_grace = 10.seconds

      w.restart = "thin restart -C #{file} -o #{port}"

      pid_path = config["chdir"] + "/" + config["pid"]
      ext = File.extname(pid_path)

      w.pid_file = pid_path.gsub(/#{ext}$/, ".#{port}#{ext}")

      w.behavior(:clean_pid_file)

      w.start_if do |start|
        start.condition(:process_running) do |c|
          c.interval = 5.seconds
          c.running = false
        end
      end
      w.restart_if do |restart|
        restart.condition(:memory_usage) do |c|
          c.above = 150.megabytes
          c.times = [3,5] # 3 out of 5 intervals
        end

        restart.condition(:cpu_usage) do |c|
          c.above = 50.percent
          c.times = 5
        end
      end

      w.lifecycle do |on|
        on.condition(:flapping) do |c|
          c.to_state = [:start, :restart]
          c.times = 5
          c.within = 5.minutes
          c.transition = :unmonitored
          c.retry_in = 10.minutes
          c.retry_times = 5
          c.retry_within = 2.hours
        end
      end
    end
  end
end

Note, there was a bug with older god installations and ubuntu installations, but they worked flawlessly for me with the 0.7.0 god release. (there were some problems with the 0.5.0 release and ubuntu, so check if you have an up to date god version)

It will look in /etc/thin for all .yml files and will start and supervise them accordlingly. To start up the monitoring use

 # god -c thin.god

After its up and running you can check the status of your servers with:

 # god status

That command should give the following output:

thin-test-8000: up
thin-test-8001: up
thin-test-8002: up

Filed under: nginx,ruby on rails — Tags: , , , , , — Michael Glauche @ 20:48

I wondered about how many parallel thin you need to get the optimal throughput. I often saw values from 3 to 10 servers in different configuration examples around the net. Time to get some real numbers.

To get a meaningful benchmark i took an old dual 1,3 GHz P3-s system with the latest ubuntu. I did install rails 2.0.2 and a testing project with it, then created a very simple controller, that just passes a “hello world” string to the view. Also, no database connections should be made.

The server is started with

rake thin:cluster:start RAILS_ENV=production SIZE=5 PORT=8000

You’ll need the latest thin 0.5.2 for it, as 0.5.1 has some serious bug that prevents it from running daemonized. The command above will start 5 thin instances, listening on ports 8000-8004. To actually benchmark the number of thin instances used, i did modify the upstream entries in nginx:

# only one server used in this example
upstream thin {
   server 127.0.0.1:8000;
   #server 127.0.0.1:8001;
   #server 127.0.0.1:8002;
   #server 127.0.0.1:8003;
   #server 127.0.0.1:8004;
}

The results are not really supprising and show that the nginx + thin combo scales well even up to 100 concurrent users. All Tests were done with

ab -n 10000 -c 20 http://localhost/foo/

where the “foo” controller was the minimal one returning the “hello world” page.

The results can be seen in this graph:

Nginx & thin Benchmark

As you can see, the best results for this dual cpu machine was indeed running two instances of thin. What is interesting to see that thin+nginx scale quite well over many concurent requests.

Of course this is an idealized test with minimal load times for each pagecall. In the next version i’ll put in a random delay before the template is rendered to simulate real wold loading times and slow operations (users up/downloading large files, etc ..)

Filed under: nginx,ruby on rails — Tags: , , , , — Michael Glauche @ 22:51

Recently i’ve been playing around with xen and different hosting solutions, and i was wondering about lightweight, yet performant replacements for the usual apache + mod_fcgi + dispatcher stack. I did toy around with nginx before, together with mongrel with quite good success.

But it seems there is some serious competition for mongrel coming along, the Thin webserver. It combines the good points of mongrel, the HTTP parser, together with an event driven IO framework called eventmachine. The Bechmarks of it look promising:

Rails Webservice Benchmark

Its clearly to see the event driven IO approach is clearly superiour to the others when having many concurent requests.

Now why nginx when thin already performs so good ? Well, for one thing, thin is still in its quite early stages of production. Also, when it comes to serving static files the ruby version does not even come close.  Another thing that comes to play is that nowerdays most CPU’s are multi core, while ruby is single threated. That means the concurrent requests will be served by one CPU.

Here nginx comes to play, its an excellent http server, proxy and load balancer. One could start serveral rails servers with mongrel (mongrel_cluster is an excellent tool for that), or many thin servers. Right now i’m using the following rake task (from Stepehn Celis) for starting thin:

namespace :thin do
  namespace :cluster do    desc 'Start thin cluster'
    task :start => :environment do
      `cd #{RAILS_ROOT}`
      port_range = RAILS_ENV == 'development' ? 3 : 8
      (ENV['SIZE'] ? ENV['SIZE'].to_i : 4).times do |i|
        Thread.new do
          port = ENV['PORT'] ? ENV['PORT'].to_i + i : ("#{port_range}%03d" % i)
          str  = "thin start -d -p#{port} -Ptmp/pids/thin-#{port}.pid"
          str += " -e#{RAILS_ENV}"
          puts str
          puts "Starting server on port #{port}..."
          `#{str}`
        end
      end
    end
desc 'Stop all thin clusters'
    task :stop => :environment do
      `cd #{RAILS_ROOT}`
      Dir.new("#{RAILS_ROOT}/tmp/pids").each do |file|
        Thread.new do
          if file.starts_with?("thin-")
            str  = "thin stop -Ptmp/pids/#{file}"
            puts "Stopping server on port #{file[/\d+/]}..."
            `#{str}`
          end
        end
      end
    end
  end
end

with that you can start/stop many thin instances easily:

# rake thin:cluster:start RAILS_ENV=production SIZE=3 PORT=8000
# rake thin:cluster:stop

Getting those instances into nginx is also easy, the following example layout works with the ubuntu nginx package:

upstream thin {
    server 127.0.0.1:8000;
    server 127.0.0.1:8001;
    server 127.0.0.1:8002;
}

server {
        listen   80;
        server_name  localhost;
        access_log  /var/log/nginx/localhost.access.log;
        root /var/www/test/public;

        location / {
                proxy_set_header  X-Real-IP  $remote_addr;
                proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header Host $http_host;
                proxy_redirect false;
                if (-f $request_filename/index.html) {
                        rewrite (.*) $1/index.html break;
                }
                if (-f $request_filename.html) {
                        rewrite (.*) $1.html break;
                }
                 if (!-f $request_filename) {
                        proxy_pass http://thin;
                        break;
                }
        }
}

To see if it is really working i used apache Bench on the the same setup with a simple dynamic page on a dual cpu p3-s 1,3 Ghz machine :

# ab -n 1000 -c 10 http://10.1.4.99/foo/
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 10.1.4.99 (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:        nginx/0.5.26
Server Hostname:        10.1.4.99
Server Port:            80

Document Path:          /foo/
Document Length:        59 bytes

Concurrency Level:      10
Time taken for tests:   6.247127 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      505000 bytes
HTML transferred:       59000 bytes
Requests per second:    160.07 [#/sec] (mean)
Time per request:       62.471 [ms] (mean)
Time per request:       6.247 [ms] (mean, across all concurrent requests)
Transfer rate:          78.92 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.7      0       9
Processing:     7   61  72.9     35     453
Waiting:        0   61  72.9     34     453
Total:          7   61  72.9     35     453

Percentage of the requests served within a certain time (ms)
  50%     35
  66%     59
  75%     82
  80%     97
  90%    134
  95%    203
  98%    333
  99%    380
 100%    453 (longest request)

The same on one of the internal thin servers would give the following result:

# ab -n 1000 -c 10 http://10.1.4.99:8000/foo/
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 10.1.4.99 (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:
Server Hostname:        10.1.4.99
Server Port:            8000

Document Path:          /foo/
Document Length:        59 bytes

Concurrency Level:      10
Time taken for tests:   7.880520 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      446000 bytes
HTML transferred:       59000 bytes
Requests per second:    126.90 [#/sec] (mean)
Time per request:       78.805 [ms] (mean)
Time per request:       7.881 [ms] (mean, across all concurrent requests)
Transfer rate:          55.20 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    0   0.0      0       1
Processing:    63   78  50.9     63     252
Waiting:       61   77  50.8     62     251
Total:         63   78  50.9     63     252

Percentage of the requests served within a certain time (ms)
  50%     63
  66%     63
  75%     64
  80%     64
  90%     65
  95%    251
  98%    251
  99%    252
 100%    252 (longest request)

As you can see, the nginx version scales quite a bit better with 10 concurrent users.

Now what is really missing is some nice scripted integration into startup scripts, so you can automaticly start/stop many nginx/thin installations at bootup (and not execute some manual rake tasks :) )

16 queries. 0.512 seconds. Powered by WordPress

Home