CDNでのキャッシュ時間を延ばすことによる効果

## 初めに

CDNでサイト内の動的コンテンツを結構長めにキャッシュすることになりました。  
キャッシュ効率を上げるのに「キャッシュする時間が長けりゃそれでいいというわけではない」というのは感覚的には分かっていたのですが、  
説明せよと言われたら何とも言えない感じだったので、自分なりの回答を得るべく何とか計算してみようとしました。

### 前提知識

- CDNは何のための物なのか
- CDNに関連する用語(キャッシュ、オリジン等)

### この記事で確かめること

**キャッシュ時間延ばせば、オリジンリクエストはどれだけ抑えられるのか？**  
なるべく簡単な例を簡単な算数で考えてみましょう！

[結論を急ぐ！](#確かめられたこと)

## 前準備

今回考えるのはキャッシュ効率です。それを出すにはCDNでキャッシュが効いてくれなかった時に発生するオリジンリクエストの数を計算する必要があります。

キャッシュが効かなくなる場合はどういう時かを考え、それに合った式を立てていくことになります。

### キャッシュが切れた直後のオリジンリクエスト

- キャッシュがある時はキャッシュが返り続ける
- キャッシュが切れた直後は瞬間的にオリジンリクエストが発生する

という動きになると考えます。  
特にこのキャッシュが切れた直後のオリジンリクエスト数は、

- その時間帯の1秒あたりのリクエスト数($D_{\rm Req}$)
- CDNからのオリジンリクエストの処理(CDNへ返ってくるまでの)時間($T_{\rm Proc}$秒)  
  ※以下、長いので「オリジンリクエストの処理時間」とだけ言うこととする  

で決まり、$\lfloor D_{\rm Req} \times T_{\rm Proc}\rfloor + 1$で出せます。  
この「$+1$」はキャッシュが切れた直後の最初のリクエスト分であり、そのリクエストが処理されて返るまでオリジンリクエストが$\lfloor D_{\rm Req} \times T_{\rm Proc}\rfloor$回発生するということです。

例えば、1分あたり100万リクエスト(1秒あたり16666)で、1リクエストあたりの処理時間が0.5秒だとしたら、

$$
\lfloor D_{\rm Req} \times T_{\rm Proc}\rfloor + 1 = 16666 \times 0.5 + 1 = 8,334回
$$

例えば、1分あたり10リクエスト程度で、1リクエストあたりの処理時間が0.5秒だとしたら、そのリクエスト処理中に次のリクエストが来ることは無いので1回になります。[^1]

$$
\lfloor D_{\rm Req} \times T_{\rm Proc}\rfloor + 1 = 0 + 1 = 1回
$$

### キャッシュ時間のパターン

1. キャッシュ時間短め
    - キャッシュ時間が1分～1時間
    - 範囲は1時間を考える
    - その範囲におけるリクエストの分布は一様と見做せる
2. キャッシュ時間中くらい
    - キャッシュ時間が1時間～1日
    - 範囲は1日を考える
    - その範囲のリクエストの分布は一定の動きがある (例えば、昼は凹んで夜は膨らむ等)
3. キャッシュ時間長め
    - キャッシュ時間が1日～1年(動的コンテンツをここまでキャッシュするのはリスクが高いですが・・)
    - 範囲は1カ月とか1年を考える
    - その範囲のリクエストの分布は、季節によって一定の動きがあるかもしれないし、ランダムかもしれない

長めのキャッシュ時間を考えようとするとそれだけ計算しづらい分布になります。  
また、(ネタバレにはなってしまいますが)キャッシュ時間短めでも、もし十分効果があるならばそれ以上長くしても目立った差はないので、ここではキャッシュ時間短めのパターンだけ考えます。

### 仮定

- 1つのURL(というかキャッシュキー)に対するリクエストだけ考える
- $s$分( $0<s≤60$ )に$N$( $≥1$ )回リクエストが来る場合を考える
- 1時間あたりのリクエストを考える  
  すなわち、全リクエスト数は$\lfloor 60 \times \frac{N}{s} \rfloor$回であり、オリジンリクエスト数はこれを超えることは無い
- リクエストの分布は1時間の間に一定の間隔でバラついているとする
- オリジンリクエストの処理時間は常に一定($T_{\rm Proc}$秒)とする
- 例えばキャッシュ時間を25分とかにした場合、1時間のうち2回しかキャッシュが切れないパターンと、3回切れるパターンがある。計算する場合は1少ない方に寄せることとする
  ※後述の式の、$\lfloor \frac{60}{t} \rfloor$ の意味がこれです

## 計算してみる

### オリジンリクエスト数の計算式

キャッシュ時間$t$分( $0<t≤60$ )とした時の1時間あたりのオリジンリクエスト数$R_{\rm Orig}$は

$\frac{N}{s} \times t \geq 1$の時[^2]

```math
\begin{align*}
R_{\rm Orig} &= (
  \lfloor
    D_{\rm Req} \times T_{\rm Proc}
  \rfloor + 1
) \times
\lfloor
  \frac{60}{t}
\rfloor \\
&=
\left(
  \lfloor
    \frac{N}{s\times60} \times T_{\rm Proc}
  \rfloor + 1
\right) \times
\lfloor
  \frac{60}{t}
\rfloor
\end{align*}
```

と書けます。

この式から分かること

- キャッシュ時間が短いほどオリジンリクエスト数は増える
※1時間のうち、キャッシュが切れる回数は$\lfloor \frac{60}{t} \rfloor$であることから分かる
- キャッシュが切れた直後のオリジンリクエスト数$\lfloor \frac{N}{s\times60} \times T_{\rm Proc} \rfloor$が0に近い程キャッシュが効く  
  ※0になると$R_{\rm Orig} = \lfloor \frac{60}{t} \rfloor$回で、キャッシュが切れる度に1回だけオリジンリクエストが発生するということになる
- キャッシュする時間が処理時間以下($t \leq T_{\rm Proc}$)、あるいはそれに近いとキャッシュする意味が無い  
  ※普通そんな短くはしないけれど

### 色々な数値を入れて様子を見てみる

※ここでのリクエスト数はあくまで、**1キャッシュキーに対するリクエスト数**ですので、実際のリクエスト数に当てはめる場合はそれを良く踏まえてください。

---

1分に47500回リクエスト、$T_{\rm Proc}$=0.5秒とすると

| キャッシュ時間 | 1時間あたりのオリジンリクエスト | キャッシュヒット率 |
| --- | --- | --- |
| なし | 2,850,000回 | 0.000% |
| 1分 | 23,760回 | 99.166% |
| 2分 | 11,880回 | 99.583% |
| 3分 | 7,920回 | 99.722% |
| 5分 | 4,752回 | 99.833% |
| 10分 | 2,376回 | 99.917% |
| 20分 | 1,188回 | 99.958% |
| 60分 | 396回 | 99.986% |

リクエストの処理時間が遅くても、リクエストが集中してると1分キャッシュでも中々良いキャッシュヒット率ですね。

---

1分に5000回リクエスト、$T_{\rm Proc}$=0.6秒とすると

| キャッシュ時間 | 1時間あたりのオリジンリクエスト | キャッシュヒット率 |
| --- | --- | --- |
| なし | 300,000回 | 0.000% |
| 1分 | 3,060回 | 98.980% |
| 2分 | 1.530回 | 99.490% |
| 3分 | 1.020回 | 99.660% |
| 5分 | 612回 | 99.796% |
| 10分 | 306回 | 99.898% |
| 20分 | 153回 | 99.949% |
| 60分 | 51回 | 99.983% |

やはりリクエスト数が落ちると、短いキャッシュ時間ではほんのりキャッシュヒット率に差が出ますね。

---

1分に682回リクエスト、$T_{\rm Proc}$=0.1秒とすると

| キャッシュ時間 | 1時間あたりのオリジンリクエスト | キャッシュヒット率 |
| --- | --- | --- |
| なし | 40,920回 | 0.000% |
| 1分 | 120回 | 99.707% |
| 2分 | 60回 | 99.853% |
| 3分 | 40回 | 99.902% |
| 5分 | 24回 | 99.941% |
| 10分 | 12回 | 99.971% |
| 20分 | 6回 | 99.985% |
| 60分 | 2回 | 99.995% |

リクエスト数が少ないですが、リクエストの処理時間が早ければキャッシュヒット率はとても良いですね。  
リクエストの処理時間が早くすることは、キャッシュキーを無駄なく定義してリクエスト数を集中させるよりも効果があると言えます。

---

1分に17回リクエスト、$T_{\rm Proc}$=0.1秒とすると

| キャッシュ時間 | 1時間あたりのオリジンリクエスト | キャッシュヒット率 |
| --- | --- | --- |
| なし | 1,020回 | 0.000% |
| 1分 | 60回 | 94.118% |
| 2分 | 30回 | 97.059% |
| 3分 | 20回 | 98.039% |
| 5分 | 12回 | 98.824% |
| 10分 | 6回 | 99.412% |
| 20分 | 3回 | 99.706% |
| 60分 | 1回 | 99.902% |

流石にここまでリクエスト数が少ないとキャッシュヒット率も変わってきますね。  
とは言え、1分キャッシュでも60回までオリジンリクエスト数を抑えられるのであれば、そもそも相対的な数値を見る必要は無いと言えるかもしれません。

## 確かめられたこと

### キャッシュキー毎のリクエスト数が十分多ければ例え1分であろうと効果は十分ある

リクエスト数が十分多ければ計算結果的には1分でもキャッシュヒット率が99％を超える。  
逆に言うと**キャッシュ時間を増やすだけでは、割合としてはそこまで変わらない。**

ただし、「ちゃんとキャッシュされていれば」です。意図せずBYPASSとかになってたらダメですよ。[^3]

### キャッシュキー毎のリクエスト数が十分無いとキャッシュ時間が短くなるにつれ効果は薄れる

スッカスカなのにキャッシュ時間短めにするとキャッシュによる恩恵は小さくなっていきます。  
これはつまり、**キャッシュキーを過不足なく定義することにより、各キャッシュキー毎のリクエスト数を増やすことでキャッシュ効率を上げられる**ということでもあります。

### サーバーの処理時間が短いとキャッシュヒット率が良くなる

「そもそも、**オリジンリクエストがCDNに返ってくるまでの間にリクエストがドカドカ来るのが悪いので、さっさと返してCDNにキャッシュを作れば良い**のだ。」  
と考えるのです。  
キャッシュ時間が短い時はよりその差が顕著に出てきます。

ど～～しても処理時間を減らすのが難しい！という場合は別のアプローチを検討しましょう。[^4]

## 結論

キャッシュ時間を延ばせば良いってもんじゃない！

キャッシュ時間を設定した上で、更にオリジンリクエスト数を抑えたいならば、別のアプローチを検討すべき[^4]でしょう。また、意図せずその設定が意味を為さなくなってしまっていないかも確認しなければなりません。[^3]

また、画像等の静的コンテンツならともかく、動的コンテンツについてはそもそもキャッシュ時間を長めにして良い物と悪い物があります。[^5]  
やっぱり、ユーザーに対して常に最新のコンテンツを配信出来るに越したことはないのですから、キャッシュ時間の調整に関しては「やれたらやる。」程度に考えておけば良いと思います。

[^1]: 勿論、実際はその時のリクエストのばらつき具合にもよる。
[^2]: $\frac{N}{s} \times t \geq 1$という条件の意味  
キャッシュが効く時間の間、1回以上リクエストが来ればキャッシュする意味がある。  
例えば2分に1回しかリクエストが来ないのに、キャッシュ時間を1分にしていてはキャッシュ時間を設定する意味が無い。($\frac{N}{s} = 0.5$なので、キャッシュ時間$t = 2$分以上は無いと意味が無い。)  
ちなみに$\frac{N}{s} \times t < 1$の時、キャッシュが全く効かないので全リクエスト数=オリジンリクエスト数になる。すなわち、$R_{\rm Orig} = \lfloor 60 \times \frac{N}{s} \rfloor$回となる。
[^3]: [キャッシュヒット率を上げるために何より最も重要なこと](https://qiita.com/shinjiroy/items/4681185f5604e59b26de#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E3%83%92%E3%83%83%E3%83%88%E7%8E%87%E3%82%92%E4%B8%8A%E3%81%92%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AB%E4%BD%95%E3%82%88%E3%82%8A%E6%9C%80%E3%82%82%E9%87%8D%E8%A6%81%E3%81%AA%E3%81%93%E3%81%A8)をご覧ください
[^4]: [キャッシュ時間の調整以外の方法](https://qiita.com/shinjiroy/items/4681185f5604e59b26de#%E3%82%AD%E3%83%A3%E3%83%83%E3%82%B7%E3%83%A5%E6%99%82%E9%96%93%E3%81%AE%E8%AA%BF%E6%95%B4%E4%BB%A5%E5%A4%96%E3%81%AE%E6%96%B9%E6%B3%95)をご覧ください
[^5]: [コンテンツの更新を適切に反映する](https://qiita.com/shinjiroy/items/4681185f5604e59b26de#%E3%82%B3%E3%83%B3%E3%83%86%E3%83%B3%E3%83%84%E3%81%AE%E6%9B%B4%E6%96%B0%E3%82%92%E9%81%A9%E5%88%87%E3%81%AB%E5%8F%8D%E6%98%A0%E3%81%99%E3%82%8B)をご覧ください