Ruby Enterprise Edition and Passenger

Recently, we evaluated Ruby Enterprise Edition and Apache mod_rails, known as Phusion Passenger, in a Virtual Private Server hosting environment. We compared performance and memory usage against our production instance, which runs this blog and uses an Nginx-powered Mongrel cluster.

[Editor’s note: new results have been published as of June 30, 2008.]

Preparation

First off, doing these kinds of evaluations is super easy with a VPS hosting service like Slicehost. We simply cloned a image of an existing production backup rather than building one from scratch. Then, we shut down Mongrel and Nginx.

Installation

Ruby Enterprise Edition

This was a breeze, following the installation instructions.

# cd /usr/local/src
# wget http://rubyforge.org/frs/download.php/38803/ruby-enterprise-1.8.6-20080624.tar.gz
# tar xzvf ruby-enterprise-1.8.6-20080624.tar.gz
# ./ruby-enterprise-1.8.6-20080624/installer

The installer will check for the GNU C++ compiler, and the Zlib and OpenSSL development headers, which are easy to install if you’re using a package manager like Yum on Fedora Linux.

The only hiccup we ran into was during the installation of the Enterprise Ruby-flavored version of the MySQL gem, getting the infamous “Failed to build gem native extension.” Fortunately, the rest of the installation process went through smoothly. Afterwards, we used this command to install the MySQL gem:

# /opt/ruby-enterprise-1.8.6-20080624/bin/ruby /opt/ruby-enterprise-1.8.6-20080624/bin/gem install mysql -- --with-mysql-include=/usr/include/mysql --with-mysql-lib=/usr/lib64/mysql

Important: The Ruby Enterprise Edition executable is installed in a separate location so that you end up with two versions of Ruby on your machine. This means when installing new gems, you should use Ruby Enterprise Edition version. You can make this the default version via symbolic links:

ln -fs /opt/ruby-enterprise-1.8.6-20080624 /opt/ruby-enterprise
ln -fs /opt/ruby-enterprise/bin/gem /usr/bin/gem
ln -fs /opt/ruby-enterprise/bin/irb /usr/bin/irb
ln -fs /opt/ruby-enterprise/bin/rake /usr/bin/rake
ln -fs /opt/ruby-enterprise/bin/rails /usr/bin/rails
ln -fs /opt/ruby-enterprise/bin/ruby /usr/bin/ruby

Note that the Ruby Enterprise Edition installer will install the latest stable version of Rails. So if you haven’t frozen your gems in your app and need an earlier version, just install it with

gem install rails -v 2.0.2

Passenger (mod_rails)

Again, the installation process was simple per the docs.

First, the gem:

# gem install passenger

Then, running the following command kick offs the installlation wizard.

# passenger-install-apache2-module

Again, the installer checks for required dependencies and will advise you how to handle missing ones. In our case, we did not have Apache 2 and its development headers installed on our slice (since we were using Nginx).

The installation wizard will also tell you next steps for configuring Apache, which involves copying a few lines of settings. And to make Apache aware of your Rails app, it’s as simple as setting up a Virtual Host.

  ServerName www.webficient.com
  DocumentRoot /u/apps/webficient/current/public

Don’t forget to restart Apache for the changes to take:

# service httpd restart

(You can also use this technique if you don’t want to restart the entire httpd process).

Benchmark Overview

Because our blogging platform caches content, we looked at both dynamic and static content performance to ensure the numbers weren’t misleading. Our dynamic test involved hitting the login page. Our static test requested an existing cached article.

We used Apache Bench to simulate load:

# ab -n 10000 -c 100 http://server/app_path

Both slices had the following aspects in common:

  • Linux Fedora 8, 64-bit
  • 512MB RAM
  • Rails 2.0.2

The differences were type of application server and Ruby VM:

  • “Production” slice uses 3 Mongrels (v1.1.5), 3 Nginx (v0.6.31) worker processes (with 1,024 worker connections), and Ruby 1.8.6 patchlevel 114.
  • “Test” slice uses Passenger (mod_rails) and Enterprise Ruby 1.8.6 patchlevel 111. Based on the recommendations in the Passenger documentation, we settled with a PassengerMaxPoolSize of 4, given the size of our VPS.

Memory Usage

Since we’re always interested in squeezing out every last meg of RAM in our slice, we were curious to see how things fared under mod_rails and Ruby Enterprise Edition. Before running any benchmarks, it was clear that without the memory hungry Mongrels, our mod_rails test slice was quite a bit leaner.

Idle Comparison

             total       used       free     shared    buffers     cached
mongrel:    524460     382736     141724          0      29816     118924
mod_rails:  524460     241684     282776          0      21856     121256

Be aware that Apache allocates very little memory until the first request comes in. You’ll expect to lose about 80 MB in free memory when that happens. So the above numbers will rarely be seen, unless you have a low traffic Web site.

As expected, memory usage surged during the load tests. With a PassengerMaxPoolSize equal to 4, mod_rails consumes more memory than the Mongrel/Nginx configuration. Setting it to 3 produced the opposite result. However, you’ll see this setting also affects requests per second.

Dynamic Content Test

             total       used       free     shared    buffers     cached
mongrel:    524460     481800      42660          0      27488     116984
mod_rails:  524460     500948      23512          0      20496     115360

Static Content Test

             total       used       free     shared    buffers     cached
mongrel:    524460     439552      84908          0      29564     118908
mod_rails:  524460     257604     266856          0      21544     121192

So under load, mod_rails can take it well. We are also thrilled that the mod_rails version of our blogging app is not leaking memory, unlike our Mongrel-powered production version.

Performance

What about speed? Results were in line with our expectations. Mod_rails does well serving up dynamic content but Nginx continues to dominate when static content is involved.

Dynamic Content – Mongrel/Nginx

Although the numbers shown here represent a PassengerMaxPoolSize set to 4, we also ran the same tests with a value of 3. Number of requests per second decreased while memory usage improved.

Concurrency Level:      100
Time taken for tests:   63.843779 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      30370000 bytes
HTML transferred:       25690000 bytes
Requests per second:    156.63 [#/sec] (mean)
Time per request:       638.438 [ms] (mean)
Time per request:       6.384 [ms] (mean, across all concurrent requests)
Transfer rate:          464.54 [Kbytes/sec] receive

Dynamic Content – mod_rails/Ruby Enterprise Edition

oncurrency Level:      100
Time taken for tests:   34.190459 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      30900000 bytes
HTML transferred:       25690000 bytes
Requests per second:    292.48 [#/sec] (mean)
Time per request:       341.905 [ms] (mean)
Time per request:       3.419 [ms] (mean, across all concurrent requests)
Transfer rate:          882.56 [Kbytes/sec] received

Static Content – Mongrel/Nginx

Concurrency Level:      100
Time taken for tests:   1.901919 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      540173910 bytes
HTML transferred:       538042632 bytes
Requests per second:    5257.85 [#/sec] (mean)
Time per request:       19.019 [ms] (mean)
Time per request:       0.190 [ms] (mean, across all concurrent requests)
Transfer rate:          277358.28 [Kbytes/sec] received

Static Content – mod_rails/Ruby Enterprise Edition

Concurrency Level:      100
Time taken for tests:   2.550226 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      540430000 bytes
HTML transferred:       537720000 bytes
Requests per second:    3921.22 [#/sec] (mean)
Time per request:       25.502 [ms] (mean)
Time per request:       0.255 [ms] (mean, across all concurrent requests)
Transfer rate:          206947.55 [Kbytes/sec] received

We’re Not Done Yet

Although we like what we see so far, we’re going to run some additional tests and analysis, including:


About this entry