CloudFrontとS3を活用したキャッシュ戦略:APIパフォーマンス改善への実践的アプローチ
概要
キャッシュ理解が曖昧な状態でキャッシュを活用した機能開発を行ったせいで、サーバーへの負荷を軽減することができずにパフォーマンスの悪い実装をしていました。
当たり前ですが、適切にキャッシュを利用しないと意図せずサーバーへ負荷を与え続けてしまいます。
そうならないためにも今回は、キャッシュの基本と私が行ったパフォーマンス改善の内容をご紹介します。
キャッシュとは
アプリケーションに頻繁にアクセスされるデータを一時的に保存しておくものです。
ストレージに一時的に保存しておけば、データベースへのリクエストを減らしたり、計算処理をすることなくレスポンスできたりするので、負荷を削減しつつ高速にデータ処理を行えるようになります。
リアルタイムな情報や、頻繁に更新されるコンテンツには不向きなので、状況に合わせて使用するようにしましょう。
キャッシュの種類
-
CPUキャッシュ
- システムレベルのキャッシュ
- PCのスペックにも影響を受ける。
- 種類
- L1キャッシュ
CPUコアに最も近いキャッシュ。高速で処理するのに優れている。 - L2キャッシュ
L1キャッシュよりかは保存できるキャッシュサイズは大きいが、その分処理速度は落ちる。 - L3キャッシュ
L2キャッシュよりもさらに保存できるキャッシュサイズが大きい。しかしその分処理速度は落ちる。
- L1キャッシュ
-
ディスクキャッシュ
- ハードディスクやSSDディスクにキャッシュを作り、読み込み書き込みのパフォーマンスを向上させる。
- OSレベルでも実装することがあり、その場合はRAMの一部を使用する。
-
ウェブブラウザキャッシュ
- Webページ、画像、スクリプトなどを保存するキャッシュ。
- 最初の読み込み後のリクエスト時には、ブラウザのみで完結する
- 似たニュアンスで「HTTPキャッシュ」があるが厳密には違う
-
HTTPキャッシュ
- HTTPリクエスト全体のキャッシュ
- クライアントからのリクエストをクライアント環境だけで完結する。
- サーバーサイドにはリクエストが到達しない。
-
アプリケーションキャッシュ
- 頻繁にアクセスされる計算結果を保存するキャッシュ
- Railsに備わっているキャッシュ機構もこれに該当する
- サーバーサイドでキャッシュをチェックするので、毎回サーバーリクエストが発生する。
- APIで取得する際のレスポンスデータやセッション情報もここに保存できる。
- サーバー側とクライアント側の両方で実装可能なキャッシュ
-
データベースキャッシュ
- 頻繁にアクセスされるクエリの実行結果や、インデックスなどをメモリ上に保存するキャッシュ
- データベース管理システム(DBMS)が自動で保存して管理する。
- アクセス数は多いが、データ更新が少ない場合に利用する。
-
分散キャッシュ
- 複数のサーバーで共有されるキャッシュ
- Memcached, Redisがここに該当する。
-
エッジキャッシュ(CloudFrontなどのCDNキャッシュ)
- 地理的に分散したエッジサーバーにキャッシュを生成することができる。
- ユーザーとの物理的に近い距離でコンテンツを配信するため通信速度が速くなる。
- 静的コンテンツのキャッシュに効果的
問題
キャッシュを効かせると言ってもクライアント側で効かせるのか、サーバー側で効かせるのかなど様々なアプローチ方法があります。
今回開発していた機能は大量データかつ、大量リクエストが来るAPIを活用する必要があったため、サーバー側への負荷が予想されました。
リクエストされる度に常に最新の内容をユーザーへ返す必要はない機能だったので、キャッシュを活用してサーバーへの負荷を軽減することが可能でした。
サーバーへの負荷を軽減させれるようなキャッシュを作成する必要があったので、クライアント側でキャッシュを生成する必要があったのですが、私の理解不足でクライアント側ではなく、サーバー側でアプリケーションキャッシュを使用してキャッシュの実装を行いました。
アプリケーションキャッシュはサーバー側(アプリケーション側)でキャッシュの有無をチェックするため、クライアントのリクエストは常にサーバーに到達してしまいます。
つまり、たとえキャッシュを使っていたとしても毎回サーバー側でリクエスト処理を行っているため、今回行いたかった対策にはなっておらず、サーバーの負荷は高いままになっていました。
対策方法
サーバー側(アプリケーション側)にリクエストを飛ばすAPIを叩かなないようにして、なおかつCDNでキャッシュを効かすようにして対策を行いました。
具体的には、S3とCloudFrontを使用しました。
CloudFrontのエッジロケーションでキャッシュを生成して、オリジンサーバー(S3)へのリクエスト数を減らすことで、パフォーマンス改善以外にも、通信時のコストも抑えられるので一石二鳥です。
CloudFrontとS3の活用
S3ではバケットを作成して、そのバケット配下に今回使用するデータが保存されるようにします。
CORSの設定もお忘れなく。
(CORSとは異なるオリジン間でデータ通信できるようにすることができる仕組みのことです)
CloudFront側で参照するためのS3のバケットをディストリビューションと紐づけます。
さらに必要に応じて、キャッシュポリシーを設定します。
キャッシュを更新する場合は、クエリパラメータを付与して変更してあげれば呼び出すpathがわかるため、再度新しいリソースでリクエストして新しいデータを取得することができます。
クエリパラメータは、CloudFront側のポリシーで許可した文字列であれば使用できるので、必要に応じて設定をしてください。
CloudFrontのキャッシュの仕組み
CloudFrontでキャッシュを生成する仕組みは「エッジキャッシュ」と言います。
理由は、エッジロケーションからコンテンツを配信するからです。
エッジロケーションというのは、世界中の主要な都市や地域に地理的分散して配置されている「エッジサーバー」のことを言います。
エッジサーバーを利用することで、オリジンサーバーがあるロケーションよりもユーザーに近い距離からコンテンツを配信することができます。
配信される距離が近いほど処理が高速になるのでとても便利です。
そしてエッジキャッシュは、ユーザーがオリジンサーバーへリクエストする前に、ユーザーの近い距離にあるエッジサーバーにコンテンツのキャッシュを作り出しておき、キャッシュがあればオリジンサーバーへリクエストすることなくコンテンツをユーザーへ配信することができるので、パフォーマンスの改善に大きく寄与します。
CloudFrontのキャッシュ生成の確認方法
検証ツールのNetworkタブから対象のリクエストを探します。
その中のResponse Headers内に「x-cache」があります。
ここに「Hit from CloudFront」と出ていればキャッシュが生成されています。
CloudFrontのようなCDNのキャッシュの確認は、ブラウザキャッシュがあると確認しづらいので、検証ツールの「Disable cache」をチェックしてブラウザの更新をして確認することをお勧めします。
また初回リクエストからキャッシュ生成まで数秒のラグがあるので、2回目のリクエスト時にキャッシュが作られていなければ、数秒ほど時間をおいてリクエストしてみてください。
CloudFrontとエッジロケーションの関係
CloudFrontとエッジロケーションの関係をもう少し噛み砕いて説明してみます。
わかりやすい例として、ピザの宅配サービスに例えます。
-
CloudFront = ピザの宅配サービス全体
ピザの注文から配達までを管理する全体的なシステム -
エッジロケーション = 各地域のピザ店
顧客により近い場所に配置された実際のピザ店舗です -
動作の仕組み
お客さん(ユーザー)がピザ(コンテンツ)を注文すると、最寄りのピザ店(エッジロケーション)が対応します。
もし注文されたピザがお店にある(キャッシュがある)なら、すぐに配達します。
もしお店にないなら、本部(オリジンサーバー)から取り寄せて、それから配達します。
つまり、CloudFrontはサービス全体を指し、エッジロケーションはそのサービスを実現するための各地域の拠点を指します。エッジロケーションはCloudFrontが使用するインフラの一部で、ユーザーにコンテンツを素早く届けるための重要な役割を果たしてくれます。
改善効果
CloudFrontとS3を活用したことで、結果的に読み込み時間が200倍以上早くなりました。
before | after |
---|---|
ここまで改善した理由としては、元の実装がよくなかったこともありますが、CloudFrontとS3を活用することでこれだけのパフォーマンス改善をすることができました。
まとめ
今回の経験から、適切なキャッシュ戦略の重要性を再認識しました。
私が最初に実装したサーバー側のキャッシュ実装では十分な効果が得られませんでしたが、CloudFrontとS3を活用することで、APIのパフォーマンスを大幅に改善することができました。
キャッシュを実装する際は、単にキャッシュを導入するだけでなく、どのレベルでキャッシュを行うべきかをよく考え、それがサーバーの負荷軽減にどのように影響するのかを十分に検討することが重要だとわかりました。今回の場合、CDNを利用したエッジキャッシュが効果的でした。
この経験を通じて、問題の根本原因を正確に把握し、適切な技術を選択することの重要性を学びました。
今後は、より一層パフォーマンスを意識した設計と実装を心がけて、業務に取り組んでいこうと思います。
Discussion