Slide 1

Slide 1 text

ふつうのWebアプリケー ションとキャッシュ PHP勉強会 in 大阪 Yasuyuki Higa @yh65743

Slide 2

Slide 2 text

自己紹介 名前: 比嘉 康至(ひが やすゆき) X(旧Twitter): @yh65743 勤務先: 株式会社ことば研究所 居住地: 奈良(フルリモートで勤務しています) ❏ PHP ❏ React ❏ TypeScript 普段はこのあたりを使ってWebアプリケーション開発/ 保守やってます

Slide 3

Slide 3 text

今日話すこと ● キャッシュとは何か ● HTTPキャッシュの仕組み ● 動的コンテンツのキャッシュ 今日話さないこと ● Redis や APCu 等キャッシュ用ストレージの話 ● DBキャッシュの話 ● read-through, write-through といったキャッシュのアーキテクチャの話

Slide 4

Slide 4 text

何が「ふつうのWebアプリケーション」で何が「キャッシュ」 なのか キャッシュとは → 高コストな処理/計算の結果をどこかに保存しておいて再利用できるようにす る仕組みのこと httpにおいては 「高コストな処理/計算」: アプリケーションサーバーの計算結果 「結果」: http レスポンス 「どこか」:ブラウザ、CDN、リバースプロキシ等

Slide 5

Slide 5 text

ふつうのWebアプリケーションとは ● ブログ、ニュースサイト、ECサイト、求人サイト、etc… ● PHPでよく作られるCMS的なアプリケーション

Slide 6

Slide 6 text

● うちのサイトそんなにPVないし。。。 ● 静的コンテンツはWebサーバーが勝手に キャッシュしてくれるし動的コンテンツは キャッシュするの無理だから気にしなくても いいでしょ キャッシュについて考慮されてないことが多い ふつうのWebアプリケーションとは ● ブログ、ニュースサイト、ECサイト、求人サイト、etc… ● PHPでよく作られるCMS的なアプリケーション

Slide 7

Slide 7 text

● うちのサイトそんなにPVないし。。。 → マナーの悪いbotが大挙して押し寄せてき ても大丈夫? → メディアに取り上げられたりして急なスパ イクがあっても耐えられる? → リクエスト毎に重めのクエリが発行される ようになってませんか? ふつうのWebアプリケーションとは ● ブログ、ニュースサイト、ECサイト、求人サイト、etc… ● PHPでよく作られるCMS的なアプリケーション

Slide 8

Slide 8 text

ふつうのWebアプリケーションとは ● ブログ、ニュースサイト、ECサイト、求人サイト、etc… ● PHPでよく作られるCMS的なアプリケーション ● 静的コンテンツはWebサーバーが勝手に キャッシュしてくれるし動的コンテンツは キャッシュするの無理だから気にしなくて もいいでしょ → ここで質問

Slide 9

Slide 9 text

皆さん、Laravel使ってますか?

Slide 10

Slide 10 text

皆さん、Laravel使ってますか? 問題: Laravelの返すレスポンスはデフォルトでブラウザに キャッシュとして保存されるようになっている Yes or No?

Slide 11

Slide 11 text

皆さん、Laravel使ってますか? 問題: Laravelの返すレスポンスはデフォルトでブラウザに キャッシュとして保存されるようになっている Yes or No?

Slide 12

Slide 12 text

皆さん、Laravel使ってますか? 問題: Laravelの返すレスポンスはデフォルトでブラウザに キャッシュとして保存されるようになっている Yes or No? → 正確に言うと、Laravel のレスポンスはデフォルトだと「キャッシュされる けど、そのキャッシュが使われることはない」状態になっている

Slide 13

Slide 13 text

LaravelのCache-Controlヘッダを見てみる ● HTTPキャッシュの制御は Cache-Control ヘッダで行う ● Laravel のCache-Control ヘッダ Cache-Control: no-cache, private なーんだ no-cache って書いてあるじゃん

Slide 14

Slide 14 text

LaravelのCache-Controlヘッダを見てみる ● HTTPキャッシュの制御は Cache-Control ヘッダで行う ● Laravel のCache-Control ヘッダ Cache-Control: no-cache, private なーんだ no-cache って書いてあるじゃん → no-cache はキャッシュしないという意味では ない!

Slide 15

Slide 15 text

Cache-Controlヘッダ Cache-Control: no-cache は 「キャッシュを使うときは必ずオリジンに問い合 わせ、キャッシュが有効でない限り使用しない」という意味 キャッシュさせたくないときは no-store を指定する。 「オリジンに問い合わせ」→ 配信元に対して「条件付きリクエスト」という特殊 なリクエストを送る

Slide 16

Slide 16 text

条件付きリクエスト 条件付きリクエストとは: サーバーにキャッシュが新鮮か尋ねるリクエスト。も し新鮮ならステータスコード 304 Not Modified とヘッダだけが返ってきてブラ ウザはキャッシュをボディとして再利用する。 キャッシュが新鮮でなければ普通にボディが返ってくる。

Slide 17

Slide 17 text

条件付きリクエスト 条件付きリクエストとは: サーバーにキャッシュが新鮮か尋ねるリクエスト。も し新鮮ならステータスコード 304 Not Modified とヘッダだけが返ってきてブラ ウザはキャッシュをボディとして再利用する。 キャッシュが新鮮でなければ普通にボディが返ってくる。 ではキャッシュが新鮮かどうかはどうやって判定しているか?

Slide 18

Slide 18 text

条件付きリクエストの判定 リソースの最終更新日。 ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" Last-Modified: Thu, 21 Mar 2024 17:38:20 GMT キャッシュ制御のためにレスポンスに含まれているヘッダは Cache-Control 以外にもある。キャッシュが新鮮かどうか判定するために使われるのが ETag と Last-Modified ETag レスポンスとして返すコンテンツのバージョン番号のようなもの。何をETagとして返すか はサーバ側の実装次第だけどコンテンツの更新日時やコンテンツそのもののハッシュ等が 使われる。 Last-Modified

Slide 19

Slide 19 text

条件付きリクエストの判定 (ETagの場合) サーバーは初回リクエストしてきたブラウザに対して Cache-Control: no-cache ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" というヘッダのレスポンスを返す ブラウザはレスポンスをキャッシュする。ETagの値も覚えておく。

Slide 20

Slide 20 text

条件付きリクエストの判定 (ETagの場合) ブラウザは同じURLに対して再度リクエストする。そのとき If-None-Match というヘッダ を付与し、その値として覚えていた ETag の値を使用する If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4" サーバーは If-None-Match 付きのリクエストを受け取ると、ETag の値を計算したのと同 じ方法でリソースから値を得る。例えば、ファイルの更新日のハッシュを計算する。その結 果を If-None-Match の値と比較し、一致していたらステータスコード 304 Not Modified とヘッダ部分のみのレスポンスを返す ブラウザは304のレスポンスが得られたらキャッシュをレスポンスのボディとしてユーザー に表示する

Slide 21

Slide 21 text

条件付きリクエストの判定 (Last-Modifiedの場合) サーバーは初回リクエストしてきたブラウザに対して Cache-Control: no-cache Last-Modified: Tue, 26 Mar 2024 00:40:43 GMT というヘッダのレスポンスを返す ブラウザはレスポンスをキャッシュする。Last-Modified の値も覚えておく。

Slide 22

Slide 22 text

条件付きリクエストの判定 (Last-Modifiedの場合) ブラウザは同じURLに対して再度リクエストする。そのとき If-Modified-Since という ヘッダを付与し、その値として覚えていた Last-Modified の値を使用する If-Modified-Since: Tue, 26 Mar 2024 00:40:43 GMT サーバーは If-Modified-Since 付きのリクエストを受け取ると、リソースの最終更新日と If-Modified-Sinceの値を比較し、リソースが更新されていなければステータスコード 304 Not Modifiedとヘッダ部分のみのレスポンスを返す ブラウザは304のレスポンスが得られたらキャッシュをレスポンスのボディとしてユーザー に表示する

Slide 23

Slide 23 text

Laravelのレスポンスキャッシュが何故使われないか ● レスポンスヘッダの ETag/Last-Modified がキャッシュを使っていいかどう かの判定に使われることがわかりました。 ● ではここで、すっぴんの Laravel 君が返してくれたレスポンスヘッダを眺め てみましょう。

Slide 24

Slide 24 text

Laravelのレスポンスキャッシュが何故使われないか ● レスポンスヘッダの ETag/Last-Modified がキャッシュを使っていいかどう かの判定に使われることがわかりました。 ● ではここで、すっぴんの Laravel 君が返してくれたレスポンスヘッダを眺め てみましょう。 あれ?

Slide 25

Slide 25 text

Laravelのレスポンスキャッシュが何故使われないか ● レスポンスヘッダの ETag/Last-Modified がキャッシュを使っていいかどう かの判定に使われることがわかりました。 ● ではここで、すっぴんの Laravel 君が返してくれたレスポンスヘッダを眺め てみましょう。 あれ? ETag も Last-Modified もない!

Slide 26

Slide 26 text

Laravelのレスポンスキャッシュが何故使われないか ● レスポンスヘッダに ETag も Last-Modified もないので、次回のリクエスト でブラウザは条件付きリクエストを送ることができない ● よって通常のリクエストが送られ、サーバは常にボディ付きのレスポンスを 返すことになる。

Slide 27

Slide 27 text

Laravelのレスポンスキャッシュが何故使われないか ● レスポンスヘッダに ETag も Last-Modified もないので、次回のリクエスト でブラウザは条件付きリクエストを送ることができない ● よって通常のリクエストが送られ、サーバは常にボディ付きのレスポンスを 返すことになる。 ・・・

Slide 28

Slide 28 text

Laravelのレスポンスキャッシュが何故使われないか ● レスポンスヘッダに ETag も Last-Modified もないので、次回のリクエスト でブラウザは条件付きリクエストを送ることができない ● よって通常のリクエストが送られ、サーバは常にボディ付きのレスポンスを 返すことになる。 ・・・ 安全側に倒すなら no-store にしとけばいいところ、あえて no-cache にしてい るところにメッセージを感じる(と、私は勝手に思っている)

Slide 29

Slide 29 text

キャッシュのロジックを自分で実装してみよう 雑実装につき注意 middleware を使えば この辺のことは勝手に やってくれますが、わ かりやすいように自力 でやっています

Slide 30

Slide 30 text

キャッシュのロジックを自分で実装してみよう ● レスポンスボディのハッシュを比較するだけのシンプルな実装 ● これだとレスポンスボディの計算コストはそのまま。途中重たいクエリの実 行が含まれていたら負荷軽減にはならない。 ● じゃあ動的コンテンツのキャッシュは不可能?

Slide 31

Slide 31 text

キャッシュのロジックを自分で実装してみよう ● レスポンスボディのハッシュを比較するだけのシンプルな実装 ● これだとレスポンスボディの計算コストはそのまま。途中重たいクエリの実 行が含まれていたら負荷軽減にはならない。 ● じゃあ動的コンテンツのキャッシュは不可能? → 実際に計算しなくてもコンテンツが更新される根拠を知ることができるので は?

Slide 32

Slide 32 text

キャッシュのロジックを自分で実装してみよう ● 例えばECサイトの「商品一覧」ページ ● productsテーブルが更新されていないならコンテンツとして同一である、み たいなことが言えるのでは?

Slide 33

Slide 33 text

Laravel門外漢の雑実装その2 擬似コード的な内容ですがやり たいことは伝わるのではないで しょうか。 実際にはこの辺のETag絡みの処 理は middleware に切り分けた ほうがいいと思います。 ここでは省略していますがこの ままだとviewのコードをいじっ てもキャッシュが更新されない ので、アプリケーションのバー ジョン情報などを設けておいて ETagに含めたほうがいいか も。

Slide 34

Slide 34 text

さらなる負荷軽減のために ● これなら2回目以降のリクエストがproductsの更新前なら、products に対す る重たいクエリの代わりにレコード一件の取得で済む。 ● ただし、これだと条件付きリクエストのたびにDBアクセスが発生することに なる。 ● 新商品入荷日にF5連打するような人がたくさんいたらどうしよう。 ● もう少し改善できないものか。。。?

Slide 35

Slide 35 text

さらなる負荷軽減のために ● これなら2回目以降のリクエストがproductsの更新前なら、products に対す る重たいクエリの代わりにレコード一件の取得で済む。 ● ただし、これだと条件付きリクエストのたびにDBアクセスが発生することに なる。 ● 新商品入荷日にF5連打するような人がいたらどうしよう。 ● もう少し改善できないものか。。。? → 商品の更新を必ずしもリアルタイムで反映できなくてもいいのでは?

Slide 36

Slide 36 text

max-age、Expires ● Cache-Control: max-age=300 のように指定するとキャッシュのTTLを指 定できる。この例だとレスポンスが生成された時点から300秒間はキャッ シュを使用し、条件付きリクエストも送られなくなる。300秒経過後、再度 リクエストした場合には条件付きリクエストが送られる。 ● Expires は相対的な秒数ではなくて、TTLが切れる時刻を絶対時間で指定で きる。Expires: Wed, 21 Oct 2015 07:28:00 GMT のように指定。 max-ageと同時に指定すると max-age が優先される。max-ageに対応して ない古いブラウザ向け。 ● キャッシュのTTLを指定してやるとリアルタイムに更新が反映されなくなる が、条件付きリクエストの頻度を抑えることができる。

Slide 37

Slide 37 text

Proxy, CDN ● ここまででブラウザキャッシュの話をしてきましたが、これはいわゆる private キャッシュで、同じ人が2回目以降にアクセスしてきた時の話。 ● つまり、初見のユーザーが大量に押し寄せてきた、みたいなケースには対応 できない。 ● 「商品一覧」は見る人によって表示が変わるわけではないので一度キャッ シュされた内容が広く共有されてほしい → sharedキャッシュが必要。 ● そこでブラウザだけでなく Proxy や CDN にもキャッシュしてもらう。

Slide 38

Slide 38 text

Proxy, CDN Proxy: いわゆるリバースプロキシ。Varnish, Squid, Nginx等。Webサーバーの 前でリクエストを処理してくれる。 CDN: Contents Delivery Network。ウェブコンテンツをインターネット経由で 配信するために最適化されたネットワークのこと。様々な機能があるが、Proxy 同様Webサーバーの前でキャッシュによるレスポンスを行ってくれる。

Slide 39

Slide 39 text

Proxy/CDN Origin 1. ユーザーAが商品一覧ページ をリクエスト 2. Proxy/CDN はキャッシュがな いのでコンテンツをオリジンに リクエスト 3. オリジンは Proxy/CDN にコ ンテンツをレスポンス 4. Proxy/CDN はオリジンからの レスポンスをキャッシュしたう えでユーザーにレスポンスす る。 5. ユーザーBが商品一覧ページ をリクエスト 6. Proxy/CDNはユーザーAのリ クエスト時にキャッシュしたコ ンテンツをBにレスポンス

Slide 40

Slide 40 text

Cache-Control: private, public ● これでキャッシュを共有して初見のユーザーにも対応できるようになりまし た。 ● でも共有されてほしくない情報もある。例えば個人情報とか。その手のセン シティブなコンテンツが shared キャッシュに載らないかちょっと不 安。。。 ● そういえば Laravel の返してきた Cache-Control ヘッダに no-cache 以外 にも何かついてましたね。

Slide 41

Slide 41 text

Cache-Control: private, public Cache-Control: private レスポンスがプライベートキャッシュ(ブラウザーのローカルキャッシュなど) にのみ保存できることを示す。これが付いてると Proxy や CDN にはキャッシュ されなくなる。 Cache-Control: public これがないと Proxy や CDN がキャッシュしてくれない・・・わけではない。リ クエストに Authorization ヘッダが付いていると普通そのレスポンスは shared キャッシュとして保存してはいけないのだが、それでもキャッシュさせたい、と いう場合に付ける。

Slide 42

Slide 42 text

Cache-Control: private, public ● Cache-Control: private/public は単にキャッシュの保存先を指定するだけで はなく、「通常キャッシュされないレスポンスでもキャッシュする」という 意味を持っていることに注意。 ● 例えば 403 のようなステータスコードでもキャッシュされてしまう可能性が ある(ブラウザや Proxy/CDN の実装によります) ● CDN が public を付けないとキャッシュしない仕様だとか、リクエストに Authorization ヘッダが付いていてもキャッシュしたいとかでなければ public は付けないほうがいい

Slide 43

Slide 43 text

まとめ ● キャッシュは思ってるより身近なもの ● 動的コンテンツでも工夫次第ではキャッシュできるよ ● キャッシュと仲良くしてエコなアプリケーション開発をしよう 参考文献 田中 祥平 『Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する』技術評論社 (2021/2/10) MDN Web Docks “Cache-Control - HTTP | MDN” https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control IETF “RFC 9110: HTTP Semantics” https://www.rfc-editor.org/rfc/rfc9110.html IETF “RFC 9111: HTTP Caching” https://www.rfc-editor.org/rfc/rfc9111.html