Barefoot Development

JRuby on Rails -- Performance

I have been spending some time evaluating JRuby 1.0 as a possible Ruby on Rails deployment platform. Following RailsConf 2007, I was very excited by the potential of JRuby as a vehicle for deploying Rails apps that would have the advantages of robust J2EE application servers, including multi-threaded load management, database connection pools, and wide-ranging enterprise support.

JRuby Install

We typically deploy Rails apps using mongrel clusters behind an Apache 2 webserver. So, I started with a pretty straightforward Rails app I'm building, and deployed it as normal on the mongrels.

(By the way, unless otherwise mentioned, software versions used are native Ruby 1.8.6, Rails 1.2.3, JRuby 1.0, Tomcat 6.0.13, Java 1.5.0_07 on OSX, Java 1.6.0 on RedHat Linux, Apache web server 2.2.3.)

The next step was to get JRuby and setup a .war file to deploy. I found this blog post to be a very helpful start. I already had the latest Java from Apple installed, so I installed JRuby to /usr/local/jruby-1.0. I edited /etc/profile to include the following:
export JRUBY_HOME="/usr/local/jruby-1.0"
export PATH="/usr/local/bin:/usr/local/sbin:/bin:/sbin:/usr/bin:/usr/sbin:$JRUBY_HOME/bin"
export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home"
export ANT_HOME="/Developer/Java/Ant"

I decided to deploy the .war into Tomcat since I'm more familiar with it than the recommended Glassfish and I knew how to connect it well with Apache. So, I created a JDBC connection pool to the MySQL database by adding a META-INF directory to the Rails app, with the following context.xml file:
<Context path="/myapp" reloadable="true" crossContext="true">
<!-- Database Connection Pool -->
<Resource name="jdbc/myapp"
auth="Container"
type="javax.sql.DataSource"
factory="org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory"
maxWait="1000"
removeAbandoned="true"
maxActive="30"
maxIdle="10"
removeAbandonedTimeout="60"
logAbandoned="true"
username="myuser"
password="mypass"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://127.0.0.1:3306/myapp_development?autoReconnect=true" />
</Context>

This is the corresponding database.yml file that tells Rails where to find the connection pool data source:
development:
adapter: jdbc
jndi: java:comp/env/jdbc/myapp

production:
adapter: jdbc
jndi: java:comp/env/jdbc/myapp

With these configuration files, I went through the JRuby process for creating the .war file. I setup Tomcat and deployed the app, which worked after I put the MySQL driver into Tomcat's /lib directory. I connected the Tomcat app to Apache with mod_proxy_ajp. I can post more details about these configs if there is interest.

One important optimization that I added was to include JAVA_OPTS that launched Tomcat with enhanced memory heap limits for the JVM, and with a couple other parameters I found while searching for JRuby optimizations online. So, I added this line to my /etc/profile:
export JAVA_OPTS="-Xms128m -Xmx512m -Djruby.objectspace.enabled=false -Djruby.jit.enabled=true"

Performance

After configuring and setting everything up on my MacBook Pro (2GHz Intel Core Duo, 1.5GB RAM), I was excited by the performance of JRuby. Using simple comparison of wget spiders between the native Ruby on Rails (using 3 mongrels behind Apache) and JRuby versions, I was seeing over a 2x improvement in JRuby. The spider of around 24 URLs was taking about 10 seconds on native Ruby, while only about 4 seconds on JRuby/Tomcat.

I then moved to deploying on our RedHat Linux-based staging server, where the 24 URL spider was running in about 1.5 seconds with native Ruby (same 3 mongrel cluster behind Apache 2). With repeated tweaking, I couldn't get the JRuby/Tomcat to run faster than 4 seconds, even swapping out JVMs of Java 5 and Java 6.

I then considered that JRuby might have an advantage under load, so I setup Grinder, an open-source Java-based load testing tool, to "grind" both versions. The test simulated 10 simultaneous threads from three users, with each test repeated three times. Here are some comparison numbers of a couple URLs (times in milliseconds):

Run # Native A Java A Native B Java B
1 554 1070 586 378
2 861 928 519 885
3 719 1450 496 680
4 833 800 484 507
5 863 827 701 414
Average 766 1015 557 572

On average, JRuby is either slower or nearly equal in performance under load. It's still respectable considering JRuby's 1.0 status. The main downside was the significant disparity in CPU and RAM usage between JRuby and native Ruby. JRuby pegged the dual core CPUs under load, using upwards of 250MB of RAM, while the mongrels maintained under 30% CPU with about 40MB per mongrel.

I remain excited about JRuby, but can't use it for production work quite yet. I'm sure the JRuby team will continue with optimization -- and I'll be keeping a close eye on its progress for sure.

Doug Smith, Senior Developer, Barefoot