Build high performance wordpress site using Nginx + Varnish

Highlight: Ubuntu + Nginx + PHP-FPM + Varnish for WordPress site

The current site is hosted on CentOS 6 using Apache in home lab server, so you may experience slowness due to network limitation. When I was using Apache Bench tool to test the site performance, it can only server 10 requests per second. I’ve tried using Memcache and APC, the performance gain is minimum. After use Varnish in front of Apache server, the site can server over 10,000 requests per second. Here, I am going to setup another test server using Nginx + Varnish to compare with current setup. With Nginx only, it can serve 60 requests per second. That’s 6 times faster than Apache without any cache program. Now, let’s see how much performance Nginx can gain by using Varnish compare to Apache.

Varnish in realtime

Ubuntu

First, create a Ubuntu server with basic installation. Here, I am using Ubuntu 11.04 x64 server edition. After the installation, check the server hostname and network settings. If the server is assigned an IP from DHCP, change it to static IP in the following file:

sudo vi /etc/network/interfaces

Change the primary network interface eth0 look like follows:

auto eth0
iface eth0
inet static address 192.168.0.10
netmask 255.255.255.0
network 192.168.0.0
broadcast 192.168.0.255
gateway 192.168.0.1

Restart the network:

sudo /etc/init.d/networking restart

Setup the hostname:

sudo vi /etc/hosts

Add a line like follows:

192.168.0.10    web2.cloudtech.org      web2

Logoff current session and login to verify the new hostname.

hostname -f
Hostname: web2.cloudtech.org
IP: 192.168.0.10

Update system packages to the latest version:

sudo apt-get update
sudo apt-get upgrade

Nginx

Install python properties

sudo apt-get install python-software-properties

Add the nginx PPA to the system to get the latest stable version of nginx

sudo -s
nginx=stable
add-apt-repository ppa:nginx/$nginx
apt-get update

Install nginx with the extra packages

apt-get install nginx-extras

After the installation, check the default Nginx configuration and default site to ensure the configuration matches with your system.

Nginx configuration: /etc/nginx/nginx.conf

Default server site configuration: /etc/nginx/sites-available/default

Nginx default server root directory and index:

root /usr/share/nginx/html
index index.php index.html index.htm;

* Make sure the root directory path is correct. I found that the root directory in the configuration file was set to /usr/share/nginx/www, but there’s a directory /usr/share/nginx/html on the server instead.

* Set the default page to index.php other than index.html by adding index.php before index.html.

Restart nginx and check the site:

service nginx restart

By visit http://192.168.0.10, you should see “Welcome to nginx!”. Otherwise, the nginx is not running properly.

PHP-FPM

Here, I am going to install PHP-FPM (FastCGI Process Manager). PHP-FPM is an alternative FastCGI implementation designed specifically for websites with high volumes of traffic.

mkdir /var/www
chown www-data:www-data /var/www
apt-get install php5-fpm php5-cgi php5-common php5-suhosin php-apc php5-mysql php5-dev php5-curl php5-gd php5-imagick php5-mcrypt php5-snmp
service php5-fpm restart
service nginx restart

PHP configuration file: /etc/php5/fpm/pool.d/www.conf

Create a php test page in default nginx web root:

cd /usr/share/nginx/html
echo "<?php phpinfo(); ?>" > phpinfo.php

Then, check http://192.168.0.10/phpinfo.php you will see the php configuration. Otherwise, the PHP is not working.

MySQL

Install the mysql and harden the installation:

apt-get install mysql-server mysql-client
mysql_secure_installation

* setup a root password, remove test db, disallow remote access to database if you don’t need.

Create a database and user, we will need this information when install wordpress

mysql -u root -p
create database testdb;
grant all on testdb.* to 'testuser' identified by 'dbpassword';

WordPress

Before we install wordpress, we need to create a site home directory and Nginx virtual host for the domain( eg: cloudtech.org).

Create home directory:

mkdir -p /srv/www/cloudtech.org/public_html/
mkdir -p /srv/www/cloudtech.org/logs/

Create a virtual host for the domain:

cd /etc/nginx/sites-available/
vi cloudtech.org

Copy and past the following content and change the domain name:

# W3TC config rules based on http://elivz.com/blog/single/wordpress_with_w3tc_on_nginx/
server {
listen 80; ## listen for ipv4; this line is default and implied
#listen [::]:80 default ipv6only=on; ## listen for ipv6
# Tell nginx to handle requests for the www.cloudtech.org domain
server_name cloudtech.org www.cloudtech.org;
if ($host != 'cloudtech.org') {
rewrite ^/(.*) http://cloudtech.org/$1 permanent;
}
index index.php index.html index.htm;
root /srv/www/cloudtech.org/public_html;
access_log /srv/www/cloudtech.org/logs/access.log;
error_log /srv/www/cloudtech.org/logs/error.log;
# Use gzip compression
# gzip_static on; # Uncomment if you compiled Nginx using --with-http_gzip_static_module
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 5;
gzip_buffers 16 8k;
gzip_http_version 1.0;
gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript image/png image/gif image/jpeg;
# Rewrite minified CSS and JS files
rewrite ^/wp-content/w3tc/min/([a-f0-9]+)\/(.+)\.(include(\-(footer|body))?(-nb)?)\.[0-9]+\.(css|js)$ /wp-content/w3tc/min/index.php?tt=$1&gg=$2&g=$3&t=$7 last;
# Set a variable to work around the lack of nested conditionals
set $cache_uri $request_uri;
# POST requests and urls with a query string should always go to PHP
if ($request_method = POST) {
set $cache_uri 'no cache';
}
if ($query_string != "") {
set $cache_uri 'no cache';
}
# Don't cache uris containing the following segments
if ($request_uri ~* "(\/wp-admin\/|\/xmlrpc.php|\/wp-(app|cron|login|register|mail)\.php|wp-.*\.php|index\.php|wp\-comments\-popup\.php|wp\-links\-opml\.php|wp\-locations\.php)") {
set $cache_uri "no cache";
}
# Don't use the cache for logged in users or recent commenters
if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp\-postpass|wordpress_logged_in") {
set $cache_uri 'no cache';
}
# similar to Apache Status - handy for quickly checking the health of nginx
location /nginx_status {
stub_status on;
access_log off;
allow all;
}
# Use cached or actual file if they exists, otherwise pass request to WordPress
location / {
try_files /wp-content/w3tc/pgcache/$cache_uri/_index.html $uri $uri/ /index.php;
}
# Cache static files for as long as possible - removed xml as an extension to avoid problems with Yoast WordPress SEO plugin which uses WP rewrite API.
location ~* \.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|css|rss|atom|js|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$ {
try_files $uri =404;
expires max;
access_log off;
}
# Deny access to hidden files
location ~* /\.ht {
deny all;
access_log off;
log_not_found off;
}
# Pass PHP scripts on to PHP-FPM
location ~* \.php$ {
try_files $uri /index.php;
fastcgi_index index.php;
fastcgi_pass 127.0.0.1:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
}
}

Enable the site and restart Nginx:

ln -s /etc/nginx/sites-available/cloudtech.org /etc/nginx/sites-enabled/cloudtech.org
service nginx restart

Install wordpress:

cd /srv/www/cloudtech.org/public_html/
wget http://wordpress.org/latest.tar.gz
tar -xzvf latest.tar.gz
mv wordpress/* .
rmdir wordpress
rm latest.tar.gz

Change the public_html folder ownership to www-data:

chown -R www-data:www-data /srv/www/cloudtech.org/public_html

Start installing wordpress by going to http://www.cloudtech.org

* Note that if the DNS for domain www.cloudtech.org is not set, it’s better to add the following to your computer local host file so that you can visit the site by using the domain name.

192.168.0.10 www.cloudtech.org

Varnish

Varnish Cache is a web application accelerator also known as a caching HTTP reverse proxy. You install it in front of any server that speaks HTTP and configure it to cache the contents. Varnish Cache is really, really fast. It typically speeds up delivery with a factor of 300 – 1000x, depending on your architecture. A high level overview of what Varnish does can be seen in the video attached to this web page.

Here, I will install Varnish on Ubuntu from Varnish repository:

curl http://repo.varnish-cache.org/debian/GPG-key.txt | sudo apt-key add -
echo "deb http://repo.varnish-cache.org/ubuntu/ lucid varnish-3.0" | sudo tee -a /etc/apt/sources.list
sudo apt-get update
sudo apt-get install varnish

By default, Varnish listens on port 6081, to use it acting as http reverse proxy of the Nginx server, we need to switch the Varnish listening port to 80 and switch Nginx listening port from 80 to something else, let’s use port 8080 here.

Change the default port to 80 in Varnish configuration file:

vi /etc/default/varnish

This file contains 4 alternatives, by default, it uses option 2, you can chose anyone by uncomment the code.

The default alternative 2 code looks like:

DAEMON_OPTS="-a :6081 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-S /etc/varnish/secret \
-s malloc,256m"

Now, we change the Varnish listening port to 80:

DAEMON_OPTS="-a :80 \
-T localhost:6082 \
-f /etc/varnish/default.vcl \
-S /etc/varnish/secret \
-s malloc,256m"

Before starting Varnish, we need to switch nginx to Port 8080:

vi /etc/nginx/sites-available/cloudtech.org
vi /etc/nginx/sites-available/default

* Uncomment the “listen 80;” part in default virtual host file and change the port to 8080

Now, we can restart Nginx and Varnish:

service nginx restart
service varnish restart

To verify both Nginx and Varnish are listening on correct ports:

netstat -lp
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN

Also, make sure the default Varnish configuration is pointing to the correct Nginx port.

 vi /etc/varnish/default.vcl
backend default {
.host = "127.0.0.1";
.port = "8080";
}

acl purge {
"127.0.0.1";
"localhost";
}

sub vcl_recv {
# First call our identify_device subroutine to detect the device
call identify_device;
if (req.request == "PURGE") {
if (!client.ip ~ purge) {
error 405 "Not allowed.";
}
return(lookup);
}
if (req.http.Accept-Encoding) {
#revisit this list
if (req.url ~ "\.(gif|jpg|jpeg|swf|flv|mp3|mp4|pdf|ico|png|gz|tgz|bz2)(\?.*|)$") {
remove req.http.Accept-Encoding;
} elsif (req.http.Accept-Encoding ~ "gzip") {
set req.http.Accept-Encoding = "gzip";
} elsif (req.http.Accept-Encoding ~ "deflate") {
set req.http.Accept-Encoding = "deflate";
} else {
remove req.http.Accept-Encoding;
}
}
if (req.url ~ "\.(gif|jpg|jpeg|swf|css|js|flv|mp3|mp4|pdf|ico|png)(\?.*|)$") {
unset req.http.cookie;
set req.url = regsub(req.url, "\?.*$", "");
}
if (req.url ~ "\?(utm_(campaign|medium|source|term)|adParams|client|cx|eid|fbid|feed|ref(id|src)?|v(er|iew))=") {
set req.url = regsub(req.url, "\?.*$", "");
}
if (req.http.cookie) {
if (req.http.cookie ~ "(wordpress_|wp-settings-)") {
return(pass);
} else {
unset req.http.cookie;
}
}
}

sub identify_device {
# Default to thinking it's a PC
set req.http.X-Device = "pc";

if (req.http.User-Agent ~ "iPad" ) {
# It says its a iPad - so let's give them the tablet-site
set req.http.X-Device = "mobile-tablet";
}

elsif (req.http.User-Agent ~ "iP(hone|od)" || req.http.User-Agent ~ "Android" ) {
# It says its a iPhone, iPod or Android - so let's give them the touch-site..
set req.http.X-Device = "mobile-smart";
}

elsif (req.http.User-Agent ~ "SymbianOS" || req.http.User-Agent ~ "^BlackBerry" || req.http.User-Agent ~ "^SonyEricsson" || req.http.User-Agent ~ "^Nokia" || req.http.User-Agent ~ "^SAMSUNG" || req.http.User-Agent ~ "^LG") {
# Some other sort of mobile
set req.http.X-Device = "mobile-other";
}
}

sub vcl_hash {
# Your existing hash-routine here..

# And then add the device to the hash (if its a mobile device)
if (req.http.X-Device ~ "^mobile") {
hash_data(req.http.X-Device);
}
}

sub vcl_fetch {
if (req.url ~ "wp-(login|admin)" || req.url ~ "preview=true" || req.url ~ "xmlrpc.php") {
return (hit_for_pass);
}
if ( (!(req.url ~ "(wp-(login|admin)|login)")) || (req.request == "GET") ) {
unset beresp.http.set-cookie;
set beresp.ttl = 1h;
}
if (req.url ~ "\.(gif|jpg|jpeg|swf|css|js|flv|mp3|mp4|pdf|ico|png)(\?.*|)$") {
set beresp.ttl = 365d;
}
}

sub vcl_deliver {
# multi-server webfarm? set a variable here so you can check
# the headers to see which frontend served the request
# set resp.http.X-Server = "server-01";
if (obj.hits > 0) {
set resp.http.X-Cache = "HIT";
} else {
set resp.http.X-Cache = "MISS";
}
}

sub vcl_hit {
if (req.request == "PURGE") {
set obj.ttl = 0s;
error 200 "OK";
}
}

sub vcl_miss {
if (req.request == "PURGE") {
error 404 "Not cached";
}
}

Apache Bench

* The Apache + Varnish performance is similar to Nginx + Varnish.
* During the testing, both servers have very low load( less than 1) and memory usage. The CPU usage is quite high.

Test 1 – Nginx + Varnish – 12249 requests/sec

# ab -c 1000 -n 900000 http://www.cloudtech.org/index.php

This is ApacheBench, Version 2.3 &lt;$Revision: 655654 $&gt;
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking www.cloudtech.org (be patient)
Completed 90000 requests
Completed 180000 requests
Completed 270000 requests
Completed 360000 requests
Completed 450000 requests
Completed 540000 requests
Completed 630000 requests
Completed 720000 requests
Completed 810000 requests
Completed 900000 requests
Finished 900000 requests
Server Software: nginx/1.2.3
Server Hostname: www.cloudtech.org
Server Port: 80

Document Path: /index.php
Document Length: 0 bytes

Concurrency Level: 1000
Time taken for tests: 73.472 seconds
Complete requests: 900000
Failed requests: 0
Write errors: 0
Non-2xx responses: 900004
Total transferred: 341008491 bytes
HTML transferred: 0 bytes
Requests per second: 12249.51 [#/sec] (mean)
Time per request: 81.636 [ms] (mean)
Time per request: 0.082 [ms] (mean, across all concurrent requests)
Transfer rate: 4532.54 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 15 81.8 9 7009
Processing: 0 53 794.9 16 61938
Waiting: 0 50 794.9 13 61932
Total: 0 68 827.1 26 61938

Percentage of the requests served within a certain time (ms)
50% 26
66% 28
75% 30
80% 31
90% 34
95% 37
98% 229
99% 251
100% 61938 (longest request)

Test 2 – Apache + Varnish = 12183 requests/sec

root@web2:~# ab -c 1000 -n 900000 http://www.cloudtech.org/index.php
This is ApacheBench, Version 2.3 &lt; $Revision: 655654 $&gt;
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking www.cloudtech.org (be patient)
Completed 90000 requests
Completed 180000 requests
Completed 270000 requests
Completed 360000 requests
Completed 450000 requests
Completed 540000 requests
Completed 630000 requests
Completed 720000 requests
Completed 810000 requests
Completed 900000 requests
Finished 900000 requests

Server Software: Apache/2.2.15
Server Hostname: www.cloudtech.org
Server Port: 80

Document Path: /index.php
Document Length: 0 bytes

Concurrency Level: 1000
Time taken for tests: 73.868 seconds
Complete requests: 900000
Failed requests: 0
Write errors: 0
Non-2xx responses: 899958
Total transferred: 403826717 bytes
HTML transferred: 0 bytes
Requests per second: 12183.87 [#/sec] (mean)
Time per request: 82.076 [ms] (mean)
Time per request: 0.082 [ms] (mean, across all concurrent requests)
Transfer rate: 5338.73 [Kbytes/sec] received

Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 56 420.4 4 21045
Processing: 1 25 148.3 10 12621
Waiting: 0 23 122.2 10 6847
Total: 2 80 447.8 13 21059

Percentage of the requests served within a certain time (ms)
50% 13
66% 15
75% 17
80% 19
90% 24
95% 226
98% 674
99% 3018
100% 21059 (longest request)

21 comments

  1. @Dol: It’s not the default content but rather his modifications.

    Also try to avoid if statements in your nginx config. I suspect you want to redirect all non www request to http://www.cloudtech.org, then do something like this:

    server {
    # Redirect all requests to www
    server_name cloudtech.org;
    return 301 $scheme://www.cloudtech.org$request_uri;
    }

    server {
    server_name http://www.cloudtech.org;

    }

    How is the mobile redirect working? My impression working with this is that the user agent often is rather difficult to spot.

  2. First off I want to say wonderful blog! I had a quick question in which I’d like to ask if you do not mind. I was curious to know how you center yourself and clear your head before writing. I have had a tough time clearing my mind in getting my thoughts out there. I truly do take pleasure in writing but it just seems like the first 10 to 15 minutes are lost just trying to figure out how to begin. Any recommendations or hints? Kudos!

  3. Hello would you mind letting me know which hosting company you’re utilizing?

    I’ve loaded your blog in 3 completely different web browsers and I must say this blog
    loads a lot quicker then most. Can you recommend a good web hosting
    provider at a fair price? Cheers, I appreciate it!

  4. Hello I am so thrilled I found your site, I really found you by error, while I was looking on Yahoo for something else, Nonetheless I am here now and would just like to say thanks a lot for a fantastic post and a all
    round enjoyable blog (I also love the theme/design), I don’t have time to read through it all
    at the minute but I have bookmarked it and also added your RSS
    feeds, so when I have time I will be back to read a
    great deal more, Please do keep up the excellent work.

  5. Wonderful beat ! I would like to apprentice while you amend your site,
    how can i subscribe for a weblog web site? The account helped me a acceptable deal.
    I were tiny bit acquainted of this your broadcast provided bright transparent concept

  6. What’s Taking place i am new to this, I stumbled uupon this I have
    found It absolutely useful and it has helped me out loads.
    I hope too contribute & help different users like its helped me.
    Great job.

  7. Everyone loves a good home cooked meal, but not
    everyone wants to spend time in the kitchen after
    a hard day at work. With all of the laundry, ironing and everything else to do,
    this is an easy chore they can do from an early age until they
    leave for college. mainly because I’m the one that works while everyone else plays.

  8. I like the valuable information you provide in your articles.
    I will bookmark your weblog and check again here frequently.

    I’m quite certain I’ll learn lots of new stuff right here!

    Good luuck for the next!

  9. Excellent blog you have here but I was curious if you knew of any forums that
    cover the same topics discussed in this article?
    I’d really like to be a part of online community where I
    can get opinions from other experienced individuals that share the same interest.
    If you have any recommendations, please let me know. Cheers!

    Feel free to visit my blog post :: Sims 4 Crack (Lottie)

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>