Working config for nginx + Drupal + Boost

Self-notes...after spending a few hours researching and benchmarking various configurations. Apache is so old news.

Setup:

  • Ubuntu Lucid Lynx (10.04)
  • nginx as frontend webserver
  • Drupal served using fastcgi to phpcgi
  • ...and Drupal's Boost module.

Most of these configs is gathered from http://groups.drupal.org/node/26363.

How fast is it now? How about almost 6,000 requests/sec, with throughput around 240Mbps? Fast enough?

$ ab -n 10000 -c 200 http://mysite.com/
This is ApacheBench, Version 2.3 <$Revision: 655654 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking mysite.com (be patient)
Completed 1000 requests
Completed 2000 requests
Completed 3000 requests
Completed 4000 requests
Completed 5000 requests
Completed 6000 requests
Completed 7000 requests
Completed 8000 requests
Completed 9000 requests
Completed 10000 requests
Finished 10000 requests


Server Software:        nginx/0.7.65
Server Hostname:        mysite.com
Server Port:            80

Document Path:          /
Document Length:        41691 bytes

Concurrency Level:      200
Time taken for tests:   1.716 seconds
Complete requests:      10000
Failed requests:        0
Write errors:           0
Total transferred:      422667243 bytes
HTML transferred:       418869477 bytes
Requests per second:    5828.49 [#/sec] (mean)
Time per request:       34.314 [ms] (mean)
Time per request:       0.172 [ms] (mean, across all concurrent requests)
Transfer rate:          240577.50 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        1    6   2.6      5      16
Processing:     6   28   8.5     24      50
Waiting:        1    7   3.4      6      28
Total:         11   34  10.2     29      56

Percentage of the requests served within a certain time (ms)
  50%     29
  66%     33
  75%     37
  80%     43
  90%     54
  95%     55
  98%     55
  99%     56
 100%     56 (longest request)

/etc/nginx/nginx.conf

user www-data;
worker_processes  10;
worker_rlimit_nofile  8192;


error_log  /var/log/nginx/error.log;
pid        /var/run/nginx.pid;

events {
    worker_connections  4096;
    multi_accept on;
    use epoll;
}

http {
    proxy_cache_path  /data/nginx/cache/one levels=1:2 keys_zone=one:10m max_size=1000m inactive=7d;
    proxy_temp_path /data/nginx/cache/tmp; 

## MIME types
  include            /etc/nginx/fastcgi.conf;
  include            /etc/nginx/mime.types;
  default_type       application/octet-stream;

## Size Limits
  client_body_buffer_size         1k;
  client_header_buffer_size       1k;
  client_max_body_size           20m;
  large_client_header_buffers   3 3k;
  connection_pool_size           256;
  request_pool_size               4k;
  server_names_hash_bucket_size  128;

## Timeouts 
  client_body_timeout             60;
  client_header_timeout           60;
  keepalive_timeout            75 20;
  send_timeout                    60;

## General Options
  ignore_invalid_headers          on;
  limit_zone gulag $binary_remote_addr 1m;
  recursive_error_pages           on;
  sendfile                        on;
  set_real_ip_from        0.0.0.0/32;
  real_ip_header     X-Forwarded-For;

## TCP options  
  tcp_nodelay on;
  tcp_nopush  on;

## Compression
  gzip              on;
  gzip_buffers      16 8k;
  gzip_comp_level   9;
  gzip_http_version 1.1;
  gzip_min_length   10;
  gzip_types        text/plain text/css image/png image/gif image/jpeg application/x-javascript text/xml application/xml application/xml+rss text/javascript image/x-icon;
  gzip_vary         on;
  gzip_static       on;
  gzip_proxied      any;
  gzip_disable      "MSIE [1-6]\.";

## Log Format
  log_format        main '"$http_x_forwarded_for" $host [$time_local] '
                         '"$request" $status $body_bytes_sent '
                         '$request_length $bytes_sent "$http_referer" '
                         '"$http_user_agent" $request_time "$gzip_ratio"';

  client_body_temp_path /data/nginx/cache/client_body_temp 1 2;
  access_log                   /var/log/nginx/access.log main;
  error_log                     /var/log/nginx/error.log crit;

    include /etc/nginx/conf.d/*.conf;
}

/etc/nginx/fastcgi.conf

# fastcgi.conf
fastcgi_param  SCRIPT_FILENAME    $document_root$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;
fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    ApacheSolaris/$nginx_version;
fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME        $server_name;

fastcgi_index  index.php;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

/etc/nginx/conf.d/mysite.conf

  server {
        limit_conn   gulag 10;
        listen       218.111.178.182:80;
        server_name  mysite.com;
        root         /data/www/$host/;
        index        index.php index.html;

    access_log /var/log/nginx/mysite.com_access.log;
    error_log /var/log/nginx/mysite.com_error.log info;


    ## Deny some crawlers
    if ($http_user_agent ~* (HTTrack|HTMLParser|libwww) ) {
         return 444;
    }
    ## Deny certain Referers (case insensitive)
      if ($http_referer ~* (poker|sex|girl) ) {
        return 444;
    }
    ## www. redirect
    if ($host ~* ^(www\.)(.+)) {
        set $rawdomain $2;
        rewrite ^/(.*)$  http://$rawdomain/$1 permanent;
    }
    
    ##
    ## required only when using purl, spaces & og for modules: ajax_comments, watcher and fasttoggle
    ## the /og path should be modified to match your default for og/purl URL for organic groups
    ##
    location ~* ^/og {
        rewrite ^/og\-(.*)/ajax_comments/(.*)$                  /index.php?q=ajax_comments/$2 last;
        rewrite ^/og\-(.*)/context/ajax-block-view$             /index.php?q=context/ajax-block-view last;
        rewrite ^/og\-(.*)/comment/reply/(.*)\?reload=1$        /index.php?q=comment/reply/$2&reload=1 last;
        rewrite ^/og\-(.*)/node/([0-9]+)/toggle/(.*)$           /index.php?q=node/$2/toggle/$3 last;
        rewrite ^/og\-(.*)/node/([0-9]+)/edit\?(.*)$            /index.php?q=node/$2/edit?$3 last;
        rewrite ^/og\-(.*)/user/([0-9]+)/watcher/toggle/(.*)$   /index.php?q=user/$2/watcher/toggle/$3 last;
        rewrite ^/(.*)$                                         /index.php?q=$1 last;
    }

    ## 6.x starts
    location / {
        rewrite ^/(.*)/$ /$1 permanent; # remove trailing slashes - disabled
        try_files $uri @cache;
    }

    location @cache {
        if ( $request_method !~ ^(GET|HEAD)$ ) {
            return 405;
        }
        if ($http_cookie ~ "DRUPAL_UID") {
            return 405;
        }
        error_page 405 = @drupal;
        add_header Expires "Tue, 24 Jan 1984 08:00:00 GMT";        
        add_header Cache-Control "must-revalidate, post-check=0, pre-check=0";
        add_header X-Header "Boost Citrus 1.9";               
        charset utf-8;
        try_files /cache/normal/$host${uri}_$args.html /cache/$host${uri}_$args.html @drupal;
    }

    location @drupal {
        ###
        ### now simplified to reduce rewrites
        ###
        rewrite ^/(.*)$  /index.php?q=$1 last;
    }

    location ~* (/\..*|settings\.php$|\.(htaccess|engine|inc|info|install|module|profile|pl|po|sh|.*sql|theme|tpl(\.php)?|xtmpl)$|^(Entries.*|Repository|Root|Tag|Template))$ {
        deny all;
    }

    location ~* /files/.*\.php$ {
        return 444;
    }
    location ~* /themes/.*\.php$ {
        return 444;
    }
       
    location ~ \.php$ {
          try_files $uri @drupal;       #check for existence of php file
          fastcgi_pass 127.0.0.1:9876;  #php-fpm listening on port 9000
          fastcgi_index index.php;
    }

    location ~ \.css$ {
        if ( $request_method !~ ^(GET|HEAD)$ ) {
            return 405;
        }
        if ($http_cookie ~ "DRUPAL_UID") {
            return 405;
        }
        error_page 405 = @uncached;
        #access_log  off;
        expires  max; #if using aggregator
        add_header X-Header "Boost Citrus 2.1";
        try_files /cache/perm/$host${uri}_.css /cache/$host${uri}_.css $uri =404;
    }
    
    location ~ \.js$ {
        if ( $request_method !~ ^(GET|HEAD)$ ) {
            return 405;
        }
        if ($http_cookie ~ "DRUPAL_UID") {
            return 405;
        }
        error_page 405 = @uncached;
        #access_log  off;
        expires  max; # if using aggregator
        add_header X-Header "Boost Citrus 2.2";               
        try_files /cache/perm/$host${uri}_.js /cache/$host${uri}_.js $uri =404;
    }

    location ~ \.json$ {
        if ( $request_method !~ ^(GET|HEAD)$ ) {
            return 405;
        }
        if ($http_cookie ~ "DRUPAL_UID") {
            return 405;
        }
        error_page 405 = @uncached;
        #access_log  off;
        expires  max; # if using aggregator
        add_header X-Header "Boost Citrus 2.3";               
        try_files /cache/normal/$host${uri}_.json /cache/$host${uri}_.json $uri =404;
    }

    location @uncached {
        #access_log  off;
        expires  max; # max if using aggregator, otherwise sane expire time
    }

    location ~* /files/imagecache/ {
        #access_log         off;
        try_files $uri @drupal;  #imagecache support - now it works
    }

    location ~* ^.+\.(jpg|jpeg|gif|png|ico)$ {
        #access_log      off;
        expires         30d;
        try_files $uri =404;
    }

    location ~* \.xml$ {
        if ( $request_method !~ ^(GET|HEAD)$ ) {
            return 405;
        }
        if ($http_cookie ~ "DRUPAL_UID") {
            return 405;
        }
        error_page 405 = @drupal;
        add_header Expires "Tue, 24 Jan 1984 08:00:00 GMT";
        add_header Cache-Control "must-revalidate, post-check=0, pre-check=0";
        add_header X-Header "Boost Citrus 2.4";               
        charset utf-8;
        types { }
        default_type application/rss+xml;
        try_files /cache/normal/$host${uri}_.xml /cache/normal/$host${uri}_.html /cache/$host${uri}_.xml $uri @drupal;
    }

    location ~* /feed$ {
        if ( $request_method !~ ^(GET|HEAD)$ ) {
            return 405;
        }
        if ($http_cookie ~ "DRUPAL_UID") {
            return 405;
        }
        error_page 405 = @drupal;
        add_header Expires "Tue, 24 Jan 1984 08:00:00 GMT";
        add_header Cache-Control "must-revalidate, post-check=0, pre-check=0";
        add_header X-Header "Boost Citrus 2.5";               
        charset utf-8;
        types { }
        default_type application/rss+xml;
        try_files /cache/normal/$host${uri}_.xml /cache/normal/$host${uri}_.html /cache/$host${uri}_.xml $uri @drupal;
    }

  } # end of server

/etc/init.d/phpcgi

#!/bin/bash
#
# chkconfig: - 55 45
# description: phpcgi
# probe: false

RETVAL=0
progdir="/usr/bin/"
prog="spawn-fcgi"
param="-f /usr/bin/php-cgi -p 9876 -C 10 -u www-data -g www-data"

start() {
        # Start daemons.
        if [ -n "`/bin/pidof $prog`" ]; then
                echo -n "$prog: already running"
                echo
                return 1
        fi
        echo -n $"Starting $prog: "
        # we can't seem to use daemon here - emulate its functionality
        $progdir$prog $param 
        RETVAL=$?
        [ $RETVAL -eq 0 ] && touch /var/run/phpcgi
        echo
        return $RETVAL
}
stop() {
        RETVAL=0
        pid=
        # Stop daemons.
        echo -n $"Stopping $prog: "
        killall -9 /usr/bin/php-cgi
        RETVAL=$?
        [ $RETVAL -eq 0 ] && rm -f /var/run/phpcgi
        echo
        return $RETVAL
}
restart() {
        stop
# wait for forked daemons to die
        sleep 1
        start
}       

# See how we were called.
case "$1" in
        start)
                start
                ;;
        stop)
                stop
                ;;
        restart)
                restart
                ;;
        condrestart)
                [ -f /var/lock/subsys/phpcgi ] && restart
                ;;
        *)
                echo $"Usage: $0 {start|stop|restart|condrestart}"
                exit 1
esac

exit $?