キャッシュ設計(2) - 分散

REST制約に従った分散キャッシュシステムは、計算資源だけでなくネットワーク資源も節約しします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940/730d4235-2f48-a520-3db1-bad9e0c04039.png)
https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
（REST論文の図。キャッシュはサーバーサイドではなくクライアントサイドに保存されています）

この記事ではキャッシュをストレージの場所で考察します。

## キャッシュが行われる場所

キャッシュは次の３つのいずれかに存在します。

1. サーバーサイド
2. CDN (共有キャッシュ)
3. クライアント

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3940/65bc801a-476d-7527-1c40-6512aada08df.png)

理想的なキャッシュシステムを目指し、HTML、APIに関わらずこの分散型のキャッシュシステムを設計します。

## キャッシュのTTLを知るのは誰か？

コンテンツがイベントドリブンではなく、正確なコンテンツの生存期間（TTL = Time to live）で制御する場合、サーバーサイドがTTLを決定します。例えば製品の発売日まで有効なキャンペーンサイトや、月曜の相場開始までの時間が正確に計算できる株価のデータなどです。

HTTPキャッシュはレスポンスヘッダーに`Cache-Control`ヘッダーを付与することで、CDNやクライアントに対してキャッシュのTTLを伝えます。

## キャッシュの無効化

キャッシュが無効化される理由は２つあります。TTLの指定で時間が切れたかよるものか、イベントによるものかです。

### TTLによるCDNキャッシュの無効化

TTLによる無効化は容易で、CDNやクライアント双方に使う事ができます。コンテンツの有効期間を秒数で指定します。

### イベントによるCDNキャッシュの無効化

一方、イベントによる無効化は注意が必要です。サーバーサイドのWebアプリケーションのイベントでサーバーサイドのキャッシュを無効化することは容易ですが、CDNに対して無効化するときは、インスタントパージに対応したCDNを利用する必要があります。FastlyやAkamaiなどです。そのときはイベントドリブンコンテンツはTTLでキャッシュが消去されないようにするために、キャッシュ時間を最大(１年間）にします。

インスタントパージが利用できないCDNでは、イベントドリブンコンテンツをCDNでキャッシュすることができません。キャッシュ時間をゼロにして、`must-revalidate`で都度確認するようにします。

ETag管理が最適されていると、最小限のネットワークおよびCPU資源の消費ですみますが、インスタントパージ可能なCDNのキャッシュの効率性には及びません。

## クライアントキャッシュ

```
Cache-Control: max-age=3600
```

レスポンスディレクティブのmax-age=3600 は、レスポンスが生成されてから3600 秒後まで、レスポンスが新鮮なままであることを示します。このヘッダーはCDNでもキャッシュを作成しますが、クライアントでもキャッシュを作成します。最も強力なキャッシュで、ネットワークコストを節約します。

TTLが正確に予測できる場合にのみ使用します。この期間、イベントでコンテンツを変更する事はできません。

イベントドリブンコンテンツの場合は、クラアイントはコンテンツに変更があったかを[条件付きリクエスト](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/If-Match)で確認します。変更がなければHTTPステータスコード`304`が返されるのでクライアントはクライアントサイドで保存したキャッシュを再利用します。

また、パーソナライズされたページなど、クライアント間でキャッシュが共有できないときは`private`を使います。

```
Cache-Control: private, max-age=3600
```

## CDNキャッシュ

クライアントにキャッシュを保存しないときは以下のどちらかのヘッダーで指定します。


```
Cache-Control: s-maxage=3600
```
```
Surrogate-Control: max-age=3600
```

## サーバーサイドのキャッシュ

サーバーサイドは3つのレイヤーで構成します。

* ETagリポジトリレイヤー
* サーバーサイドキャッシュレイヤー
* オリジンレイヤー

1. CDNから到達したHTTPリクエストが、条件付きリクエストの場合には、コンテンツに変更がないかをETag リポジトリレイヤーが判定してなければ304を返します。ストレージに容量はあまり必要ないのでMemcachedのようなストレージが向いています。
1. 対応するURLのサーバーサイドのキャッシュがあればそれを返却します。Redisなどを使います。
1. キャッシュがなければ、オリジンレイヤーでレスポンスを返しますが、その時のキャッシュ可能なコンテンツはETagをETagレポジトリに格納してレスポンスデータをサーバーサイドキャッシュに格納します。

## キャッシュが生成される順番

サーバーからクライアントまでキャッシュコンテンツが伝わる様子は以下のとおりです。

1. オリジンサーバーがコンテンツを生成します。
2. コンテンツのETagをETagレポジトリに格納します。
3. コンテンツのキャッシュをサーバーサイドのキャッシュに保存します。
4. オリジンはHTTPヘッダーで`ETag`を返します。 ETag: "<etag_value>"
5. CDNはそのETagとその内容をCDNキャッシュに保存します。
6. クライアントはコンテンツをローカルにキャッシュします。

クライアントAのリクエストでこのキャッシュが生成された時に、別のクライアントBは4で保存されたCDNキャッシュの恩恵を受けます。

Webブラウザには`Cache-Control`ヘッダーを理解しその通りに振る舞いますが、APIクライアントの場合には`Cache-Control`ヘッダーを理解するために`RFC7234`対応のクライアントを利用する必要があります。

RFC7234対応APIクライアント

* [JavaScript](https://www.npmjs.com/package/cacheable-request)
* [PHP](https://github.com/Kevinrob/guzzle-cache-middleware)

## キャッシュの強さ順

より強力なキャッシュの順番にリストします。

* ネットワークコストの発生しないクライアントキャッシュ(TTL)
* 都度確認をするクライアントキャッシュ(ETag)
* 304を返すCDN(ETag)
* キャッシュコンテンツを返すCDN
* 304を返すサーバーサイド(Etag)
* キャッシュコンテンツを返すサーバーサイド

## キャッシュシステム設計

サーバーサイドのアプリケーションは、コンテンツの変更の際にキャッシュ管理処理をトリガーします。

* サーバーサイドのキャッシュを保存　(Redisなど）
* サーバーサイドでETagを保存 (memcacheなど）
* CDNに対してのAPIでパージ操作

BEAR.Sundayは一般的なMVCフレームワークよりこれらの操作に適した特徴があります。

* リソースはURIがあり、その操作はステートレス
* リソースのメソッドにセマンティックがありどれが読み込みで(onGet）どれが更新か区別がある
* AOP可能

ユーザーがアプリケーションで操作をすることなく、大部分のキャッシュ操作がフレームワークにより自動化されれます。