Amazon CloudFront で同一オブジェクト同時リクエスト時の Request Collapsing について確認してみました

2022.11.01

いわさです。

高速な Web サイトを構築する際にやはり CloudFront は欠かせないですよね。
エッジにキャッシュが存在している際に、キャッシュヒットする条件であればエッジがそのままレスポンスしてくれるので、オリジンの負荷を大幅に下げることが出来ます。

ところで、キャッシュが期限切れでエッジに存在してない状態で瞬間的に大量のリクエスト (100 とか 1000 とか) が送信されたとき、どうなるかご存知でしょうか。
全てのリクエストがキャッシュなしと判断されオリジンに送信されるのでしょうか。

答えは「キャッシュがない状態で短期間にリクエストが集中しても、オリジンへのリクエストは 1 回」です。
これは、CloudFront の機能の request collapsing という機能です。日本語ドキュメントでは「リクエストを折りたたむ」と表現されています。

有効期限が切れている場合、CloudFront はすぐにオリジンにリクエストを送信します。ただし、同一オブジェクトへの同時リクエストがある場合 (同じキャッシュキーを使用)、CloudFront が最初のリクエストへのレスポンスを受信する前に、同一オブジェクト (同じキャッシュキー) への追加のリクエストがエッジロケーションに届く場合、CloudFront は一時停止してから、追加のリクエストをオリジンに転送します。この短い一時停止により、オリジンでの負荷を減らすことができます。

本日はこの「リクエストを折りたたむ」機能を検証してみました。

検証環境を用意

以下の Application Load Balancer + EC2 (Apache) テンプレートを使って環境を用意します。

さらに、追加で PHP をインストールし、一定時間スリープするエンドポイントを用意します。
今回は折りたたみ処理を確認したいのでオリジンですぐにレスポンス処理されるとキャッシュされていただけなのかがよくわからないからです。

あとは CloudFront でディストリビューションを作成し、ALB をオリジンに指定します。

TTL は適当に 30 秒くらいにしました。
また、キャッシュキーが折りたたみの挙動のポイントなので適当なヘッダーをキャッシュキーに含めています。

同一キーでリクエスト

今回は JMeter を使ってまとめてリクエストを送信してみました。
まずは 1 秒間に 5 件のリクエストを送信してみます。
エンドポイントのスリープ時間は 20 秒ほどです。

送信出来たら EC2 上で Apache のアクセスログを確認します。

10.0.0.104 - - [24/Oct/2022:05:37:07 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:37:10 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.0.104 - - [24/Oct/2022:05:37:17 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:37:20 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:37:26 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:37:27 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:37:30 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.0.104 - - [24/Oct/2022:05:37:37 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:37:40 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"

お、オリジンアクセスが 1 回だけ発生しましたね。

JMeter でリクエストごとの処理結果を確認してみると、約 20 秒後にすべてのリクエストにほぼ同時にレスポンスが返ってきました。
5 件のうち 1 件は Miss from cloudfront です。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 12
Connection: keep-alive
Date: Mon, 24 Oct 2022 05:37:36 GMT
Server: Apache/2.4.54 () PHP/5.4.16
X-Powered-By: PHP/5.4.16
X-Cache: Miss from cloudfront
Via: 1.1 c6e672f66f0c430c2e883081a311e09e.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRT20-C1
X-Amz-Cf-Id: hmPBe6qOqK7jJpT3GL8lJMh9rZh5Rd2Uqis0Qmxpr2oEJhcPURheOQ==

残り 4 件は Hit してますね。

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 12
Connection: keep-alive
Date: Mon, 24 Oct 2022 05:37:36 GMT
Server: Apache/2.4.54 () PHP/5.4.16
X-Powered-By: PHP/5.4.16
X-Cache: Hit from cloudfront
Via: 1.1 4c88cf886add957cd777a3b7eec7de7c.cloudfront.net (CloudFront)
X-Amz-Cf-Pop: NRT20-C1
X-Amz-Cf-Id: UZSd7xu6LN6Sjuex-IIIWfBdFYY7C90g5slJOszxZu8z-7JruEKxkQ==

なるほど、これが折りたたみ!

別のキャッシュキー

今度はキャッシュキーの条件に含めたカスタムヘッダーに異なる値を設定して同時に送信してみましょう。

CloudFront からオリジンへは 2 回リクエストが送信されましたね。

10.0.1.149 - - [24/Oct/2022:05:43:30 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.0.104 - - [24/Oct/2022:05:43:37 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:43:40 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:43:44 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:43:47 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:43:50 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:43:54 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:43:57 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:44:00 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"

JMeter のレスポンスを確認してみると、Miss が 2 つで Hit が 8 つでした。
同一のキャッシュキーごとに折りたたまれています。なるほどなるほど。

キャッシュしない

この折りたたみの挙動ですが、キャッシュキーに紐づくものになっており、キャッシュされない設定の場合は折りたたみもされないとドキュメントには記述されています。
こちらも念の為動作確認してみます。

今度は 10 件のオリジンリクエストが発生しました。

10.0.1.149 - - [24/Oct/2022:05:47:20 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.0.104 - - [24/Oct/2022:05:47:27 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:47:30 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.0.104 - - [24/Oct/2022:05:47:32 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.1.149 - - [24/Oct/2022:05:47:32 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.1.149 - - [24/Oct/2022:05:47:33 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:47:33 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:47:33 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:47:37 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:47:40 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"
10.0.1.149 - - [24/Oct/2022:05:47:42 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.1.149 - - [24/Oct/2022:05:47:43 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.1.149 - - [24/Oct/2022:05:47:43 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.1.149 - - [24/Oct/2022:05:47:43 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.1.149 - - [24/Oct/2022:05:47:43 +0000] "GET / HTTP/1.1" 200 12 "-" "Amazon CloudFront"
10.0.0.104 - - [24/Oct/2022:05:47:47 +0000] "GET / HTTP/1.1" 200 12 "-" "ELB-HealthChecker/2.0"

さいごに

本日は Amazon CloudFront で同一オブジェクト同時リクエスト時の Request Collapsing (折りたたみ)について検証してみました。

ドキュメントに記載のとおり、キャッシュが存在しない場合でも CloudFront がオリジンへアクセス集中が発生しないようにうまく制御してくれていることが確認出来ました。
この機能すごいな。