キャッシュは難しい
この記事ではWebアプリケーションの出力のキャッシュが困難な理由とそれを解決する方法について考えます。
静的コンテンツのキャッシュの実際
静的コンテンツは完全にHTTPキャッシュが実現されています。
CSSファイルやJSファイルをCDNに載せるのは簡単です。一度ディプロイしてしまえば基本的には変更がないものとしてTTL(生存期間)を設定できます。
HTMLではサーバーはそのファイルがいつ最後に更新したかを知ることができますし"Last-modified"ヘッダーに付加することができます。変更があれば以前のETagを無効にして新しい"ETag"を有効にします。いつコンテンツの変更があったのかを正確に、低いコストで知ることができます。そのETagの維持には関心を払う必要さえありません。全ては自動でHTMLファイルのキャッシュは完璧です。一度GETされたHTMLはCDNとクライアントに保存され、コンテンツに変更があっても条件付きリクエストで即座に反映されます。
Webアプリケーションでは管理が必要
Webアプリケーションのイベントドリブンコンテンツの最終更新日付はいつでしょうか?これは誰がどう管理すべきでしょうか?
データベースの「記事テーブル」に作成日時や更新日時のカラムが加えられることがありますが、これをそのまま利用すればいいでしょうか?ブログの記事ページは、記事だけでなくカテゴリーや人気記事一覧などの他のコンテンツも含まれまれ、これを全体のページの最終更新日付とすることはできません。
含まれるコンテンツ全ての更新日付のうちの最も新しい日付が分かれば、それをページの最終更新日付にしてその日付が更新されるタイミングでコンテンツ全体のハッシュを計算してETagを生成することはできます。
コンテンツ間の依存の管理
つまりイベントドリブンコンテンツのページ全体としてのキャッシュをするときは、サーバーサイドでのコンテンツの依存管理が必要です。ページAはコンテンツBとコンテンツCを含んでいて、コンテンツBはコンテンツDを含んでいる。このような場合、コンテンツB、C、Dいずれかが更新された時にコンテンツAの更新日付とETagを更新するような管理が必要になってきます。
しかもこの依存に基づいたキャッシュの無効化はRedisなどのサーバーサイドや、CDNの双方に必要です。こんな面倒な依存グラフを手動で管理したくありません。
キャッシュタグ
しかしこの依存管理の強い味方となるソリューションがサーバーサイドのキャッシュ、CDNのキャッシュ双方にあります。
- Symfony Cacheのタグ機能 https://symfony.com/doc/current/cache.html#using-cache-tags
- Fastlyのサロゲートキー https://docs.fastly.com/ja/guides/working-with-surrogate-keys
それぞれ呼び方は違いますが、考え方は同じです。つまりコンテンツを保存するときに、その依存をタグとして保存しておいて後で、そのタグをキーにキャッシュの無効化を行うことができるのです。
先ほどの例だとページAを保存する時にB, C, Dのタグをつけて保存しておけば、B,C,Dのいずれかのコンテンツが更新されるタイミングでページAのキャッシュとそのETagが無効化されます。
- Symfony Cacheはスマートで、Redisなどタグの機能があるものはそれを使い、ファイルキャッシュなどタグの機能がないものは独自に実装しています。
インバウンドリンク
RESTにおいて、外のリソースをリンクすることをアウトバウンドリンクと呼び、リソース自身の中に他のリソースをリンクして取り込むことをインバウンドリンクと呼びます。HTMLではAタグは外のリソースをリンクするアウトバウンドリンクで、<img>
タグや <script>
タグはそのHTML自身にコンテンツを埋め込むインバウンドリンクです。
BEAR.Sundayでは#[Embed]
リンクがインバウンドリンクです。これはそのままコンテンツの依存関係に利用できます。埋め込むリソースにタグを命名して、そのタグをつけてページのコンテンツのキャッシュを保存すれば良いのです。
ETagを保存する時にもこの依存のタグを使用して、依存が変更になったときにETagを無効化します。
セマンティックメソッド
一般的にMVCのコントローラーは以下のようなものでしょう。
class HelloController extends Controller
{
public function index()
{
// .... $bodyを自由に計算
return new Response($body)
}
}
モデルの取得とその合成は自由で、制約はありません。どのモデルもどのようにもアクセスできます。しかし、人はコードを読んで理解できますが、セマンティックがないので機械(フレームワーク)から見ればどのモデルからどのように合成されているかは分かりません。自由に名付けたread
メソッドやfetch
メソッドはそれが読み込み用のものと人は理解できるでしょうが、機械は理解できません。全てを合成したレスポンス(文字列)を返却するだけです。
BEAR.Sundayは全てのモデル(リソース)にURIを含みます。#[Embed]
アトリビュートはリソースを埋め込みながら、特定のリソースがどのリソースに依存をしているのかを宣言しています。
BEAR.SundayにはonGet()
は非破壊的リソース操作、それ以外は破壊的リソース操作というセマンティックがあります。フレームワークがキャッシュの依存性を管理できる素養が見えてきました。
AOP
理想的にはリソースがドメインを扱うときにログやキャッシュに関心を持ちたくありません。複雑なコンテンツの依存グラフなど尚更です。
AOPの出番です。インターセプターはメソッドが実行されるタイミングでその操作を見て、サーバーサイドのキャッシュとETagの作成や依存グラフによる破壊、更新を行います。CDNのキャッシュの破壊に関してもAPI経由で行います。
ソリューション
Webアプリケーションのコンテンツの依存の構造があり、イベントドリンブンコンテンツページ全体をキャッシュするためには、その依存構造が反映されていないといけません。
既存のMVCフレームワークのアークテクチャは通常コンテンツの構造に関心がありません。
これはWordpressとWikiPediaのコンテンツ入力と対比させることができます。Wordpressは最終出力だけを考えて本文に自由にコンテンツを入力します。コンテンツは構造化されていません。対してWikipediaは特定の意味を表す記法など制約があり、コンテンツが構造化されていて再利用性や検索性に優れます。更に詳しくはDCCをご覧下さい。
REST制約のインバウンドリンクによるリソース合成とセマンティックを持つメソッドにより、難題であった依存関係のあるイベントドリブンキャッシュが可能になりました。