dala00
Posted on March 4, 2019
Crieitの記事詳細ページのレスポンスがdev.toのような速さになりました。実際にはdev.toよりも速いです。技術記事系サービスとしてはQiita等を含めて日本一となります。
この画像は各サービスの記事ページのレスポンスにかかった時間を比較したものになります。タイミングによってばらつきもあるため速いものをメモしてみましたが、国内のQiitaやQrunchは170ミリ秒ほど、爆速で話題になったdev.toも意外にも30ミリ秒ほどで、平均だともうちょっと遅い感じがします。そしてCrieitは15ミリ秒くらいなのでダントツで最速です。
確認方法としては、トップページから記事ページのリンクをクリックするか、記事ページでリロードするかが分かりやすいと思います。
(当記事の以降の内容は対応直後でまだ検証中のため、誤った情報などがあればご指摘いただけると助かります)
なぜ速いのか
理由としては、Laravel側で動的にレンダリングしたHTMLをCloudflareというCDNでキャッシュしてもらっているため、サイトを閲覧している人のブラウザではそれが表示されているだけだからです。つまり、プログラム無しの単なるhtmlファイルを読み込んでいるのと同じレベルということです。実際に阿部 寛のホームページも大体17ミリ秒という感じで同じくらいの速さです。
最近Gatsbyという静的サイトジェネレータが流行っています。これもコンテンツを事前にビルドし、サイト上では静的なファイルを表示するだけのため最速でコンテンツを届けることができるのですが、つまりはこれと全く同じレベルということです。しかもCloudflareのようなCDNは閲覧者の近くのサーバーから配信を行うため、ネットワークのレイテンシも非常に少なくあっという間にブラウザに表示されます。なので、速いのは何もすごくはなく、当たり前といえば当たり前のことです。
mizchiさんもブログで仰っていました。
フロントエンドの最適化の行き着く先の解の一つ、それは「CDN Edge で HTML をキャッシュして返却する」というものです。
Edge Worker PaaS の fly.io が面白い - mizchi's blog
完璧なのか
全然完璧ではありません。そもそも現在記事ページしか対応していないので、他のページは今までどおりです。
さらには、各ページには一度誰かがアクセスしなければならないので、投稿後や編集後に限っては今までどおりですし、普段誰もアクセスしない記事などはキャッシュされていないので最初に誰かがアクセスした時に重さを味わってもらわなければなりません。具体的にはChromeのDev Tools等で見ると下記のようにHITというレスポンスになっていればキャッシュが効いている状態です。(MISSになっている場合はリロードするとその後はHITになります)
あと僕自身の理解も完璧とは言えないので、何かしら勘違いしている可能性もあります。一度でもbotがアクセスされればキャッシュされるという認識ですが、適当な記事にアクセスしてみると意外にキャッシュされていないことが多いです。まだリリースして間もない時期ということもあり不明点や十分な確認ができていない部分も多いため、このあたりは引き続き要確認です。
ということで記事タイトルも文字数を気にしなければ「Crieitの記事詳細ページのHTML取得レスポンスはCDNのキャッシュが効いているページに限っては日本の技術系投稿サービスで最速になったが効いてなければすごく遅い」が正確です。
GatsbyやHugoとの違い
最近流行っているGatsbyやHugoを使えばよいのでは、と思うかもしれませんが、これらは事前にビルドが必要なため、Crieitのようなユーザー投稿型のサービスで利用することはできません。(何か方法があるのかもしれませんが、どちらにしろリアルタイムで反映していくのは難しそうな気がします)
そのためサーバーサイドでレンダリングしたものをそのままキャッシュしてもらうのが現実的な気がします。ビルドも不要です。
どんなメリットがあるのか
速い事自体が良い
とにかく速い事自体がメリットです。遅いとやはりイライラしますし、検索エンジン的にも速ければ速いほど良いと思います。そしてアクセスした時の気持ちよさも大きいです。
サーバーの負荷もない
CDN側で配信してくれるため、サーバーへのアクセスが来ません。そのためサーバーの負荷を抑えることができるようになります。実際にこのサーバーはGoogle Compute Engineの一番しょぼいサーバーで無料運用しているレベルのため、パフォーマンスも悪いしアメリカのリージョンのためレイテンシも大きいです。大体普通にアクセスすると1秒くらいかかってしまいます。(なので、dev.toは当然として、QiitaやQrunchも非常に高速という印象です)
まあそれはそれほどの問題にはならないのですが、記事や投稿が増えてそれに伴ってページが増えていくと、サイトを勝手に巡回するよくわからないbotのアクセスもどんどん増えていってしまいます。普通に閲覧してくれるユーザーさんのアクセスだけだと全く問題にはならないのですが、そういうbotのアクセスまで含めてしまうと結構限界が来るのも早くなってしまうのではないかと思います。
ですのでbotは全部もううちのサーバーに一切来なくなってほしいという思いからも対応を決意しました。
具体的にどうやったのか
そもそもCloudflareとは
CDNというのは各サービスの静的コンテンツをキャッシュし、それを世界中に張り巡らされた配信ネットワークを利用して、最も近い拠点から配信してくれる、というものすごい仕組みです。
例えばCloudflareではデフォルトではjs, css, 画像を配信してくれます。jsファイルやcssファイルは最近はビルドされていて数MBになっていることもあるため、サーバー上から配信するとかなり遅かったりします。うちでも初期はダウンロードに数秒かかっていました。CDNを通して配信すると一瞬で配信できるため、そもそもCloudflareがないと成り立たないレベルになっています。
デフォルトでは上記のファイルしか配信されないのですが、Page rulesを設定すると他のパターンでも配信できるようになります。それを用いて記事のURLのパターンを設定し、配信されるようにしました。
ヘッダーの調整が必要
しかし、ただPage rulesを設定してもキャッシュされません。というのもCloudflareはページのレスポンスヘッダの内容を見てキャッシュするかどうかを判断してくれます。キャッシュ期間を定めていればその期間でキャッシュを破棄して再度更新されるようにすることができますし、そもそも個人情報などを含んでいてキャッシュしてはならないページなどはキャッシュしないようになっています。
例えばLaravelとかであればデフォルトで全てキャッシュを無効化するレスポンスヘッダが含まれていますので、一切キャッシュされません。そのため、Middlewareやルーティングを設定してキャッシュを許可するためのレスポンスヘッダを返す必要があります。
具体的にはCache-Controlヘッダで下記を返すようにします。CDN用のMiddlewareグループを作成し、それ用のルーティングファイルにルーティングを記述するようにしています。ヘッダだけでなく、安全のためセッションも無効にしています。
public
好き勝手にキャッシュしていいよ、というやつです。
s-maxage
CDN向けのキャッシュ時間です。とにかく大きくして永久にキャッシュしてもらうようにしました。(実際にされているかどうかは検証できていませんが)
max-age
ブラウザ向けのキャッシュ時間です。s-maxageを指定していない場合はCDNのキャッシュ時間にも用いられます。これは長くしすぎるとCDN側のキャッシュが切れたことに気づくことができませんので、短めにしています。ブラウザキャッシュが切れてもCDNのキャッシュを取ってくるだけなので、問題はありません。
Set-Cookieヘッダを送信しない
Set-Cookieをレスポンスで返すと個人情報が含まれている可能性があると判断されるようでキャッシュが行われませんので送信しないようにします。
Laravelであれば\App\Http\Middleware\NoCookie::class
のミドルウェアを追加します。ただ、webのルーティングでこのミドルウェアだけ追加しても動作しないようなので、別途CDN用のミドルウェアグループを作ると良いと思います。
キャッシュのpurgeが必要
このままだと永久にキャッシュされてしまい、記事を更新しても反映されなくなってしまいます。そのため、キャッシュを破棄する必要があります。
Cloudflareの管理画面で可能ですが、APIも用意されているためそれを用います。
PHPでCloudflareのAPI経由でキャッシュを削除する
これで記事が更新された時にキャッシュをpurgeするようにしました。今のところは上手くいっている気がします。
ログイン状態をどう管理するか
静的にキャッシュしてしまっているため、サーバーサイドのテンプレート上でログインしている場合だけこれを表示、みたいなことはできなくなります。そのため、VuexのStoreにログインしている状態を保持しておき、ページが表示されたあとに必要なところだけ置き換えるようにしました。
置き換えると言っても単にVueコンポーネント化しておき、Storeの情報によって表示を切り替えているだけです。
リアルタイムのデータをどうするか
リアルタイムのデータというのは、例えばキャッシュはできないけど表示が必要なコメントやアクセス数などです。これらはページが表示されたあと、ajaxで取って来ています。つまりキャッシュされていればまだ良いのですが、キャッシュされていない場合はページの描画を含めると2回通信していることになります。
ただ、これは独自でアクセス数をカウントするため元々通信していたので変わらないのと、あとあくまでもやろうと思ったきっかけはbot対策のため、そのアクセスを軽減できれば問題ないかなと思っています。よくアクセスがあるページはだいたいキャッシュされた状態になると思いますし、そうでないページはさほどアクセスも来ないと思うので誤差かなと思います。とにかくbotを無視して、実際に見に来てくれる方の負荷だけを気にすれば良くなると考えると大きなメリットだと思っています。
問題点等の考察
まだ謎や問題点があるので考察していきます。単なる知識不足の勘違いの可能性もあるのでわかる方は是非アドバイスをお願いします。また、この考察はあくまでもCloudflareの話ですので、他のCDNサービスの場合は全然関係ない可能性もあります。
キャッシュされていない?
Google Analyticsでアクセスされたことが確認できたページや、サーバーのログでbotが来たらしきページにアクセスしてみるのですが、キャッシュが効いていない事が多いです。(前述のヘッダがHITではなくMISSになる)
そのため上手く期限が設定できていなかったり、そもそもCDN自体の仕様を勘違いしているのではないか、と心配になりました。ただ、もしかするとネットワークのエリア毎にキャッシュをしているようであれば、そのようになる可能性もあるのかな、と思いました。
前述のように近いエリアからコンテンツが配信されるので、必ずしも全てのCDNサーバーがキャッシュを持っているとは限りません。確かにそのあたり細かく連携するとレスポンスの速さが失われる要因になるような気もするので、エリアごとの管理で十分なのかなという気はします。purgeは多分全て連動で行ってくれるのかな、と思います。(どこかで瞬時に削除する、というような記述を見たような)
もしくはbotっぽいアクセスはキャッシュを使わず素通りさせるようにしているとか?
アップデート時にpurgeが必要
あとで気づいたのですが、よくよく考えると何かしら機能を更新してリリースしても、HTMLが変わらないのでリリースしたことが反映されません。ビルド済みのJavaScriptファイルはバージョニングされているのですが、それも新しいバージョンのものを見てくれないので、結局何かアップデートした際にはpurgeが必要で、また全てのページが一度重い状態になります。頻繁にアップデートすればするほどキャッシュが効かない状態が多くなります。(とはいえ対応前と変わらない状況ではありますが)
そのためとりあえず手動全purgeするか、しんどくなってきたら今手動でやっているデプロイを自動デプロイできるようにしてその中に組み込むか、あとはCDN芸をやめて最速の座から降りるか、というところになりそうです。
まとめ
とりあえずCloudflareを使った無料でのCDN芸をやってみたかった、というところもあったので試してみました。有料にはなりますが、他のCDNサービスであればもうちょっと楽な運用ができるのではないかと思います。もしログイン情報をあまり使っていないサービス等であればすぐにでも導入できたりするかもしれません。
Crieit上でもボードのデータ数が増えてきているため、こちらもCDN化を試しているところです。プライベート機能があるのでそのあたりでちょっとコツが必要だったりします。また、サーバー側のアクセスログもどういう風にアクセス数が推移しているかも解析してみたいなと思っています。また面白い情報が出たら記事にしてみます。(詳しく設定を確認していないのでもう消えているかもしれませんが…)
Posted on March 4, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.