Why Nginx?

Since I wanted to improve my sites load speed I thought I should look around for some lighttpd-alternatives which I have used for so many years. The really annoying thing about lighttpd is its configuration syntax. I don’t think it’s comprehensive nor easy to learn. Besides that I wanted to have a configuration where memcahed plays a major role: Serve static content (html) from memcache. Lighttpd does have some modules/tutorials how to interact with memcached but I couldn’t find any working configuration. Frustration but also curiosity brought me to Nginx.

According to the author Nginx is an HTTP and reverse proxy server, as well as a mail proxy server. I thought I should give it a try and test its performance against Lighttpd. Meanwhile I’m using Lighttpd 'AND’ Nginx on my server due to some regexp issue that have to be solved very soon. In that case Nginx acts a a proxy server (see below for configuration stuff).

Get it!

Before going further you should have the latest (stable) available Nginx version. If you use Debian make sure, you add following to your /etc/apt/sources.list:

1
deb-src http://nginx.org/packages/debian/ squeeze nginx

On my system I have currently version 1.0.9-1 installed.

Basic configuration

The main file for this part is /etc/nginx/nginx.conf. This is my nginx.conf:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
user www-data;
worker_processes  5;
worker_rlimit_nofile 8192;

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

events {
    worker_connections  4096;
    use epoll;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    keepalive_requests    50;
    keepalive_timeout     300 300;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #cp_nopush     on;

    # Compression
    gzip on;
    gzip_buffers 16 8k;
    gzip_comp_level 2;
    gzip_http_version 1.1;
    gzip_min_length 10;
    gzip_types text/plain text/css application/x-javascript text/xml;
    gzip_vary on;
    gzip_proxied any;
    gzip_disable "MSIE [1-6].";

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

I won’t explain every line since this is not what I’m supposed to do. Please have a look at the links at the end of this article for additional links.

vHost configuration

Well this is probably the most interesting part. You should have a configuration file per each vhost located at /etc/nginx/conf.d/. In my case the configuration was adapted to Drupals system and needs. Let’s have a look:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
server {
        listen 80;
        server_name dornea.nu;

        root /var/www/sites/dornea.nu/htdocs/current; ## <-- Your only path reference.

        ## Drupal Boost with nginx 
        set $boost "";
        set $boost_query "_";

        if ( $request_method = GET ) { 
                set $boost G;
        }

        if ($http_cookie !~ "DRUPAL_UID") {
            set $boost "${boost}D";
        }

        if ($query_string = "") {
            set $boost "${boost}Q";
        }

        if ( -f $document_root/cache/normal/$host$request_uri$boost_query.html ) {
            set $boost "${boost}F";
        }

        if ($boost = GDQF){
            rewrite ^.*$ /cache/normal/$host/$request_uri$boost_query.html break;
        }

        location = /favicon.ico {
                log_not_found off;
                access_log off;
        }

        location = /robots.txt {
                allow all;
                log_not_found off;
                access_log off;
        }

        # This matters if you use drush
        location = /backup {
                auth_basic            "Restricted";
                auth_basic_user_file  /etc/lighttpd/lighttpd_passwords.inc;
                #deny all;
        }

        # Very rarely should these ever be accessed outside of your lan
        location ~* .(txt|log)$ {
                allow 192.168.0.0/16;
                deny all;
        }

        location ~ ..*/.*.php$ {
                return 403;
        }

        location / {

                set $memcached_key "nginx:$request_uri";
                memcached_pass 127.0.0.1:11211;
                default_type       text/html;

                error_page 404 405 502 = @fallback;
                autoindex  on;
        }

        location @fallback {
                try_files $uri @rewrite;
        } 

        location @rewrite {
                # Some modules enforce no slash (/) at the end of the URL
                # Else this rewrite block wouldn't be needed (GlobalRedirect)
                #rewrite ^/system/test/(.*)$     /index.php?q=system/test/$1;
                #rewrite ^/system/files/(.*)$    /index.php?q=system/files/$1;
                rewrite ^/sitemap.xml$          /index.php?q=sitemap.xml;

                rewrite ^/(.*)$                 /index.php?q=$1;
        }

        location ~ .php$ {
                fastcgi_split_path_info ^(.+.php)(/.+)$;
                #NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param  VERIFIED $ssl_client_verify;
                fastcgi_param  DN $ssl_client_s_dn;
                fastcgi_intercept_errors on;
        }

        # Fighting with ImageCache? This little gem is amazing.
        location ~ ^/sites/.*/files/imagecache/ {
                try_files $uri @rewrite;
        }
        # Catch image styles for D7 too.
        location ~ ^/sites/.*/files/styles/ {
                try_files $uri @rewrite;
        }

        location ~* .(js|css|png|jpg|jpeg|gif|ico)$ {
                expires 24h;
                log_not_found off;
        }
}

Having Drupals boost module enabled you should have static pages on your server which will be served by the memcache daemon. At the beginning of the configuration nginx checks if any static content should be served at all (ist the user logged in? etc.) If there is a html file which correlates with the URI then nginx will load the files content from memcache. If not: the PHP script will be executed. Speaking of PHP.. Lets have a look at /etc/nginx/:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# cat /etc/nginx/fastcgi_params 
fastcgi_pass   unix:/tmp/fcgi.sock;
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    nginx/$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;

fastcgi_param  REDIRECT_STATUS    200;

I use a socket instead of child processes. The socket can be created using /etc/init.d/fastcgi:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# cat /etc/init.d/fastcgi 
#!/bin/sh

### BEGIN INIT INFO
# Provides:          fastcgi
# Required-Start:    $local_fs $remote_fs $network $syslog
# Required-Stop:     $local_fs $remote_fs $network $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the FastCGI server
# Description:       starts spawn-fcgi using start-stop-daemon
### END INIT INFO

PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/local/bin/php-fastcgi

test -x $DAEMON || exit 0

set -e

RETVAL=0
case "$1" in
  start)
    $DAEMON
    RETVAL=$?
  ;;
  stop)
    killall -9 php5-cgi
    RETVAL=$?
  ;;
  restart)
    killall -9 php5-cgi
    $DAEMON
    RETVAL=$?
  ;;
  *)
    echo "Usage: php-fastcgi {start|stop|restart}"
    exit 1
  ;;
esac
exit $RETVAL

php-fastcgi is located at /usr/local/bin/:

1
2
3
# cat /usr/local/bin/php-fastcgi 
#!/bin/sh
/usr/bin/spawn-fcgi -F 1 -C 10 -a 127.0.0.1 -s /tmp/fcgi.sock -u www-data -g www-data -f "/usr/bin/php5-cgi -c /etc/php5/cgi/php.ini" -P /var/run/fastcgi-php.pid

Nginx as proxy server

As already mentioned nginx can be used as a proxy server. In my case I need nginx to redirect requests on a certain subdomain to lighttpd listening to a local interface. The configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
### dl.dornea.nu
server {
    listen 80;
    server_name dl.dornea.nu;
    root /var/www/sites/dl.dornea.nu/htdocs/current;
    rewrite_log on;
    index index.php index.html;

    location / {
        #index index.php index.html;
        proxy_pass         http://127.0.0.1:8080/;
        proxy_redirect     off;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_connect_timeout      90;
        proxy_send_timeout         90;
        proxy_read_timeout         90;

        proxy_buffer_size          4k;
        proxy_buffers              4 32k;
        proxy_busy_buffers_size    64k;
        proxy_temp_file_write_size 64k;
    } 

    location ~* .(js|css|png|jpg|jpeg|gif|ico)$ {
        expires 24h;
        log_not_found off;
    }
}

All requests to 'dl.dornea.nu/*’ will be redirected to 'http://127.0.0.1:8080’. Isn’t that simple? Simple configuration, no need for magic!

Benchmarks

ab (apache suite) benchmark

lighttpd

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ ab -n 1000 -c 2 http://dornea.nu/
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 dornea.nu (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
Completed 1000 requests
Finished 1000 requests

Server Software:        lighttpd/1.4.28
Server Hostname:        dornea.nu
Server Port:            80

Document Path:          /
Document Length:        48134 bytes

Concurrency Level:      2
Time taken for tests:   1438.721 seconds
Complete requests:      1000
Failed requests:        2
   (Connect: 0, Receive: 0, Length: 2, Exceptions: 0)
Write errors:           0
Total transferred:      48619020 bytes
HTML transferred:       48133998 bytes
Requests per second:    0.70 [#/sec] (mean)
Time per request:       2877.441 [ms] (mean)
Time per request:       1438.721 [ms] (mean, across all concurrent requests)
Transfer rate:          33.00 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       36   41  18.8     39     271
Processing:  1369 2836 1314.7   2641   15499
Waiting:     1215 2649 1306.8   2467   15346
Total:       1406 2877 1314.6   2680   15536

Percentage of the requests served within a certain time (ms)
  50%   2680
  66%   2903
  75%   3062
  80%   3208
  90%   3841
  95%   5057
  98%   6924
  99%   9111
 100%  15536 (longest request)

nginx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
$ ab -n 1000 -c 2 http://dornea.nu/
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 dornea.nu (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
Completed 1000 requests
Finished 1000 requests

Server Software:        nginx/1.0.9
Server Hostname:        dornea.nu
Server Port:            80

Document Path:          /
Document Length:        48216 bytes

Concurrency Level:      2
Time taken for tests:   133.533 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      48451000 bytes
HTML transferred:       48216000 bytes
Requests per second:    7.49 [#/sec] (mean)
Time per request:       267.065 [ms] (mean)
Time per request:       133.533 [ms] (mean, across all concurrent requests)
Transfer rate:          354.34 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:       36   42  18.5     39     268
Processing:   190  225  43.3    212     483
Waiting:       36   43  17.9     40     270
Total:        227  267  52.1    252     713

Percentage of the requests served within a certain time (ms)
  50%    252
  66%    259
  75%    262
  80%    269
  90%    299
  95%    371
  98%    471
  99%    481
 100%    713 (longest request)

tools.pingdom.com benchmark

lighttpd

lighttpd

nginx

lighttpd

Literature