WordPress (+plugins) is not exactly the most resource-efficient content management system. In order to go easy on CPU and memory, to guarantee short website load times, and to be able to stand up to page requests with high frequency, WordPress must be served from a cache rather than answering each request via PHP and from the database. In this post, you can find my nginx config for deploying PHP-FPM-based WordPress behind nginx whereas most of the requests are served from the nginx FastCGI cache. No changes to WordPress and no WordPress plugins are required.
On shared hosting platforms, caching on the WordPress application level is a reasonable solution (via plugins such as WP Super Cache). Application-independent caching (i.e. on the server level) however increases maintainability and performance. Numerous solutions are available. The FastCGI cache built into the nginx web server is one of them. It allows for caching any FastCGI application, such as PHP-FPM-based WordPress. Before activating the nginx FastCGI cache, my (weak) machine was able to answer about 10 WordPress page requests per second. With the cache, this number grew by 3 orders of magnitude to roundabout 7000 requests per second. Setting up PHP-FPM-based WordPress behind nginx is a simple task. If you are running Apache, I recommend not to fear the transition towards nginx. It is done quickly and you will like the nginx configuration system. It is well thought trough and easy to learn.
Contents:
- 1) Key points of the configuration we’re going to look at
- 2) Nginx config
- 3) Comparison: benchmark with and without caching
1) Key points of the configuration we’re going to look at
-
Web application and environment:
WordPress driven by PHP5 FPM and MySQL from the standard repositories of Ubuntu 12.04.
-
Web server and cache:
Nginx (Brian’s nginx including the cache purge module). Installation:apt-get install python-software-properties add-apt-repository ppa:brianmercer/nginx apt-get update apt-get install nginx-custom
-
Caching details:
- Generally, requests to the PHP5 FPM backend are cached and served from the cache via nginx’ FastCGI cache.
- Caching parameters are configured based on return code (e.g. code 200 responses are generally served from the cache, whereas the cache is refreshed e.g. once per hour).
- POST and HEAD requests as well as requests containing WordPress cookies are not cached. This way the cache does not interfer with the WordPress dashboard and commenters immediately see their comments.
- The cache for certain request URIs can be manually deleted (and therefore refreshed on the next request) via calling
http://domain.com/purgecache/request_uri
, e.g.http://domain.com/purgecache/2013/01/blogpost-title/
.
2) Nginx config
In this section, you can find my nginx config as of January 2013. I have enriched it with comments. However, I strongly recommend reading and understanding the nginx configuration documentation, in particular the caching options in the HttpFastcgiModule and this helpful discussion.
Note: Disclosing a web server configuration is a security risk. Therefore, I removed/changed certain parts irrelevant to the general setup. If you still find a problematic part in my config, please let me know rather than misusing it.
The main nginx config (/etc/nginx/nginx.cfg
):
user www-data; worker_processes 4; pid /var/run/nginx.pid; events { worker_connections 1024; } http { sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 3; types_hash_max_size 2048; include /etc/nginx/mime.types; default_type application/octet-stream; access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; gzip on; gzip_disable msie6; gzip_static on; gzip_comp_level 4; gzip_proxied any; gzip_types text/plain text/css application/x-javascript text/xml application/xml application/xml+rss text/javascript; index index.php index.html index.htm; # Define PHP FPM backend. upstream phpfpmbackend { server unix:/tmp/php5-fpm.sock; } # Define FastCGI cache. fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=wordpresscache:30m inactive=60m; fastcgi_temp_path /var/cache/nginx/tmp; include /etc/nginx/conf.d/*.conf; include /etc/nginx/sites-enabled/*; }
The PHP FPM backend is defined with the name phpfpmbackend
. A FastCGI cache zone with the name wordpresscache
is declared. These names are used in the website config below (/etc/nginx/sites-available/gehrckede.cfg
):
# Redirect www.gehrcke.de to gehrcke.de. server { server_name www.gehrcke.de; access_log off; return 301 $scheme://gehrcke.de$request_uri; } # Redirect any other server name to gehrcke.de. server { server_name _; access_log off; rewrite ^ $scheme://gehrcke.de$request_uri redirect; } server { server_name gehrcke.de; root /path_to/gehrcke_de_website/; error_log /path_to/nginx_logs/nginx_error_log.log error; access_log /path_to/nginx_logs/nginx_access_log.log; # Security: deny access to any files with a .php extension in # WP upload directory. location ~* /(?:uploads|files)/.*\.php$ { deny all; } # Try if static file exists. If not, hand over to WordPress index.php. location / { try_files $uri $uri/ /index.php?$args; } # Deactivate access_log for WordPress monitoring site. Do not cache. location /status { access_log off; include fastcgi_params; fastcgi_pass phpfpmbackend; fastcgi_param SCRIPT_FILENAME $document_root/index.php; } # Add trailing slash to */wp-admin requests. rewrite /wp-admin$ $scheme://$host$uri/ permanent; location ~ \.php$ { fastcgi_pass phpfpmbackend; include fastcgi_params; set $wordpress_nocache ""; if ($http_cookie ~ (comment_author_.*|wordpress_logged_in.*|wp-postpass_.*)) { set $wordpress_nocache "true"; } if ($request_method ~ ^(HEAD|POST)$) { set $wordpress_nocache "true"; } fastcgi_cache_use_stale error timeout invalid_header http_500; fastcgi_cache_key "$scheme$request_method$host$request_uri"; fastcgi_cache wordpresscache; # Logging configuration. Log to two files: # 1) caching log, indicating whether a request was answered from # cache or via PHP backend. # 2) normal access log for e.g. evaluation via AWStats. # The 'cache' logging definition prints warning on nginx restart (saying # it should be defined in HTTP block). However, there it does not work # properly. Here, it shows all information correctly. log_format cache '***[$time_local] ' 'upstream_cache_status: "$upstream_cache_status" ' 'Cache-Control: "$upstream_http_cache_control" ' 'Expires: "$upstream_http_expires" ' 'Request: "$request" Status: "$status" ' 'User agent: "$http_user_agent" '; access_log /home/gehrckede/nginx_logs/nginx_cache.log cache; access_log /home/gehrckede/nginx_logs/nginx_access_log.log; # These parameters have to be chosen for each project individually. fastcgi_cache_valid 200 302 304 1h; fastcgi_cache_valid 301 1h; fastcgi_cache_valid any 5m; fastcgi_no_cache $wordpress_nocache; fastcgi_cache_bypass $wordpress_nocache; } # Activate cache purge feature. # Request e.g. http://domain.com/purgecache/2013/01/blogpost-title/ location ~ /purgecache(/.*) { fastcgi_cache_purge wordpresscache "$scheme$request_method$host$1"; } # Don't show favicon requests in access log. location = /favicon.ico { log_not_found off; access_log off; } # Deny all attempts to access hidden files such as .htaccess. location ~ /\. { deny all; } # Hide certain locations. location = /privatearea { return 404; } # Allow directory listing for certain locations. location /files/stud { autoindex on; } location /awsac/permstuff { autoindex on; } # Explicit redirects. location ^~ /gsoc { rewrite ^ $scheme://gehrcke.de/projects/google-summer-of-code redirect; } # PHP Monitoring. # These monitoring sites are configured in the php-fpm config. location ~ ^/(status_php5fpm|ping_php5fpm)$ { include fastcgi_params; fastcgi_pass phpfpmbackend; } # nginx monitoring. location /nginxstatus { stub_status on; access_log off; } }
3) Comparison: benchmark with and without caching
The following benchmark was executed with ab running on the same machine as the web stack (nginx, PHP, …). The machine has only 2 (virtual) CPUs, so the benchmark results have to be interpreted qualitatively.
Without caching:
$ ab -n 100 -n 10 http://gehrcke.de/ This is ApacheBench, Version 2.3 <$Revision: 655654 $> [...] Server Software: nginx/1.2.4 Server Hostname: gehrcke.de Server Port: 80 Document Path: / Document Length: 91079 bytes Concurrency Level: 10 Time taken for tests: 8.228 seconds Complete requests: 100 [...] Requests per second: 12.15 [#/sec] (mean) Time per request: 822.823 [ms] (mean) Time per request: 82.282 [ms] (mean, across all concurrent requests) Transfer rate: 1083.11 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 0 0.1 0 0 Processing: 548 791 180.7 793 1366 Waiting: 379 591 164.5 586 1136 Total: 548 791 180.7 793 1366 Percentage of the requests served within a certain time (ms) 50% 793 66% 860 75% 896 80% 910 90% 1024 95% 1150 98% 1228 99% 1366 100% 1366 (longest request)
With caching:
$ ab -n 20000 -c 200 http://gehrcke.de/ This is ApacheBench, Version 2.3 <$Revision: 655654 $> [...] Server Software: nginx/1.2.4 Server Hostname: gehrcke.de Server Port: 80 Document Path: / Document Length: 91079 bytes Concurrency Level: 200 Time taken for tests: 2.916 seconds [...] Requests per second: 6858.12 [#/sec] (mean) Time per request: 29.163 [ms] (mean) Time per request: 0.146 [ms] (mean, across all concurrent requests) Transfer rate: 611202.76 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 0 3 1.0 3 11 Processing: 4 22 2.4 21 30 Waiting: 1 3 2.0 3 23 Total: 8 25 2.3 24 33 Percentage of the requests served within a certain time (ms) 50% 24 66% 25 75% 26 80% 27 90% 28 95% 30 98% 31 99% 31 100% 33 (longest request)
~10 vs. ~7000 page requests per second: That’s why WordPress should be cached.
Leave a Reply