こんにちは。はじめまして。アメーバブログでフロントエンドエンジニアをしている@satominです。

2019年8月末、アメーバブログ(以下、アメブロ)の閲覧面においてCDN(Fastly)の導入を開始し、その後、現在進行系で日々パフォーマンスチューニングを進めています。世の中は2020年へと突入し、少し時間が経ってしまいましたが、本記事では、アメブロのフルCDN化 苦労話 についてお伝えできればと思います。

はじめに

弊社でフルCDN化と聞けば、こえのブログの事例が思い浮かぶかもしれません。

SSRアプリの一筋縄ではいかない方のパターンをお話する予定・・・です!

今回の記事は、上記ツイートにもある一筋縄ではいかない方のお話です。。。

これまでのアメブロ

2004年から現在まで15年続くブログサービスで、2017年頃からAkamaiのCDNを利用していましたが、ほとんどキャッシュしておらず、していても一律同じ有効期限&イベント駆動パージはしていなかったので、とても有効な使い方が出来ていたとは言えない状態でした。また、アプリケーションも(特にアメブロの閲覧面は)キャッシュしてもよいコンテンツを返していますが、ほとんどのコンテンツがHTMLに含まれているなどそもそもキャッシュに最適な作りになっていませんでした。

方針と目標

キャッシュ戦略を練るにあたってはじめに大まかな方針と目標を決めました。

  • ameblo.jp ドメインで配信するHTMLとAPIをキャッシュ対象とする
  • 基本的にOriginサーバーのレスポンスヘッダーでキャッシュルールを管理する
  • 既存のURLパスは変更しない
  • サーバーレスポンスタイムの減少&Originサーバーの負荷軽減を目標とする
  • 初期リリースでHIT率は重要ポイントに置かない(後々チューニングして上げていく)

立ちはだかる問題たち

よし!やるぞ!と張り切ったものの、やはり一筋縄ではいきません。まずはキャッシュに適したアプリケーションへと構成を変える必要がありました。主にぶち当たった問題は以下の通りです。

エンドポイントが多い問題

キャッシュ戦略を立てるためにエンドポイントの把握は必須でした。ここは気合でなんとか…。ソースコードからエンドポイントを洗い出して一つずつ設定を考えていきましたが、関連リポジトリも多数存在していたので時間もかかりました。

エンドポイントを洗い出したリスト
エンドポイントを洗い出したリストの一部

システム的に用意しているエンドポイント以外は、後述する漏れ検知の仕組みに頼ることにしました。

Originサーバーがたくさんある問題

アメブロでは機能ごとにOriginサーバーが分かれていることが多く、また、その機能が作られた時期やその時のトレンドによってシステム構成や開発言語も異なっていました。これも「がんばる」以外方法がなかったのですが、とにかく大変でした…(ただの感想ですね)。

全部SSRで返している問題

サーバーサイドから返却されるレスポンスはHTMLに全部のせ状態でした。ここには更新頻度の高いデータやランダム性のあるデータも含まれていました。このままではほとんどのコンテンツがキャッシュ出来ないため、SSR(サーバーサイドレンダリング)で必要なものをピックアップし、それ以外をCSR(クライアントサイドレンダリング)に変更しました。2016年9月にフロントエンドがJavaベースのアプリから、node.js・Reactベースのアプリへとシステム移行されたため、CSRへの変更はそれほど難しくありませんでした。
アメブロ2016 ~ React/ReduxでつくるIsomorphic web app ~
上記記事内でも言及されている「ガタンッ」問題は、今回も予め高さを取ることで回避しました。

キャッシュしてはならないが送信数があるエンドポイントの存在

リクエスト数は多いが、どうしてもキャッシュできないエンドポイントがありました。Fastlyではリクエスト数に応じた課金があるため必要コストの増加が懸念されます。今回は、不要な送信処理を削除したりサンプリングを行ったりして送信数を削減し、その上でキャッシュをしない設定にしました。将来的にはこれらのエンドポイントを乗せるドメインを分離、送信方法の変更などを検討しています。

ルールに漏れたエンドポイントの存在

15年の開発積み重ねは伊達ではありませんでした。プロジェクトのソースコードだけでは把握しきれないエンドポイントがあったのです。そこで、Originからのレスポンスにキャッシュルールが存在するか否かをカスタムヘッダーを付けて検知できるようにしました。また、CDNに到達したリクエストをDatadog Logsで見れるようにして、ルールに漏れたエンドポイントがないかを確認しました。

本当に必要なクエリパラメータがわからない問題

本来、見た目に影響を与えるクエリパラメータ以外はCDNキャッシュのパスに含める必要がありません。しかし、メインシステム以外のエンドポイントでパラメータが結果にどういう影響を与えるのかすべてを限られた時間で把握することは困難でした。そのため実際のアクセスログから不要なクエリをブラックリスト形式で除外する方法を取りました。将来的にはホワイトリスト形式にしたいと思っています。

データ更新箇所がバラバラ問題

メインシステムのデータ更新処理はシステム刷新のタイミングで統一されていたので大変実装しやすかったです(とは言え量は膨大でした汗)。
システム刷新については以下のスライドで詳しく紹介されているので参考にしてみてください。

しかし、サブシステムや社内用に開発された独自の管理ツールなどはデータ更新機構をそれ自身が持っていました。ユーザ影響が無いことと対応時間が足りなさそうだったため、初期リリースでは内部ツールにパージ処理を実装することを諦め、代わりにSlackのアプリを使ってcurlコマンドがわからなくてもパージ出来る仕組みを提供しました。

多段キャッシュされている問題

ブラウザ、CDN、Origin(Web Server)、API、DB、それぞれの間にキャッシュが存在する
負荷軽減目的で様々なところで一定時間キャッシュされているコンテンツがありました。この場合、データ更新時にCDNキャッシュのパージリクエストを送信しても後ろに控えているアプリケーションが古い情報を返す可能性があります(定期的にコンテンツを再読み込みする系)。stale-while-revalidateを利用することで、キャッシュから返せる割合を担保しつつコンテンツに変更があった場合に新しい内容を返すことが出来ます。と、書いておきながらまだアメブロでは適用していないので、今後試していく予定です。

URLルールが統一されていない問題

アメブロ2019: こえのブログでのPWA (CDN前提のURL設計とキャッシュ設定 の項目参照)
こえのブログのようにCDNありきで設計されていなかったためエンドポイント1つずつ処理とコンテンツの内容を追って把握してからSurrogate-KeyやSurrogate-Controlを決定しました。中には \\\ ドウシテコウナッタ // という実装もあり、かなり心が削られた部分でもありましたw

ついにFastly利用開始

色々な問題を解決しつつ、Fasltyキャッシュに必要な設定をしていきました。そして、一気にすべてを切り替えるのではなく、段階的な切り替えを実施しました。これは変更に対する効果検証をしやすくする目的とあとは若干のビビリが理由ですw

はじめに素通りリリース(Faslty上でキャッシュはせず、リクエスト経路を変更するだけというなんとも贅沢な(もったいない)使い方)をしました。

Fastly導入直後のリクエスト数の推移

トラフィックが増えていく様子は圧巻です。
続いてFaslty上で短い時間のキャッシュを開始しました。

CDNキャッシュ開始直後のリクエスト数の推移

素通りしていたリクエストがキャッシュされるようになり、PASSESステータスが減りました。まだキャッシュのHIT率は低いです。

パフォーマンスチューニング

ひとまずFaslty化が出来たので、ここからはHIT率の向上を図るべくチューニングを行いました。以下はその一例です。

  • クエリのノーマライズ
    見た目に影響を与えないクエリパラメータをキャッシュパスから除外
  • Varyヘッダーから不要なUser-Agent
    User-Agentによらず同一コンテンツを返して良いエンドポイントのVaryヘッダーからUser-Agentを削除
  • CDN上でのキャッシュ時間を延長
    各パージ処理に漏れがないことを確認した上でよりCDNでレスポンスが返せるようにTTLを延長
  • 更新頻度の異なるコンテンツを別APIに分離
    A(更新頻度低い)とB(更新頻度高い)を一緒に返していたAPIを、Aだけ返すAPIにしてTTLを延長&Bだけ返すAPIに分離して非同期取得したりHTMLから削除したりしてパージ対象を狭くする

一定時間おきにHIT率が大きく低下する新たな問題が発生

CDNキャッシュの様子を眺めていると、一定時間おきにHIT率が大きく低下する現象が発生していました。

一定時間ごとにCDNキャッシュのHIT率が低下する様子を表したグラフ
一定時間ごとにCDNキャッシュのHIT率が低下する様子を表したグラフ

調査の結果、とあるバッチの影響で人気な記事ほど広範囲なパージが高頻度で発生 = キャッシュが長生きしない負の連鎖が起きている事がわかりました。このままではどれだけCDN上でのキャッシュ時間を延長してもその効果を受けづらくなってしまいます。パージ対象のデータがSSRのHTMLに含まれていることも原因の一つだったので、以下を行いHIT率の改善を目指しました。

  • 該当データのみを取得するエンドポイントを新設&専用のSurrogate-Keyを設定
  • 描画処理をCSRに変更
  • パージする際に指定するSurrogate-Keyを新設したものに変更

改善後の同時間帯のHIT率です。一定時間ごとの低下が改善されグラフの上下がなだらかになり、全体的なHIT率も大きく向上しました。また、キャッシュカバレッジにも向上が見られました🎉

パフォーマンス改善後にHIT率が安定した様子を表したグラフ
パフォーマンス改善後にHIT率が安定した様子を表したグラフ
Fastly管理画面上でのキャッシュHIT率の推移グラフ
Fastly管理画面上でのキャッシュHIT率の推移グラフ

CDN化やチューニングによる効果

リクエスト数については具体的な数値を出せないので規模感がうまく伝わらない気がしますが、雰囲気だけでも感じ取っていただければ幸いです。

HIT率

CDNキャッシュ開始時、及びパフォーマンス改善時に大きく向上しました。

CDNキャッシュのHIT率推移を表したグラフ
CDNキャッシュのHIT率推移を表したグラフ

キャッシュカバレッジ

CDNキャッシュのTTL延長によりキャッシュカバレッジも向上しました。

CDNのキャッシュカバレッジ推移を表したグラフ
CDNのキャッシュカバレッジ推移を表したグラフ

Originのリクエスト数

導入前の半分以下になりました。これにより、Originサーバーの台数を減らすことが出来ました。

Originのリクエスト数推移を表したグラフ

CPU使用率

元々CPU使用が多いことがシステム課題点の一つでしたが、Originの仕事が減ったために負荷も軽減しました。

OriginのCPU使用率推移を表したグラフ
OriginのCPU使用率推移を表したグラフ

Speed Index

Speed Indexの値が低いセッション数が増加しました。HTMLがCDNから高速に返るようになったことで、表示速度が向上したと考えられます。

CDN切り替え前後のSpeed Indexの推移

最後に

最初にアメブロをフルCDN化すると聞いた時は、その大変さが容易に想像できたのもあり、終わりが見えませんでした。実装を進めていく途中でも問題がどんどん出てきて、残作業が1つ減ったと思ったら2つ増え…という状態を繰り返していて全く終わる気がしなかったです。しかし、新しいことに挑戦出来たことと、ともに作業したメンバーに恵まれたことで最後まで楽しく結果的にスケジュールどおりに出来たことはとてもよかったと思います。また、システム面でも毎日のように気にしていたOriginサーバーの負荷をそこまで気にせずよくなりましたし、突発的なアクセスにもビビらずに済むようになりました。管理サーバー台数も減ったことでデプロイが早くなったこともフルCDN化の恩恵だと感じています。15年積み重ねられたアメブロの、言語・環境・設計思想の異なるアプリケーションの集合体に手を加えるという部分が、難易度の高いポイントだったと感じていますが、総じてフルCDN化してよかったね!と思います。今後は更なるパフォーマンス改善とチーム内のCDN意識向上を進めていく所存です。

おまけ

先日Originが全死した際に、CDNキャッシュに載せるコンテンツを拡大していたおかげで生き残れた子たちがいました…!💣🔥(これまでだったらすべてエラーページが返却されていました…。)

Origin死亡時にCDNのキャッシュHIT率がほぼ100%になった際のグラフ
Origin死亡時のFastlyキャッシュHIT率のグラフ

ちなみにその時間帯はMISSという概念が無いためキャッシュHIT率はほぼ100%でしたw ありがとうCDN!
それではみなさまもよいCDNライフをお過ごしください👋

2012年新卒入社のエンジニア。2018年にバックエンドからフロントエンドへと転身。ずっとアメブロ界隈の人。時々ポ◯モンマスターになったり人理修復するマスターになったりする。