Nginxを使ったWordPressのチューニングといえば、フロントエンドのNginxとバックエンドのNginx(もしくはApache)に分けてproxy cacheを効かせるのが王道です。
さらにWP Super Cacheプラグインを利用してなるべくPHPやMySQLにアクセスさせないようにすると、手軽で絶大なパフォーマンスアップが可能です。
今回はそこからもう一歩進めたチューニングについて書きたいと思います。
二段階層を廃したシンプルな構成
まずは、図をご覧ください。
前述の王道チューニングの構成はA図となります。
proxy cacheはNginxがバックエンドのサーバーに処理を回し、返ってきたレスポンスをキャッシュして、Nginx自身がキャッシュを返すことでパフォーマンスを上げる仕組みです。
A図-1がキャッシュの無いアクセス、A図-2がキャッシュが効いているアクセスを表しています。
A図-3は静的なファイル(css, js, jpgなど)をフロントのNginxが直接返すことを表しています。
fastcgi cache(HttpFcgiModule)
proxy cache自体はバックエンドのサーバーがHTTPでコンテンツを返してくれれば、どんなアプリケーションでも問わない汎用的なキャッシュ機能です。つまり、proxy cacheを利用しようとするともうひとつHTTPサーバーが必要になり、A図のようにHTTPのレイヤーが二段構成になります。
Nginxにはproxy cacheと似た機能でfastcgi cacheという機能(HttpFcgiModule)があります。これはバックエンドのFastCGIのレスポンスをキャッシュする機能です。
これを利用すると、proxy cacheを利用したのと同じ効果をHTTPサーバーなしに実現できます。
A図の8080ポートのサーバーが必要なくなり、B図の構成になります。
無駄な通信と処理がなくなり、シンプルです。
WP Super Cacheを最大限利用する
WP Super Cacheは、アクセスごとに動的に生成される記事を、静的なHTMLファイルとして保存(キャッシュ)して、それを返すように振舞うプラグインです。
キャッシュが効いている間はMySQLにアクセスすることが無いのでパフォーマンスが上がります。
.htacessの落とし穴
WP Super Cacheを利用すると、.htaccessに以下の設定が追加されます。
(利用しているプラグインによって多少変わります)
# BEGIN WPSuperCacheRewriteEngine On RewriteBase / AddDefaultCharset UTF-8 RewriteCond %{REQUEST_METHOD} !POST RewriteCond %{QUERY_STRING} !.*=.* RewriteCond %{HTTP:Cookie} !^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$ RewriteCond %{HTTP:X-Wap-Profile} !^[a-z0-9\"]+ [NC] RewriteCond %{HTTP:Profile} !^[a-z0-9\"]+ [NC] RewriteCond %{HTTP_USER_AGENT} !^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).* [NC] RewriteCond %{HTTP_user_agent} !^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).* [NC] RewriteCond %{HTTP_USER_AGENT} !^(DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/) RewriteCond %{HTTP_USER_AGENT} !(DDIPOCKET;|WILLCOM;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE;|PDA;\ SL-|PlayStation\ Portable;|SONY/COM|Nitro|Nintendo) RewriteCond %{HTTP:Accept-Encoding} gzip RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html.gz -f RewriteRule ^(.*) "/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html.gz" [L] RewriteCond %{REQUEST_METHOD} !POST RewriteCond %{QUERY_STRING} !.*=.* RewriteCond %{HTTP:Cookie} !^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$ RewriteCond %{HTTP:X-Wap-Profile} !^[a-z0-9\"]+ [NC] RewriteCond %{HTTP:Profile} !^[a-z0-9\"]+ [NC] RewriteCond %{HTTP_USER_AGENT} !^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).* [NC] RewriteCond %{HTTP_user_agent} !^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).* [NC] RewriteCond %{HTTP_USER_AGENT} !^(DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/) RewriteCond %{HTTP_USER_AGENT} !(DDIPOCKET;|WILLCOM;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE;|PDA;\ SL-|PlayStation\ Portable;|SONY/COM|Nitro|Nintendo) RewriteCond %{DOCUMENT_ROOT}/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html -f RewriteRule ^(.*) "/wp-content/cache/supercache/%{HTTP_HOST}/$1/index.html" [L] # END WPSuperCache
これは、すでにWP Super Cacheによって静的なファイルが生成されている場合、PHPを介さずにApacheが直接リクエストを返すようにする設定です。
WP Super Cacheの推奨設定では、index.htmlとそれを圧縮したindex.html.gzが用意されるので、冗長な設定になっています。
Nginxにとって.htaccessはただのテキストファイルですから、上記RewriteRuleをNginxの設定ファイルに移植しないと、WP Super Cacheは本来のパフォーマンスを発揮しません。
この移植をすることで、B図-4のように、fastcgi cacheに到達することもなく、静的ファイルと同じ処理、パフォーマンスになります。
HttpGzipStaticModule
NginxのHttpGzipStaticModuleは、同じディレクトリ内にあらかじめ圧縮ファイル(.gz)が用意されている場合に、クライアント判定を行なって自動的に圧縮ファイルを返してくれるモジュールです。(正確には、Gzip圧縮に対応しているクライアントからリクエストが来て、同ディレクトリに同タイムスタンプで同名+.gzという名前のファイルがある場合)
このモジュールを導入するメリットは、gzファイル分の.htaccessを移植する手間がなくなることです。ただし、コアモジュールではないので、コンパイル時にオプションで指定する必要があります。
./configure --with-http_gzip_static_module
コンパイルし直したnginxの入れ替えを行う場合は、こちらを参考にしてください。
NginxにはコアモジュールでHttpGzipModuleがあり、設定をオンにすることで特定のContent-TypeにGzip圧縮をかけてレスポンスを返すことができます。こちらはリクエストごとにオンザフライで圧縮をかけるので、余分に処理がかかります。あらかじめcssやjsなどの圧縮ファイルが用意できる場合は、同階層に設置してHttpGzipStaticModuleの恩恵を受けたほうがいいと思います。
サンプル設定ファイル
前述の項目を網羅して、かつ可能な限り正常に動いている環境の設定ファイルを載せています。
nginx.conf
user nginx nginx; worker_processes 2; # サーバーのコア数に合わせて pid /var/run/nginx.pid; events { worker_connections 1024; use epoll; # Linuxの場合 } http { include mime.types; default_type application/octet-stream; # 最後の$request_timeは処理時間(ms) log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" $request_time'; sendfile on; tcp_nopush on; tcp_nodelay on; keepalive_timeout 10; connection_pool_size 256; client_header_buffer_size 1k; large_client_header_buffers 4 2k; request_pool_size 4k; if_modified_since before; ignore_invalid_headers on; server_tokens off; gzip on; gzip_min_length 0; gzip_buffers 4 8k; gzip_types text/plain text/xml application/x-javascript text/css; gzip_disable "msie6"; gzip_vary on; # HttpStaticGzipModuleをオンに gzip_static on; output_buffers 1 32k; postpone_output 1460; # fastcgi cacheの設定(httpディレクティブ内のみ有効) fastcgi_cache_path /usr/local/nginx/cache levels=1:2 keys_zone=wpcache:10m max_size=50M inactive=30m; server { listen 80; server_name localhost; charset utf-8; location / { return 403; } location /nginx_status { stub_status on; access_log off; allow 127.0.0.1; deny all; } # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } } include ./wordpress.conf; }
wordpress.conf
# 設定を変えやすいようにupstreamにまとめておく upstream phpfpm { # ローカルの場合はUNIXソケットで server unix:/var/run/php-fpm/www.sock; } server { listen 80; server_name example.com; root /var/www/wordpress; access_log logs/access.log main; location / { # 静的なファイルの場合は処理をとめる # リクエストの度にファイルの存在をチェックするのは無駄だという意見もありますが、 # どのURLの時index.phpに渡すのか、プラグインを含めて全仕様が分からないため、 # これが最も安全だと思います。 if (-f $request_filename) { break; } # ここからWP Super Cacheの設定(少しfastcgi cacheの設定も) # モバイルからのアクセスはキャッシュさせないようにする変数 set $nocache ""; set $supercache_file $document_root/wp-content/cache/supercache/${http_host}${uri}/index.html; set $supercache_uri ""; if (-f $supercache_file) { set $supercache_uri /wp-content/cache/supercache/${http_host}${uri}/index.html; } if ($request_method = "POST") { set $supercache_uri ""; set $nocache "1"; } if ($query_string ~ .*=.*) { set $supercache_uri ""; } if ($http_cookie ~ ^.*(comment_author_|wordpress_logged_in|wp-postpass_).*$) { set $supercache_uri ""; set $nocache "1"; } if ($http_x_wap_profile ~ ^[a-z0-9\"]+) { set $supercache_uri ""; set $nocache "1"; } if ($http_profile ~ ^[a-z0-9\"]+) { set $supercache_uri ""; set $nocache "1"; } if ($http_user_agent ~ ^.*(2.0\ MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo\ Wii|Nitro|Nokia|Opera\ Mini|Palm|PlayStation\ Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian\ OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows\ CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915\ Obigo|LGE\ VX|webOS|Nokia5800).*) { set $supercache_uri ""; set $nocache "1"; } if ($http_user_agent ~ ^(w3c\ |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-).*) { set $supercache_uri ""; set $nocache "1"; } if ($http_user_agent ~ ^(DoCoMo/|J-PHONE/|J-EMULATOR/|Vodafone/|MOT(EMULATOR)?-|SoftBank/|[VS]emulator/|KDDI-|UP\.Browser/|emobile/|Huawei/|IAC/|Nokia|mixi-mobile-converter/)) { set $supercache_uri ""; set $nocache "1"; } if ($http_user_agent ~ (DDIPOCKET\;|WILLCOM\;|Opera\ Mini|Opera\ Mobi|PalmOS|Windows\ CE\;|PDA\;\ SL-|PlayStation\ Portable\;|SONY/COM|Nitro|Nintendo)) { set $supercache_uri ""; set $nocache "1"; } if ($supercache_uri) { rewrite ^ $supercache_uri last; break; } rewrite ^ /index.php last; } location ~ \.php { # 存在しないPHPファイルをシャットアウト if (!-f $request_filename) { return 404; break; } # fastcgi とfastcgi cacheの設定 include ./fastcgi.conf; fastcgi_pass phpfpm; fastcgi_cache wpcache; fastcgi_cache_key "$scheme://$host$request_uri"; fastcgi_cache_valid 200 10m; fastcgi_cache_valid 404 1m; # $nocache = "1"の時、fastcgi cacheが無効になる fastcgi_cache_bypass $nocache; } # よくアクセスされる静的ファイルにブラウザキャッシュが効くように設定 location ~ \.(jpg|png|gif|swf|jpeg)$ { log_not_found off; # 404の時にerror_logに書き込まないようにする設定 access_log off; expires 3d; } location ~ \.ico$ { log_not_found off; access_log off; expires max; } location ~ \.(css|js)$ { charset UTF-8; access_log off; expires 1d; } # ドット始まりのファイルはアクセスできないように location ~ /\. { deny all; log_not_found off; access_log off; } # リライトされたWP Super Cacheのファイル location ~ /wp-content/cache/supercache/${http_host}${uri}/index\.html(\.gz)?$ { charset UTF-8; internal; # この指定をしておくとURLを指定して直接アクセスできなくなる } location ~ /wp-admin/$ { rewrite ^/wp-admin/$ /wp-admin/index.php last; } }
処理の解説
Nginxのlocationディレクティブの評価順番は、wordpress.confを例にとると、すべてのアクセスが最初に “location /”ディレクティブを通ります。(記述順番は関係ありません。)
静的ファイルのチェック
このディレクティブの先頭でリクエストされた静的ファイルが存在するかのチェックを行なっています。
静的ファイルとして存在した場合、breakで”location /”ディレクティブの処理が終わります。次に、マッチするlocationディレクティブがあればそちらが実行されます。
例えば、cssファイルだった場合は、”location ~ \.(css|js)$”ディレクティブへ処理が続いていきます。これがB図-3の流れです。
WP Super Cacheのチェック、fastcti cacheのチェック
静的ファイルとして存在しなかった場合、処理は下へ進みます。
次の処理ではWP Super Cacheが生成したキャッシュファイルの存在をチェックし、キャッシュファイルを利用するかどうかを下に続く条件で確認していきます。各条件では同時にfastcgi cacheを利用するかどうかのチェックも行なっています。
ここでは、$supercache_uriが””(空文字)に書き換えられると、WP Super Cacheの静的キャッシュファイルが使われず(下の判定で利用されます)、$nocacheが”1″に書き換えられると、fastcgi cacheが使われません( ~ \.phpディレクティブで利用されます)。
$supercache_fileが存在し、$supercache_uriが空でない場合は、”location ~ /wp-content/cache/supercache/${http_host}${uri}/index\.html(\.gz)?$”ディレクティブへ処理が続きます。これがB図-4の流れです。
全てを受け止めるindex.php
静的ファイルのチェック、WP Super Cacheのチェックが終わっても処理が続く場合は、すべてにリクエストがindex.phpへと渡され、”location ~ \.php”ディレクティブへ処理が続きます。
すでにキャッシュされていて期限が有効な場合は、fastcgi cacheが使われ(B図-2)、キャッシュがなかったり、期限が切れていた場合は、FastCGI(PHP)へ処理が渡されます(B図-1)。
今回はステータスコード200の時は10分、404の時は1分キャッシュするように設定してあります。
404がキャッシュされない
目に見える記事部分のキャッシュが効いて応答が良くなると忘れてしまいがちですが、WordPressが返す404 Not Foundは無視できないくらい重い処理です。
404に対してキャッシュが作られないと、存在しない記事にアクセスがくる度にPHPとMySQLが仕事をしてしまいます。
そこで、fastcgi cacheの設定で “fastcgi_cache_valid 404 1m;” を指定しているわけですが、そのままだとキャッシュが効ききません(WordPress 3.2.1)。
調べてみると、WordPressは404の時、ヘッダーに “Cache-Control:no-cache, must-revalidate, max-age=0” を付けてレスポンスを返すことがわかりました。
このヘッダーがあるのでfastcgi cacheはキャッシュを作らないようなのです。
もっとスマートな方法があると思いますが、今回はWordPressのソースを少しいじって対応することにしました。
wp-includes/class-wp.php
function handle_404() { global $wp_query; if ( !is_admin() && ( 0 == count( $wp_query->posts ) ) && !is_404() && !is_robots() && !is_search() && !is_home() ) { // Don't 404 for these queries if they matched an object. if ( ( is_tag() || is_category() || is_tax() || is_author() || is_post_type_archive() ) && $wp_query->get_queried_object() && !is_paged() ) { if ( !is_404() ) status_header( 200 ); return; } $wp_query->set_404(); status_header( 404 ); //nocache_headers(); # ここをコメントアウト } elseif ( !is_404() ) { status_header( 200 ); } }
これで404もキャッシュされるようになりました。
おわりに
弊社はWordPessのイベント、WordCamp 2011 Tokyoに微力ながらスポンサーとして協力させて頂きました。
ブログにもWordPressの記事が少なく、弊社がWordPressにコミットしている感じが伝わっていないと思ったので、開催前に何とか間に合って良かったです。