Thursday, April 9, 2009

Hosting Rails Apps Using Nginx and Mongrel Cluster


Hosting Rails Apps Using Nginx and Mongrel Cluster

From: http://www.gregbenedict.com/2007/07/06/hosting-rails-apps-using-nginx-and-mongrel-cluster/


Posted by gbenedict under: apachehostinglinuxmongrelnginxruby on rails .

While I had never been happy with the performance of Apache, Lighttpd and FastCGI for hosting Rails, it was in fact a more pressing need that caused me to find something faster. See, there is this little known fact that FastCGI caches ranges for model validations in Rails.

But caching is good isn’t it?
Well, not in this case.

The range I was validating against was a dynamic date range. Specifically, will something occur in the next six months. This is where the caching became an issue. The to and from components of that range are dynamic, but regardless of when I did the check they were always the same as the day FastCGI was restarted (6 months after that day for the high end). This did not show up in development using lighttpd, webrick or mongrel. I also tried to change it using a custom validation, but that failed as well.

Everything I had read to this point showed Apache –> Lighttpd –> FastCGI as the standard. It’s just what people used. It’s what we used.

As I began my search for a solution, Google quickly turned me to nginx via a post on Err The Blogwhich pointed me in the direction of Geoffrey Grosenbach. Yeah, the same guy makes those excellent PeepCode screencast tutorials and does the Ruby on Rails Podcast. The Err The Blog posting also pointed me to Ezra’s new love of nginx. He’s been using it for rails hosting atEngineYard. So now I’ve found 3 blogs that I read using it and loving it.

Nginx is the brain child of Igor Sysoev and has been in use by over 20% of all Russian websites for several years now. How does that fly under the radar???

In case you are wondering it’s pronounced Engine X.

Installing nginx
I’m using Centos 4.4 on my testing server, but the install is fairly straight forward.

  1. mkdir /usr/local/nginx
  2. cd /usr/local/nginx
  3. wget http://sysoev.ru/nginx/nginx-0.5.22.tar.gz
  4. tar -xvzf nginx-0.5.22.tar.gz
  5. ./configure && make && make install

Installing Mongrel and Mongrel Cluster
This assumes you have ruby 1.85 and ruby gems already installed.

  1. sudo gem install mongrel
  2. sudo gem install mongrel_cluster
  3. sudo cp /usr/lib/ruby/gems/1.8/gems/mongrel_cluster-0.2.1/resources/mongrel_cluster /etc/init.d/mongrel_cluster
  4. add a PATH statement to mongrel_cluster file just above the CONF_DIR variable:PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local:/usr/local/sbin:/usr/local/bin
  5. sudo chmod +x /etc/init.d/mongrel_cluster
  6. sudo chown -R : /var/www/domains//current/
  7. sudo chown -R : /var/www/phpmyadminI have phpMyAdmin installed to make things easier with all this testing. It also shows you how to run php under nginx at the same time.

Configuring nginx

I borrowed an nginx init.d script from TopFunky. I suggest you do the same. This allows you to start nginx as a service using

service nginx start

Here is my nginx configuration script (nginx.conf)

user railswww railswww;

worker_processes 1;

pid /var/run/nginx.pid;

# Valid error reporting levels are debug, notice and info
error_log logs/error.log debug;

events {
worker_connections 1024;
}

http {

# pull in mime-types. You can break out your config
# into as many include’s as you want to make it cleaner
include /usr/local/nginx/conf/mime.types;

# set a default type for the rare situation that
# nothing matches from the mimie-type include
default_type application/octet-stream;

# configure log format
log_format main ‘$remote_addr - $remote_user [$time_local] ‘
‘”$request” $status $body_bytes_sent “$http_referer” ‘
‘”$http_user_agent” “$http_x_forwarded_for”‘;

# main access log
access_log /var/log/nginx_access.log main;

# main error log
error_log /var/log/nginx_error.log debug;

# no sendfile on OSX
sendfile on;

# These are good default values.
tcp_nopush on;
tcp_nodelay off;
# output compression saves bandwidth
gzip on;
gzip_http_version 1.0;
gzip_comp_level 2;
gzip_proxied any;
gzip_types text/plain text/html text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript;
# gzip_min_length 1100;
# gzip_buffers 4 8k;

# the server directive is nginx’s virtual host directive.
server {
# port to listen on. Can also be set to an IP:PORT
listen 80;

# sets the domain[s] that this vhost server requests for.
# None means listen to all.
server_name test1.tgfi.net;

# Set the max size for file uploads to 50Mb
client_max_body_size 50M;

# Replace with the full path to your phpmyadmin directory:
root /var/www;

# vhost specific access log
access_log /var/log/nginx.test1.tgfi.net.access.log main;

index index.html index.htm index.php;

# this rewrites all the requests to the maintenance.html
# page if it exists in the doc root. This is for capistrano’s
# disable web task
if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}

location / {
# Uncomment to allow server side includes so nginx can
# post-process Rails content
## ssi on;

# needed to forward user’s IP address to rails
proxy_set_header X-Real-IP $remote_addr;

# needed for HTTPS
proxy_set_header X_FORWARDED_PROTO https;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;

# If the file exists as a static file serve it directly without
# running all the other rewite tests on it
if (-f $request_filename) {
break;
}

# check for index.html for directory index
# if its there on the filesystem then rewite
# the url to add /index.html to the end of it
# and then break to send it to the next config rules.
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}

# Look for existence of PHP index file.
# Don’t break here…just rewrite it.
if (-f $request_filename/index.php) {
rewrite (.*) $1/index.php;
}
}

# Requires you to start one instance of http://topfunky.net/svn/shovel/nginx/php-fastcgi.sh
location ~ \.php$ {
fastcgi_pass 127.0.0.1:8888;
fastcgi_index index.php;

fastcgi_param SCRIPT_FILENAME /var/www/$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
}

}

# The following includes are specified for virtual hosts
include /var/www/domains/domain1/current/config/nginx.conf;
include /var/www/domains/domain2/current/config/nginx.conf;
include /var/www/domains/domain3/current/config/nginx.conf;
}

You’ll notice at the very end I reference an included conf file from each rails project (deployed via Capistrano). This makes config a snap and I only have to make a change in one place on the server to include the file. Here is the virtual host config file:

# The name of the upstream server is used by the mongrel
# section below under the server declaration
upstream mongrel_domain1 {
server 127.0.0.1:8200;
server 127.0.0.1:8201;
server 127.0.0.1:8202;
}

server {
# port to listen on. Can also be set to an IP:PORT
listen 80;

# Set the max size for file uploads to 50Mb
client_max_body_size 50M;

# sets the domain[s] that this vhost server requests for.
# None means listen to all.
server_name domain1.tgfi.net;

# doc root
root /var/www/domains/domain1/current/public;

# vhost specific access log
access_log /var/www/domains/domain1/shared/log/nginx.access.log main;

# this rewrites all the requests to the maintenance.html
# page if it exists in the doc root. This is for capistrano’s
# disable web task
if (-f $document_root/system/maintenance.html) {
rewrite ^(.*)$ /system/maintenance.html last;
break;
}

location / {
# Uncomment to allow server side includes so nginx can
# post-process Rails content
## ssi on;

# needed to forward user’s IP address to rails
proxy_set_header X-Real-IP $remote_addr;

# needed for HTTPS
#proxy_set_header X_FORWARDED_PROTO https;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect false;
proxy_max_temp_file_size 0;

# If the file exists as a static file serve it directly without
# running all the other rewite tests on it
if (-f $request_filename) {
break;
}

# check for index.html for directory index
# if its there on the filesystem then rewite
# the url to add /index.html to the end of it
# and then break to send it to the next config rules.
if (-f $request_filename/index.html) {
rewrite (.*) $1/index.html break;
}

# Look for existence of PHP index file.
# Don’t break here…just rewrite it.
if (-f $request_filename/index.php) {
rewrite (.*) $1/index.php;
}

# this is the meat of the rails page caching config
# it adds .html to the end of the url and then checks
# the filesystem for that file. If it exists, then we
# rewite the url to have explicit .html on the end
# and then send it on its way to the next config rule.
# if there is no file on the fs then it sets all the
# necessary headers and proxies to our upstream mongrels
if (-f $request_filename.html) {
rewrite (.*) $1.html break;
}

# You’ll need to change this proxy_pass to match what
# what you specified above. It must be unique to each vhost.

if (!-f $request_filename) {
proxy_pass http://mongrel_domain1;
break;
}
}

error_page 500 502 503 504 /500.html;
location = /500.html {
root /var/www/domains/domain1/current/public;
}

# Pass the PHP scripts to FastCGI server listening on ip:port.
#
# Requires you to start one instance of http://topfunky.net/svn/shovel/nginx/php-fastcgi.sh
location ~ \.php$ {
fastcgi_pass 127.0.0.1:8888;
fastcgi_index index.php;

fastcgi_param SCRIPT_FILENAME /var/www/domains/domain1/current/public$fastcgi_script_name;
fastcgi_param QUERY_STRING $query_string;
fastcgi_param REQUEST_METHOD $request_method;
fastcgi_param CONTENT_TYPE $content_type;
fastcgi_param CONTENT_LENGTH $content_length;
}

}

Configuring Mongrel Cluster
Mongrel Cluster is a wraper to easily setup load balanced sites of multiple mongrel servers. It can easiloy fire up using a yaml file which you can put inside each rails project’s config directory. This will start 3 servers listening on ports 8200 - 8202 (referenced in the nginx vhost config file).

cwd: /var/www/domains/domain1/current
port: 8200
environment: production
user: railswww
group: railswww
address: 127.0.0.1
pid_file: /var/www/domains/domain1/shared/pids/mongrel.pid
servers: 3

Whenever you deploy you have Capistrano do a restart (or a start on cold deploy) of Mongrel Cluster.

set :mongrel_config, “/var/www/domains/#{application}/current/config/mongrel_cluster.yml”

desc “Restart mongrel_cluster(which restarts rails)”
#namespace :deploy do
task :restart do
sudo “mongrel_rails cluster::restart -C #{mongrel_config}”
end
#end

desc “Cold deploy start mongrel_cluster(which restarts rails)”
task :start do
sudo “mongrel_rails cluster::start -C #{mongrel_config}”
end

Up and Running
Once I got nginx and mongrel cluster up and running I could see what everyone was raving about. I always thought the speed with which Apache/Lighttpd/FastCGI served pages was a bottleneck of rails. Boy was I wrong! Immediately I saw a speed increase — a big increase. Nginx has a much higher throughput and only gets better under load.

The static file serving by nginx is an added benefit as it doesn’t have to proxy down a level to Mongrel to read a html, css, jpg, gif, etc. file. Anything cached by Rails gets put into /public as an html file. After the first hit and cache file creation, nginx just knows to pick it up.

Conclusion
The combination of nginx and Mongrel Cluster fixed my date range caching issue, but I’m much more excited about the new found speed our rails apps have. Hats of to Igor Sysoev for an excellent piece of work.

If you’ve got an questions, post them in the comments below, or drop me a note at gbenedict [at] gmail [dot] com.

Additional Help
Here are the links that helped me out along the way:

No comments: