SSR の CDN によるキャッシュ戦略
Adventarを支える技術 Advent Calendar 2019 の19日目です。
今日は Server Side Redering (以下 SSR) した結果を CDN でキャッシュする戦略について書きます。SSR の概要については以下にも概要を書いたので一読しておくとよりわかりやすいと思います。
上記に書いてありますが、今回は SSR を Lambda で行っていて、Lambda の実行はそんなに速くないし、実行回数での課金になるので、前段の CDN でキャッシュすることで、パフォーマンスとコストを最適化することができます。
その設計について書こうと思いますが、、先に書いておくと今回は SSR の結果をキャッシュせずに毎回リクエストのたびに Lambda を実行しています。そのあたりの理由も含めて書いていきます。
キャッシュのパージ
キャッシュはやれば速くなるのは確実ですが、コンテンツが更新されたタイミングで適切にパージしないと、新しいコンテンツがユーザーに届きません。パージの戦略は提供するコンテンツやキャッシュする範囲によっても様々です。
例えばブログサービスのようなサイトでページキャッシュするケースを考えてみます。単純に考えると、ブログ記事が更新されたときに対象のブログページだけパージすればよさそうですが、キャッシュする範囲によってはその限りではありません。
例えばブログにコメントや Like のような機能があって、それらも含めてキャッシュしているならコメントや Like が更新されたタイミングでも必要かもしれません。また、トップページや最新記事一覧ページのようなところに記事のコンテンツが表示されていて、それらのページもキャッシュしているなら、すべての記事作成、更新でそれらのページのパージが必要です。
さらに、はてなブログのようなログイン機能があったらどうでしょう。ログインしている状態でキャッシュをつくると最初にキャッシュしたときにログインしているユーザー情報が全員に表示されてひどいことになります。そんなことないだろ、と思うかもしれませんが気をつけていてもミスによってそういった状態になってしまう例をいくつも見てきました。最近ではメルカリのニュースが記憶に新しいです。
なのでキャッシュは狭い範囲で必要最小限に留めるべきです。例えば上記のようなブログサービスの例であれば、コメントや Like、ユーザー情報の表示はクライアント側(JS)でレンダリングする、キャッシュするのは記事ページのみに留める、などです。
今回の Adventar でも、キャッシュはカレンダーページ( https://adventar.org/calendars/3860 のようなページ)だけに留め、ログイン情報も SSR では扱わず、クライアント側で認証情報を付け足す、という設計にしました。認証については以下に書きました。
これで、キャッシュする範囲は最低限ですが、それでもパージするタイミングは少なくありません。
- カレンダーのタイトル、概要の更新
- エントリの登録・編集・削除
- ユーザー情報(名前、アイコン)の変更
- デプロイ
などです。このような操作が行われたときに、どうやってパージするかを見ていきましょう。
Fastly によるパージ
まず最初にこの設計を考えたときに一番手にあがったのは Fastly です。Fastly には Instant Purge という機能があり、パージリクエストをして 150ms 以内にキャッシュをパージしてくれます。
https://www.fastly.com/products/web-and-mobile-performance/caching-and-purging
パージの速さはユーザー体験につながるので、ぜひこれを使いたいところです。Fastly で具体的にどのようにパージするかを説明します。
- カレンダーのタイトル、概要の更新
- エントリの登録・編集・削除
これらは単純で、該当カレンダーページのキャッシュだけをパージすればいいだけです。Fastly のパージは非常にシンプルで、該当の URL に PURGE
メソッドでリクエストするだけです。
https://docs.fastly.com/api/purge
$ curl -X PURGE https://adventar.org/calendars/1
だけで済みます。特別なクライアントもいらず、非常に簡単です。
- ユーザー情報(名前、アイコン)の変更
はもう少し複雑で、そのユーザーが登録しているカレンダーページをすべてパージする必要があります。一つ一つ丁寧に上記のPURGE
メソッドでパージすることもできますが、数が多くなると負荷も大きくなりますし、fastly の API Limit もあります。こういうときに使えるのが Surrogate-Key
という機能です。
https://docs.fastly.com/en/guides/getting-started-with-surrogate-keys
この機能を使うと、キャッシュをタグのようなものでグルーピングして一括パージすることができます。リクエスト時にこのヘッダに複数の値を指定します。例えばSurrogate-Key
の値にu1 u2 u3
のように、そのカレンダーに登録しているユーザー ID のをもたせておきます。そして user_id: 1 のユーザーがプロフィールを更新したら、u1
を指定してキャッシュをパージすることができます。
- デプロイ
デプロイ時にはすべてのキャッシュを消す必要がありますが、それは purge_all という API があるのでこれを使います。
https://docs.fastly.com/api/purge#purge_bee5ed1a0cfd541e8b9f970a44718546
このように、やりたいことは完璧に実現できそうだったので Fastly を使いところではあったのですが、Fastly の料金は最低料金が $50/month ということで、コスト面が折り合わず今回は断念しました。
一方 CloudFront は、最低利用料金がなく、転送量だけであれば先日の記事にも書いたように、最もアクセスが多い12月でも$30〜$40ぐらいで落ち着きそうで、1月〜10月はほぼアクセスがないのでこれの 1/10 ぐらいに収まると思っています。個人サービスの上に1円も稼いでいないサービスなので $600/year はけっこうきついので、今回は CloudFront を採用しました。
なお、Fastly にはオープンソース向けの無償アカウントがあるので、これを申請してみようかと思っています。
https://docs.fastly.com/en/guides/accounts-and-pricing-plans#free-open-source-developer-accounts
CloudFront によるパージ
まず、CloudFront のパージは Fastly ほど高機能ではありません。パージの速度も Fastly ほど速くありません(公式のドキュメントが見つけられませんでしたが、少なくとも ms のオーダーではない)し、Surrogate-Key
のような機能もありません。
それだけであれば許容できると思ったのですが、調べてみると思ったよりパージに料金がかかることがわかりました。
https://aws.amazon.com/cloudfront/pricing/
月間で無効をリクエストしたパスの最初の 1,000 パスまでは追加料金なし。それ以降は、無効をリクエストしたパスごとに 0.005 USD かかります。
どのぐらいパージ処理が走りそうかを概算してみます。
- カレンダーのタイトル、概要の更新
- ユーザー情報(名前、アイコン)の変更
- デプロイ
については、回数も多くないし一旦無視します。問題は
- エントリの登録・編集・削除
です。去年ベースで考えるとエントリの数は13767、そのうちコメントが更新されているもの12063、URL が更新されているものが11893でした。Adventar ではこれらの更新処理はタイミング的に別々に行わられるので、単純に足し算になります。さらに削除や、コメントやURLを複数回変更する場合もあります。ここではざっくり全体の10%ぐらいで計算してみます。
- 13767 + (12063 * 1.1) + (11893 * 1.1) = 40118.6
これらはほぼすべて11月、12月で呼ばれるので、その2ヶ月でどのぐらいかかるかを出してみます。また、無料枠が 1000req/month あるので、それも加味します。
- (40118.6 - 2000) * 0.005 = $190.593
だいたい $100/month くらいといったところです。ざっくり計算ですが、カレンダーや登録数は年々線形に伸びているし、もっと多くなる可能性もありそうです。
11月と12月以外は無料枠に収まると思うので、トータルで見ると Fastly よりは安く済みそうですが、ピーク時のコストを10,000円前後ぐらいで考えていたので少し厳しい金額です。
なので、今回は CDN によるキャッシュを諦め、毎回 Lambda を実行することにしました。ちなみにこれによって Lambda の実行回数は多くなりますが、昨日の記事にも書いたように Lambda のコストは $0.4 ぐらいになりそうです。爆安。
まとめ
SSR の結果を CDN でキャッシュする戦略と、コスト面でその戦略を諦めた話を書きました。キャッシュによるパフォーマンスの最適化はユーザー体験にかなり寄与すると思っているので、Fastly のオープンソース向けの無償アカウントなどでコスト面の折り合いがつけばまたチャレンジしたいと思っています。
明日は Bugsnag によるエラートラッキングについて書きます。