Nginxのfastcgi_cacheでWordPressを高速化

WordPressの高速化施策ではNginxのリバースプロキシキャッシュが有名です。最近パフォーマンスを改善したくてPHP7だったりOPcacheだったりMySQLのクエリキャッシュなどやってたんですが、キャッシュを利用すると処理をバイパスするので段違いに応答が速くなります。数百ミリ秒だった応答が数十ミリ秒で返るので体感でもだいぶ変わってきます。

今回はNginxでキャッシュの設定を行うと共にWordPressプラグインのNginx Cache Controllerを利用して、管理画面からキャッシュの有効期限を設定できたり、キャッシュを削除できるように設定しました。便利です。

proxy cacheとfastcgi cache

Nginxのリバースプロキシキャッシュを調べてるとproxy cacheとfastcgi cacheの2通りのやり方が出てきます。仕組みの違いについては下記のサイトが図解付きで分かりやすくまとまっていました。

Nginxを使ったもう一歩進んだWordPressチューニング

リンク先の図解を見ると分かるようにproxy cacheのほうはNginxが2段構成になるのでやや冗長です。fastcgi cacheのほうがシンプルですね。対してproxy cacheはバックエンドがfastcgi以外の場合、キャッシュ用のNginxをロードバランサーも兼ねて別サーバーで構成する場合などに良さそうです。

今回はfastcgi cacheを利用します。

X-Accel-Expiresを使って有効期限を柔軟に変更する

Nginxのキャッシュの有効期限はいくつか設定方法があるのですが、X-Accel-ExpiresというHTTPヘッダーを利用するとWordPress側から柔軟に有効期限を変えることができます。

header( 'X-Accel-Expires: 86400' ); // 秒数を指定する

この仕組はproxy cacheでもfastcgi cacheでも同じようです。今回利用したNginx Cache ControllerもこのX-Accel-Expiresを利用しておりproxy cacheでもfastcgi cacheでも同じようにこのX-Accel-Expiresを通して有効期限を変更する事ができます。

Nginxの設定

まずnginx.confのhttpディレクティブの中にfastcgi_cache_pathを設定します。

http {

    fastcgi_cache_path /var/cache/nginx keys_zone=czone:10m levels=1:2 inactive=7d max_size=1000m;

    (省略)
}

fastcgiのパラメーターの意味はこちらが詳しいです。

keys_zoneでキャッシュパスのゾーン名とキャッシュのキー管理用のメモリサイズを指定します。inactiveで指定した期間アクセスがなかったキャッシュは削除されます(自動でお掃除してくれるようです)。max_sizeはキャッシュファイルの最大の総合計サイズです。

/var/cache/nginxをキャッシュのパスに指定してるのでこのフォルダにキャッシュのファイルが生成されます。

次にdefault.confを下記のように設定します。

server {

  listen       80;
  server_name  _;

  fastcgi_read_timeout 300;
  client_max_body_size 10m;

  root   /share;
  index  index.php index.html;

  error_page   500 502 503 504  /50x.html;
  location = /50x.html {
    root   /usr/share/nginx/html;
  }

  fastcgi_cache_key "$scheme://$host$request_uri";

  set $do_not_cache 0;
  if ($request_method !~ ^(GET)$) {
    set $do_not_cache 1;
  }
  if ($request_uri ~* "/wp-admin/|/xmlrpc.php|wp-.*.php|/feed/|index.php|sitemap(_index)?.xml") {
    set $do_not_cache 1;
  }
  if ($http_cookie ~* "comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in") {
    set $do_not_cache 1;
  }

  location / {
    try_files $uri $uri?$args $uri/ /index.php?$uri&$args /index.php?$args;
  }

  location ~ \.php$ {
    fastcgi_pass    unix:/var/run/php-fpm/php-fpm.sock;
    fastcgi_index   index.php;
    fastcgi_param   SCRIPT_FILENAME $request_filename;
    include         fastcgi_params;
    fastcgi_cache_bypass $do_not_cache;
    fastcgi_no_cache $do_not_cache;
    fastcgi_cache czone;
    fastcgi_cache_valid 200 302 7d;
    fastcgi_pass_header X-Accel-Expires;
  }

}

fastcgi_cacheで使用するキャッシュゾーンを指定してます。fastcgi_cache_validで指定しているのがキャッシュの有効期限です。ここではHTTPステータスコードが200と302の時に7日という形にしています。この値はPHP側でヘッダーにX-Accel-Expiresを指定するとそちらが優先されます。

あと$do_not_cacheという変数でコンテキストによってキャッシュ可否を制御しています。

Nginx Cache Controllerの設定

デフォルトではキャッシュの有効期限が1日(86400秒)になっています。必要に応じてこれを変更します。

NewImage

キャッシュの削除がとても便利です。この機能のためだけにこのプラグインを入れました。キャッシュディレクトリとキャッシュレベルの設定はnginx.confで指定したfastcgi_cache_pathの値と揃えます。

NewImage

これでサイトを閲覧しながらNginx Cacheのメニューからページのキャッシュを削除することができますし、投稿を追加したり更新したりすると自動でキャッシュが削除されます。便利。

モバイルで表示を変えてる場合のキャッシュの設定

モバイル対応してないか、あるいは完全にレスポンシブで作られたサイトなら問題ないのですが、UAを見て出力するHTMLを変えている場合はキャッシュの設定で注意が必要です。

fastcgi_cache_key "$scheme://$host$request_uri";

このキーの指定の仕方だとPCからのアクセスもモバイルからのアクセスも同じキーでキャッシュしているため、モバイルで見てるのにPC用のキャッシュが使われてしまったり、その逆もあったりしてしまいます。

そこで下記のようにNginxの設定でUAを判別しキーを変える必要があります。

  set $mobilef '';
  if ($http_user_agent ~* '(iPhone|iPod|Android.*Mobile|Windows.*Phone|dream|CUPCAKE|BlackBerry|webOS|incognito|webmate)') {
    set $mobilef 'mobile.';
  }
  fastcgi_cache_key "$mobilef$scheme://$host$request_uri";

これに合わせてNginx Cache Controllerもフックを利用してモバイルのキャッシュも削除できるように設定します。

/** Nginx Cache Controller **/

/**
 * $urlに該当するキャッシュファイルを削除する
 * @param $url
 */
function flush_cache( $url ) {

	$nginxchampuru = NginxChampuru::get_instance();

	global $wpdb;
	$table = $wpdb->prefix.'nginxchampuru';
	$sql = $wpdb->prepare(
		"select `cache_key`, `cache_id`, `cache_type`, ifnull(`cache_url`,\"\") as `cache_url` from `$table` where `cache_url` = %s",
		$url
	);
	$keys = $wpdb->get_results( $sql );
	$keys = array_map( function ( $key ) {
		return $key->cache_key;
	}, $keys );

	$caches = $nginxchampuru->get_cache_file( $keys );
	foreach ( $caches as $cache ) {
		if ( is_file( $cache ) ) {
			unlink( $cache );
		}
	}

	$sql = "delete from `$table` where cache_key in ('" . join( "','" , $keys ) . "')";
	$wpdb->query( $sql );

}
add_filter( 'nginxchampuru_flush_cache', 'flush_cache', 10, 1 );

/**
 * キャッシュファイルのキーをモバイルの場合とで分ける
 * - Nginxのキーの設定と同じになるようにモバイル判定ロジックを合わせる事
 * @param $url
 * @return string
 */
function get_key( $url ) {
	// 下記のキー決定のロジックはNginxと合わせている
	if ( is_mobile() ) {
		return md5( 'mobile.' . $url );
	} else {
		return md5( $url );
	}
}
add_filter( 'nginxchampuru_get_reverse_proxy_key', 'get_key', 10, 1 );

/**
 * UAでモバイルかを判定するロジック
 * @return bool
 */
function is_mobile() {

	$ua = array(
		'iPhone', // iPhone
		'iPod', // iPod touch
		'Android.*Mobile', // 1.5+ Android *** Only mobile
		'Windows.*Phone', // *** Windows Phone
		'dream', // Pre 1.5 Android
		'CUPCAKE', // 1.5+ Android
		'BlackBerry', // BlackBerry
		'webOS', // Palm Pre Experimental
		'incognito', // Other iPhone browser
		'webmate', // Other iPhone browser
	);

	$pattern = '/' . implode( '|', $ua ) . '/i';
	$match   = preg_match( $pattern, $_SERVER['HTTP_USER_AGENT'] );

	if ( 1 === $match ) {
		return true;
	} else {
		return false;
	}

}

上記のis_mobile関数で判別しているUAはNginxの設定と合わせる必要があります。

これでNginx Cache Controllerを使ってキャッシュを削除する際にPCとモバイルのキャッシュ双方が同時に消えてくれるようになります。

冗長構成下におけるキャッシュの削除

ちなみに単一のサーバーで運用している場合はNginx Cache Controller便利なんですが、冗長構成の場合だとキャッシュの削除機能は使えないですね。全てのサーバーでキャッシュ削除が同時に実行されるわけではないのでサーバーによってキャッシュがちぐはぐな状態で残ってしまいます。冗長構成になっている場合はNginx Cache Controllerではなく別の手段が必要です。

ぼくの場合はOpsWorksを使っているので、キャッシュを削除するレシピを書くのが手っ取り早いです。レシピを書いておけば任意のサーバーで同時に削除する事ができます。

追記(2016/2/24)

下記の現象を確認したのでこのページの設定を書き換えて更新しています。詳しくは下記をご覧ください。

Nginxのプロキシキャッシュで画面が真っ白になる