Nginxのキャッシュがうまく効いてなかったのを解決しました✌

2016/11/10

こんばんは、WordPress高速化ネタが大好きなあんそくです。

WordPressを使って運営しているGeeklesでは、高速で軽量なWebサーバーNginxを使っています。

Nginxのキャッシュ機能を使うことで、ページ生成にかかる時間を省き、WordPressの高速化を図っているわけなのですが、最近設定を見直していたら、キャッシュが効いていない問題を発見したので、その原因と対処についてまとめます。

サーバーの環境

  • CentOS 6.6
  • WordPress 4.6.1
  • Nginx 1.11.4
  • MySQL 5.5.42
  • PHP 7.0.12

Nginx -> php-fpmで動いています。ページキャッシュ系プラグインを使わずにFastCGIキャッシュだけを使用、ZendOpcacheとAPCuはEnable。

「Multi Device Switcher」というプラグインで、PC/タブレットとスマートフォンでテンプレートを分けています。

Nginxのキャッシュが効かない現象

色々とサーバー設定を調整して、レスポンスやキャッシュの効き具合を確認していたところ、何度かアクセスしてキャッシュがあることを確認したのに、別の端末で見るとキャッシュが効いてないという現象が起きていることに気づきました。

キャッシュが効いているかどうかは、キャッシュステータスをレスポンスヘッダに載せるようにして確認しています。

add_header X-Cache $upstream_cache_status;

※この1行を.confに書くだけでキャッシュステータスが確認できる

レスポンスヘッダ

Connection:keep-alive
Content-Type:text/html; charset=UTF-8
Date:Thu, 10 Nov 2016 09:49:57 GMT
Server:nginx
Set-Cookie:multi-device-switcher=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT; Max-Age=0; path=/
Transfer-Encoding:chunked
Vary:Accept-Encoding
Vary:User-Agent
X-Accel-Expires:259200
X-Cache:HIT
X-Cached:Wed, 09 Nov 2016 15:57:55 GMT

X-Cacheに、キャッシュが効いているときはHIT、効いていないときはMISSが返されます。
X-Cachedには、キャッシュされた日時が載るので、いつ作られたキャッシュなのかが分かる。

完全にキャッシュが効かない問題の場合

完全にキャッシュが効かないという問題と解決策はたくさん見つかります。
多くはno-cacheやmax-age=0などのヘッダがあったせいというものでしたが、私の環境で起きている「ちゃんとキャッシュは作られるけど、別の端末で確認すると効いてない」とは別問題。

ちなみに、全く効かないという問題は、キャッシュを無効にするno-cacheやmax-age=0のヘッダをプラグインが追加していることが原因だそうです。

find [WordPressディレクトリパス] -name '*.php' | xargs grep 'no-cache'

findとgrepで問題のプラグインを探すか、

fastcgi_ignore_headers Cache-Control Expires Set-Cookie;

そもそも、そんなヘッダがあっても無視をする、fastcgi_ignore_headers設定をNginx側でしてあげれば解決します。

私もfastcgi_ignore_headersを上記の用に設定していたので、ここは全く問題ないと思っていました。

原因を特定しよう

最初はキャッシュの有効期限が短すぎて、すぐにキャッシュが削除されてしまうことが原因かと思い、下記のコマンドでキャッシュの有効期限を上書きしているプラグインを探してみました。

find [WordPressディレクトリパス] -name '*.php' | xargs grep 'X-Accel-Expires'

私の環境では、Nginx Cache Controllerが見つかりましたが、これは自分で設定している期限にちゃんとなっていたので関係ありませんでした。

Nginx キャッシュの有効期限

Nginxのプロキシキャッシュ/FastCGIキャッシュの有効期限は、いくつかの箇所で設定できますが、X-Accel-Expiresが最も優先順位が高く、それ以外の低いものを上書きします。

  1. X-Accel-Expires ヘッダ
  2. Cache-Control ヘッダ
  3. proxy_cache_valid パラメータ(.conf内)

原因はVaryヘッダ

有効期限ではないとすると、何をトリガーにキャッシュが効いたり、効かなかったりしているのか……。

もう一度設定を見直したり、最近追加したものを確認したりして、原因を見つけました。

Vary User-Agent;

こいつだ!!

Varyヘッダは、「指定したヘッダの内容ごとに、返すレスポンスが変わる場合がある」ということを伝えるもの。キャッシュ設定がある場合、Varyヘッダごとに返すキャッシュが変わる設定になってしまいます。

Vary User-Agent;

これだと「ユーザーエージェントごとにレスポンスが変わるよ」

Vary Accept-Language, Cookie

これだと「ユーザー利用言語とクッキーによってレスポンスが変わるよ」

といった感じに。

つまり、私の環境ではユーザーエージェントごとにキャッシュを生成していたようです。まじかよ。

User-Agent:Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/48.0.2564.23 Mobile Safari/537.36

これが一致しないと同じキャッシュを返さない……。

わかりやすく考えても、端末名×OSバージョン×ブラウザ名×ブラウザバージョン……。どんだけパターンあるんだ……。

だから、作られたキャッシュは他の端末で使いまわせることが滅多にないので、いつ確認しても必ずキャッシュが効いてないように見えたというわけです。

解決方法

原因が分かれば解決は簡単です。

fastcgi_ignore_headers Cache-Control Expires Set-Cookie Vary;

こうすることで、Varyヘッダを無視します。終了。

レスポンシブデザインではなく、PC/タブレットとスマホでテンプレートを出し分けている場合でも、キャッシュキーをしっかり設定すれば大丈夫。

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

※$mobileは$http_user_agentでスマートフォンを判別して、@mobileなどの文字列を追加

Nginxのキャッシュが効かない対策まとめ

私の環境では、今回varyヘッダが原因でしたが、その調査をする過程で色々な対策を目にしたのでまとめておきます。

  1. add_header X-Cache $upstream_cache_status;でキャッシュの状態を確認できるようにする
  2. HTTPヘッダをよく確認して、X-Accel-Expires, no-cache, max-age=0でキャッシュの無効や短い有効期限になってないか確認
  3. 原因となっているヘッダを出力しているプラグインやテーマを特定するためにgrep
  4. 原因のコードを変更するか、設定を変更するか、プラグインを停止する
  5. または、fastcgi_cache_keyで原因となっているヘッダを無視してしまう

こんな感じでチェックしていけば、キャッシュがうまく効かない問題は解決できると思います!