nginxでTTFBが長い時はfastcgi_cacheを設定してみよう(WordPress&PHP7.3&nginx&CentOS6)

ページの読み込み速度には、そのサイトの設計思想やフロントエンドの最適化が大きく関わってきます。
少し前に、このサイトのリニューアルでソースを一から書き直したのですが、完成から今日までの1週間はページの読み込み速度を短くする為に色々とやっていました。

CSSやJSを圧縮し、それぞれを一つのファイルにまとめてHTTPリクエストの数を減らしたり、HTTP2プロトコルを使ったり、Gzip圧縮をしてみたりと色々やってページ自体の読み込み速度はそれなりに改善できたのですが、PageSpeed Insightsにサーバーの応答時間(TTFB)が遅いと言われてしまいました。

ちょっと気になってしまったので、TTFBをどうしたら改善できるか調べてみる事に。

TTFB = Time To First Byte

TTFBとはTime To First Byteの略で、その名の通り「最初の1Byteが届くまでの時間」。
言い換えれば、ブラウザ(クライアント)がページを読み込み始めてから、最初のデータが実際にクライアントに届くまでの時間です。
つまりそれはサーバーがリクエストを受け取ってからどれぐらいの速さでそれに応答しているかという数値。

普段ブラウジングをしていてそこまで意識することはないかも知れませんが、ブラウザが読み込みを始めてからゲージが一気に動き始めるぐらいまでの間がそのTTFBだと思っておくと良いはず。

静的コンテンツ(サイト)の場合このTTFBは大抵50ミリ秒(ms)以下らしいです。
この程度ならユーザーがそれを体感することはほぼ無いのですが、WordPressなどで動的にコンテンツを生成しているサイトだと多くの場合、速くてもわずかに体感できるほどの長さになってしまいます。
それでも大体300~400ms以下ならば、そこまで気にはならないはず。

500ms以上になると、0.5秒を超えてくるので分かりやすくモタつきを感じます。
ここまでくるとTTFBが明らかに長いと思っておいた方が良いかも。

TTFBを計測してみる

TTFBの計測方法にはいくつか種類があります。

  1. curlを使って純粋なレスポンスタイムを取得する
  2. ブラウザの開発者ツールを使う
  3. webpagetest.orgGTmetrixなどの診断ツールを使う

1番は下記のコマンドを入れるだけのシンプルな方法。
https://google.com/の部分を計測したいurlに置き換えてください。
TTFBは秒単位で表示されるので、この場合は126msということになります。

LEoREo-no-MacBook-Pro:~ LEoREo$ curl -o /dev/null -w "TTFB: %{time_starttransfer}\n" https://google.com/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   220  100   220    0     0   1744      0 --:--:-- --:--:-- --:--:--  1746
TTFB: 0.126028

2番の方法は、ブラウザで開発者ツールを起動し、Networkタブに切り替えることでTTFBを確認できる方法です。
Networkタブに切り替えた後に一度ページを再読み込みさせる必要があります。
どのブラウザでも確認方法は大体同じなのですがChromeの場合はWaterfallで並び替えてから、一番上の項目にカーソルを重ねるとWaiting(TTFB)というラベルで表示されます。

3番目はページ速度診断ツールを使ってTTFBを確認する方法。
例えばwebpagetest.orgだと、まずはurlを入力して出た結果の1番目の画像をクリック。
途中にあるRequest DetailsにTTFBがあります。


当初のbeehivegaming.gaのTTFBはサーバーの性能があまり高くない事もあって不安定で、遅い時は2秒以上かかる時もありました。

LEoREo-no-MacBook-Pro:~ LEoREo$ curl -o /dev/null -w "TTFB: %{time_starttransfer}\n" https://beehivegaming.ga/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 24073    0 24073    0     0  12352      0 --:--:--  0:00:01 --:--:-- 12351
TTFB: 1.235933

どうせなので他のサイトのTTFBも調べてみることに。

Twitter。274ms。

LEoREo-no-MacBook-Pro:~ LEoREo$ curl -o /dev/null -w "TTFB: %{time_starttransfer}\n" https://twitter.com/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  241k  100  241k    0     0   421k      0 --:--:-- --:--:-- --:--:--  421k
TTFB: 0.274072

Wikipedia。322ms。

LEoREo-no-MacBook-Pro:~ LEoREo$ curl -o /dev/null -w "TTFB: %{time_starttransfer}\n" https://www.wikipedia.org/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 82849  100 82849    0     0   165k      0 --:--:-- --:--:-- --:--:--  166k
TTFB: 0.322011

Robocraft公式サイト。1354ms。
紅茶鯖なのはゲームだけじゃなかった

LEoREo-no-MacBook-Pro:~ LEoREo$ curl -o /dev/null -w "TTFB: %{time_starttransfer}\n" https://robocraftgame.com/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 68459  100 68459    0     0  42427      0  0:00:01  0:00:01 --:--:-- 42415
TTFB: 1.354186

これ以外にも一通り調べてみた結果、一部の例外はあるもどこも大体200~400msぐらいでした。
ちなみにGoogleは200ms以下のTTFBを推奨しているようで、100ms以下でもっといい!という感じみたいです。

nginxのfastcgi_cacheを使う

nginxにはFastCGIキャッシュという機能があり、これを使うことでPHPコールの結果を一時的に保管させることができます。
一度PHPで動的に生成したページを、キャッシュとして保存させることで2回目以降もPHPを再度実行する必要がなくなるため、TTFBの大幅な短縮が期待できます。

WordPressのキャッシュ系プラグインの多くも同じようにページのキャッシュを保存して高速化を図っていますが、FastCGIキャッシュはWordPressのPHPそのものを一切介さずにWebサーバーであるnginxの内で全てが完結するため非常に高速で効率的です。
似たようなものにプロキシキャッシュがありますが、これはFastCGIに絞っているのでこっちの方が効率的なはず(?)。

とはいえメリットだらけでも無いのですが(後述)、ひとまずはサーバーのレスポンスをできるだけ速くしたいので早速設定をしていきます。
サーバー環境は下記の通り。

  • さくらVPS1G
    • CentOS 6.10
    • nginx/1.15.7
    • PHP7.3.0
    • WordPress4.9.8/5.0

まずは/etc/nginx/nginx.confのhttpセクションを編集します。

http {
    fastcgi_cache_path /var/cache/nginx/phpwpcache levels=1:2 keys_zone=phpwpcache:100m inactive=8h;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";

    #以下略
}

上記のfastcgi_cache_pathの設定はあくまで一例ですが、他にもこのようなオプションを指定できます:

  • パス: キャッシュの場所。どこでも問題ないはずだが/var/cache/nginx/以下に置いておくのが無難。
  • levels: キャッシュの構造レベル。1:2で良い
  • keys_zone: キャッシュのゾーン名とキャッシュキー管理のメモリ量。ゾーン名は適当で良い。
  • max_size: キャッシュの最大量。これを超えると最も長い間使われていないものから消えていきます。上記では指定していませんが、必要なら指定しておきましょう
  • inactive: キャッシュの有効期間。これを超えたものは最大量に関係なく削除されます。短くしすぎるとキャッシュする意味がなくなり、長くしすぎても更新の反映が遅くなります

これ以外は特に指定しなくてもいいと思います。一応全オプションは公式ドキュメントで確認できます。

次に、/etc/nginx/conf.d/内の.confファイルを編集します。

server {
    listen 443 ssl http2;
    server_name beehivegaming.ga www.beehivegaming.ga;
    root /var/www/html/beehivegaming;
    index index.php index.html index.htm;

    # 略

    location ~ \.php$ {

        # 略

        #ここを追記
        fastcgi_cache phpwpcache;
        fastcgi_cache_valid 200 8h;
        add_header X-Fastcgi-Cache $upstream_cache_status;
    }

    # 以下略
}
  • fastcgi_cache: 先ほど設定したキャッシュのゾーン名と同じ名前を指定する
  • fastcgi_cache_valid: どのHTTPコードのレスポンスをどの間保存するか。複数のレスポンスに対しては200 302 8hと指定する。時間は先ほど設定したinactiveと同じ時間で良い
  • fastcgi_cache_methods: どのリクエスト形式を保存するか。デフォルトのGETとHEADで十分なので上記の例では指定していない
  • add_header X-fastcgi-Cache…: キャッシュが使用されているかどうか確認できるようにX-Fastcgi-Cacheヘッダーを追加している

編集が終わったらservice nginx restartで再起動させましょう。
reloadでもいいかも。

FastCGIキャッシュが使われているか確認する

ブラウザの開発者ツールを立ち上げてHTTPヘッダーを確認します。
先ほど設定したX-Fastcgi-CacheヘッダーがHITとなっていればFastCGIキャッシュが使用されています。

TTFBが短くなったか確かめる

この状態でもう一度beehivegaming.gaのTTFBを確認してみます。
ページをキャッシュするために最初に何度かアクセスしてあげる必要がありますが、一度キャッシュされると…

LEoREo-no-MacBook-Pro:~ LEoREo$ curl -o /dev/null -w "TTFB: %{time_starttransfer}\n" https://beehivegaming.ga/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 23767    0 23767    0     0   503k      0 --:--:-- --:--:-- --:--:--  504k
TTFB: 0.041966

おお、速い!
ブラウザがデータをダウンロードする時間も含めるともう少し長くなるはずですが、それでも41msはほぼ一瞬ですよね。
細かいwebサーバーベンチマークは省きますが、beehivegaming.gaのPageSpeed Insightのスコアも大幅に改善しました。

GTmertixも参考までに確認してみました。
香港のサーバーからなのでTTFBが若干長くなってしまっていますが、それでもFully Loaded Timeは1.2秒なのでもう満足です。

FastCGIキャッシュの懸念点

FastCGIキャッシュはPHPの実行結果、つまり動的に生成されたページをそのまま一定時間保存するので、もしその間にサイトに何かしらの変更を加えるとキャッシュが自然に削除されるまでその変更内容が反映されません。

キャッシュが自然に削除されるまで待たなくても要はキャッシュをディレクトリごと消せればいいのですが…
nginxにページ内容の変更を検知して自動でキャッシュをリフレッシュするような機能はありません。
厳密には有料版のnginxだと似たような機能があるのですが、通常のnginxだと手動で対象のディレクトリをまるごと消してキャッシュを削除するか、外部のツールを頼るしかなさそうです。

WordPressのプラグインとかで投稿を公開したと同時に自動でキャッシュディレクトリを削除するようなもの、作れるかも?
そのうち色々試してみたいと思います。
とりあえず今はこれでいいや(放棄

おわり。