Our society places a great deal of value on speed whether that’s good or not is debatable. It’s clear though that on the Internet speed is king. I’m not just talking about the speed of your Internet connection but also the speed of the website you are communicating with. In 2010 Google announced that it would include website speed or latency in ranking webpages. I recently started looking (again) at the performance and speed of my personal website. For the past 4 years I’ve been running the typical LAMP stack comprised of Linux, Apache, MySQL and PHP to power my WordPress and Simple Machines Forum websites. And while the LAMP stack is very stable and well established among Internet web servers it’s not exactly known for breaking any speed records.
How can I do more without more?
With traffic growing year after year and the website feeling slow (anything beyond 3 seconds is slow to me) I picked up the torch again hoping to finally answer the question of How can I do more without more? How can I squeeze more performance out of the same hardware (VPS) that I currently have?
For the past three weeks I’ve been exploring the Apache (LAMP) vs Nginx (LEMP) debate trying to understand the pros and cons and if the performance gains would be applicable to my specific needs in running a website that processes under 100,000 page views monthly. It’s clear that Nginx can really help a website scale but I was curious what Nginx could do for me. It wasn’t just Nginx that I was testing but also PHP-FPM as opposed to using mod_php for Apache. I decided that the only way I could figure out the answer was the spend the time doing some actual testing and benchmarking.
I spun up a Linode512 instance and deployed CentOS 6.2 which I subsequently upgraded to release 6.3 via yum. I installed MySQL along with Nginx, PHP-FPM and APC from the REMI repository. Once that was all done I loaded an XML backup of my blog into the test server and attempted to duplicate my production website (blog) as closely as possible. The numbers showed an improvement but they didn’t really justify the effort needed to actually migrate the site. It wasn’t until I enabled W3 Total Cache (W3TC) along with some custom Nginx configurations that I observed a very significant improvement in performance. While there was a performance gain utilizing PHP-FPM over mod_php the real performance gain came in Nginx serving up static HTML files as compared to Apache. While WordPress is a PHP application, the plug-in W3 Total Control (W3TC) provides the ability to serve up static HTML files which allows small servers such as mine (Linode1024) to not only handle thousands of users but to serve them quickly via cached data and static HTML as opposed to actually running PHP for every request/session.
Last week I migrated this website to the Linode512 instance, upgraded the Linode1024 instance and migrated the site back without any downtime. While I had to spend some time scouring Google and testing some Nginx configurations but the effort was well worth it.
This website now loads in 1-2 seconds as opposed to previously loading in 6-7 seconds.
Benchmarks
I ran a number of different benchmarks including web-based applications such as WebPageTest and command line tools such as ab (Apache Bench). Here are the before and after benchmarks from the same Linode1024 instance.
blog.michaelfmcnamara.com (Apache/mod_php)
From: Dulles, VA – IE 8 – Cable
Thursday, November 15, 2012 9:00:52 PM
Performance Results (Median Run)
Document Complete | Fully Loaded | |||||||||
---|---|---|---|---|---|---|---|---|---|---|
Load Time | First Byte | Start Render | DOM Elements | Time | Requests | Bytes In | Time | Requests | Bytes In | |
First View (Run 5) | 4.451s | 0.255s | 1.138s | 593 | 4.451s | 46 | 834 KB | 6.400s | 48 | 857 KB |
Repeat View (Run 4) | 1.981s | 0.343s | 0.634s | 593 | 1.981s | 8 | 19 KB | 2.027s | 8 | 19 KB |
blog.michaelfmcnamara.com (Nginx/PHP-FPM/W3TC)
From: Dulles, VA – IE 8 – Cable
Saturday, November 24, 2012 12:04:14 PM
Looking at the numbers you can quickly see that we went from an average of 6-7 seconds to 1-2 seconds which is an incredible performance boost. I had tried a few different times over the years to get the FTB (First Time Byte) under 1.0 second but it wasn’t until I started utilizing some caching and static HTML (W3TC) that I was able to accomplish that goal. It’s clear now that PHP was creating the large FTB value as it was processing the code (WordPress).
Tweaks
Here are a few of the tweaks I needed to get everything running properly on my CentOS 6.3 server with Nginx and PHP-FPM with APC caching running WordPress and Simple Machines Forum.
/etc/nginx/conf.d/www.acme.com.conf
server { # Tell nginx to handle requests for the www.yoursite.com domain server_name www.acme.com; index index.php index.html index.htm; root /srv/www/www.acme.com/html; access_log /srv/www/www.acme.com/logs/access.log; error_log /srv/www/www.acme.com/logs/error.log; # Allow uploads of 20M in size client_max_body_size 20M; # 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 location ~* \.(css|js) { expires 30d; add_header Pragma public; add_header Cache-Control "public"; if (!-f $request_filename) { rewrite ^/wp-content/w3tc/min/(.+\.(css|js))$ /wp-content/w3tc/min/index.php?file=$1 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'; } # 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?q=$uri&$args; } # Cache static files for as long as possible location ~* \.(xml|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_buffers 8 256k; fastcgi_buffer_size 128k; fastcgi_intercept_errors on; fastcgi_pass unix:/tmp/php5-fpm.sock; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SCRIPT_NAME $fastcgi_script_name; } }
I did have some issues with Nginx and PHP tracking sessions in SMF. I would constantly get Session Verification failed when trying to logout of the forums or login to the administrative portal. That turned out to be an issue with the default value of session.save_path in the php.ini file so I modified the path to use /tmp and made sure that directory was accessible to all.
/etc/php.ini
session.save_path = "/var/lib/php/session" to session.save_path = "/tmp"
There were a few other tweaks but those were the ones that took me the longest to resolve/assemble.
Overall I’m really happy with the performance gains, if you’re running a WordPress website and your looking for numbers between 1-2 seconds you should definitely check out Nginx with PHP-FPM and APC, and don’t forget W3 Total Cache.
I also had to add the following to my Nginx configuration file to allow facilitate the RSS redirect to Google’s FeedBurner service.
# FeedBurner RSS Redirect - replace URLs below with your values if ($http_user_agent !~ FeedBurner) { rewrite ^/comment/feed/ http://feeds.feedburner.com/CommentsForMichaelFMcnamara last; rewrite ^/feed/ http://feeds.feedburner.com/michaelfmcnamara last; }
Cheers!