こんにちは、エンジニアのセキヤです。
健康診断の前のお腹の空いている時にこの記事を書いており、ページキャッシュをベーコンキッシュと空目してしまいました。みなさんはどんなキッシュが好きですか?
さて本記事では、aumoのサイトのNuxtサーバの前にNGINXリバースプロキシを置いてページキャッシュを導入し、特定アクセス(ログイン中でないユーザからの特定パスへのアクセス)のレスポンスを爆速にした話をします。
目次
ページキャッシュ導入の経緯
aumoでは「東京のオススメ人気スポット30」といった、「あるエリアの人気スポット30選を紹介するページ」(以降では「一覧ページ」と表記)を以下のように4つのドメインを使って運営しています。
- https://gourmet.aumo.jp/prefectures/13
- https://leisure.aumo.jp/prefectures/13
- https://travel.aumo.jp/prefectures/13
- https://aumo.jp/prefectures/13
私の当面の目標は、この一覧ページがGoogle検索で上位に表示されるようにすることです。
過去の施策では、ページ内のユニークコンテンツの拡充などを行い、検索結果掲載順位の向上をある程度達成することができました。(施策をリリースしてから掲載順位への効果が大きく表れるには3ヶ月~半年ほどかかりました。)
しかしコンテンツが充実する一方、今度はレスポンス速度が低下してCore Web Vitalの評価が低下していることがわかりました。
Core Web VitalとはGoogleが定めるWebサイトにおけるUXを測る重要な指標のことです。これはPageSpeed Insightsというサイトで誰でも1ヶ月間の平均値を見ることができます。
aumoのとあるページのレスポンス速度に関する主な指標の測定結果は、Time to First Byte (TTFB): 2.8秒 First Contentful Paint (FCP): 3.7秒 Largest Contentful Paint (LCP): 4.3秒で軒並み不合格でした。
そこで、特に好調な幾つか(数千個)のページをページキャッシュすることで、コンテンツ増加の影響を受けないレスポンス速度の改善を目指します。
ページキャッシュ導入前システム概要
下図のような構成でした。詳しくはこちらの弊社TechBlogをご覧ください。
今回の変更では主に赤点線枠で囲ったEC2について手を加えます。
ページキャッシュ導入後システム概要
赤点線枠で囲ったEC2について、最終的に以下図のように、NGINXサーバを立てる構成としました。
NGINXサーバをNuxtサーバの前にリバースプロキシとして置いた形になります。
開発の流れ
要件
本開発の要件は以下でした。
- Nuxtのサーバ、またはAPIのサーバを更新する度に全てのページキャッシュを削除する
- ログイン中でないかつ対象パスへのアクセスにのみキャッシュを利用する
- 対象パスを指定できること
- ログイン中でないユーザからのアクセスにのみキャッシュを利用する
- PC用ページとSP用ページを別々にキャッシュする
NGINX × Nuxtの構成で実装
NGINX標準のproxy_cache_pathディレクティブを使って、EC2インスタンス内部に保存して返す方法を採用しました。
http {
......
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:8m max_size=1280m inactive=24h;
proxy_temp_path /var/cache/nginx/tmp;
......
}
これにより、要件1.のキャッシュ削除については、デプロイなどのインスタンスを入れ替える行為を行うことで実現可能です。
キャッシュ対象アクセスかどうかの判定の実装
次に要件2.のキャッシュ対象アクセスの判定です。
条件分岐というとif文が頭に浮かびますが、NGINXではif文は邪悪と言われておりなるべく使わないべきとされています。そこで、mapディレクティブで条件分岐を行うようにしました。
例えば、要件2.1.のパス判定を表す$is_target_pathは、mapディレクティブで以下のように実装しました。
map $host$request_uri $is_target_path {
default 0;
include /etc/nginx/cache_target_path.map;
}
ファイル /etc/nginx/cache_target_path.map には、以下のような行が数千行あります。
aumo.jp/prefectures/13/scenes/8 1;
aumo.jp/prefectures/27/categories/a12 1;
gourmet.aumo.jp/areas/466/categories/a15 1;
これで要件2.1.のキャッシュ対象パスである場合に1になり、そうでない場合に0になる$is_target_pathが用意できました。
別途要件2.2.のログイン判定を01で表す$is_logging_inを用意すれば、要件2.のキャッシュさせるかの判定を01で表す$is_not_use_cacheは、以下のように実装できます。
map $is_logging_in:$is_target_path $is_not_use_cache {
default 1;
0:1 0;
}
この$is_not_use_cacheをlocationブロックで以下のように設定することで、対象アクセスのみをキャッシュしキャッシュから探すように設定することができます。
location / {
......
proxy_cache_bypass $is_not_use_cache;
proxy_no_cache $is_not_use_cache;
......
}
デバイスによるキャッシュの出しわけの実装
要件3.のデバイス判定を表す$deviceを用意して、以下のようにcache keyに設定することで出しわけを実現できました。
location / {
......
proxy_cache_key $host$uri:$device;
......
}
これで全ての要件を満たせました。
効果検証
ではページキャッシュを実装してみてレスポンス速度がどれほど改善したかを見ていきます。
NGINXのログを確認すると、結果は以下でした。
status:200 request_time:0.000 upstream_response_time:- upstream_cache_status:HIT host:aumo.jp request_method:GET request_uri:prefectures/27/categories/a12
なんとNGINXは0.000秒でレスポンスしています!複雑かつたくさんの条件分岐を記述しましたが、0.000秒でレスポンスが返っているようでとても嬉しいです!
PageSpeed Insightsによる評価も見てみます。
以前は、TTFB: 2.8秒 FCP: 3.7秒 LCP: 4.3秒 で軒並み不合格で真っ赤になっていました。
キャッシュリリースにより、1ヶ月の平均値がTTFB: 0.5秒(2.3秒改善!) FCP: 1.3秒(2.4秒改善!) LCP: 1.7秒(2.6秒改善!)となり、それぞれの項目で合格ラインに入り、全体で見ても合格になりました!
掲載順位の向上、流入の向上も徐々に見られ始めています!
開発時の苦労
実はNGINX × Nuxtの構成の他にも様々検討していました。
技術選定の苦労
Nuxtの構成
nuxt generate もしくはnuxt exportを検討したが、機能自体が削除になっていたり、非推奨になっていたりして、過去の産物でした。
Nuxt × memcachedの構成
https://github.com/arash16/nuxt-ssr-cache (https://github.com/arash16/nuxt-ssr-cache/issues/23)
https://github.com/ziaadini/nuxt-perfect-cache
https://github.com/kimyvgy/nuxt-page-cache
どれもメンテナンスが微妙で断念しました。
この辺りで、NGINXの導入を検討し始めました。NGINXのリバースプロキシを導入しそこでキャッシュをすれば、Nuxtサーバへのリクエストを減らせて、Nuxtサーバの負荷を減らす利点が生まれると考えました。
NGINX × Nuxt × memcachedの構成
ngx_http_enhanced_memcached_moduleを使おうとしました。結論から言うとできませんでした。
READMEにmemcachedにページキャッシュを保存して返すことができるみたいなことが書いてありましたが、それはできることではなく今後やりたいことだったらしいです。
この方と同じ轍を踏んでました。https://github.com/bpaquet/ngx_http_enhanced_memcached_module/issues/1
READMEだけでなく実際の中身の実装を見ることも大事だと学びになりました。
OpenResty(拡張NGINX) × Nuxt × memcachedの構成
https://github.com/openresty/srcache-nginx-moduleとhttps://github.com/openresty/memc-nginx-moduleを使おうとしました。
docker の imageのバージョンの違いなどから色々と対応していきましたが、memcachedに保存されず探しにも行かず、ログも何も出ずで断念しました。
CloudFront × Nuxtの構成
月に何十万と追加運用費用が想定されること、今回の要件を満たすにはオーバースペックであること、から採用しませんでした。
そして結局、Nuxtサーバの前にNGINXのリバースプロキシを置くというシンプルな構成に落ち付きました。
追加の運用費用を一切なしでできたので、とても良かったです。
学んだこと
成功するまで挑戦すること
技術選定のタイミングで色々ありましたが、そこで諦めない力、公式ドキュメントを読む力、READMEだけでなく実際の中身の実装を見る力、GitHubのissueからほかの人の対応を学ぶ力、成功するまで費用対効果を考慮しつつ道を探し続け挑戦し続ける力を学びました。
初の試みは全体感の把握に努める
システム全体を通して初の試みは実装可能性や懸念事項などの全体感すら不明瞭でした。
実際に動かしてみると動かなかったり、RepositoryのREADMEから想像できる挙動とは異なる挙動であったり、が重なると工数の見通しが難しかったです。
初めと各工程で全体感を把握するべきだったこと、不測の事態が発生した際に工数の見通しを一気に伸ばす肌感を養うこと、が学びです。
最後に
aumoのNuxtサーバの前にリバースプロキシとしてNGINXを置いてページキャッシュさせることで、レスポンス速度を爆速にすることができました。
キャッシュや速度改善に興味のある方、aumoのサービスや技術に興味がある方は、ぜひ弊社選考を受けていただき、まずはカジュアルにランチからでもお話をしましょう!
ご興味がある方もない方も、是非 弊社採用ページ にお越しください!