✈️

Cloudflare CDN でオリジン負荷軽減の為の Request Collapsingを検証してみた

に公開

はじめに

CDNを使う大きなメリットの一つは「オリジンサーバーの負荷軽減」です。
キャッシュが空の状態で大量のリクエストが同時に来たらどうなるでしょうか

普通に考えると、100件のリクエストが同時に来たら100件全部がオリジンに殺到しそうです。しかし、Cloudflareには Request Collapsing(リクエストコラプシング)という機能があり、これを防いでくれます。
https://developers.cloudflare.com/cache/concepts/revalidation/

今回は実際に検証して、この機能がどう動くのか確かめてみました。

Request Collapsingとは

Request Collapsingは、同一リソースへの同時リクエストを束ねてオリジンへのリクエストを1回にまとめる機能です。

Cloudflareの公式ドキュメントによると

1,000件のリクエストが同時に単一のCloudflareデータセンターに到着し、リクエストされたアセットがCloudflareのキャッシュにない場合(キャッシュミス)、キャッシュロックを使用してオリジンと通信します。最初のリクエストだけがオリジンにアセットを取得しに行き、残りの999件は最初のリクエストがデータを取得するのを待ちます。

ということなので、今回は17MBの大きめの画像を用意
100件同時リクエストを送信してどんな振る舞いになるか観察します。

検証環境

  • CDN: Cloudflare
  • オリジン: NGINX (Google Cloud)
  • テスト画像: 約17MBのJPGファイル
  • 負荷ツール: hey
  • Request Collapsing機能はデフォルトでonになっています。

検証方法

1. キャッシュをパージ

まず、完全なキャッシュミス状態を作るためにキャッシュをパージします。

curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
  -H "Authorization: Bearer {token}" \
  -H "Content-Type: application/json" \
  --data '{"files":["https://taka-cloudflare.win/ana-1.jpg"]}'

2. 同時リクエストを発生

heyコマンドで100件のリクエストを同時に送信します。

hey -n 100 -c 100 https://taka-cloudflare.win/ana-1.jpg
  • -n 100: 合計100リクエスト
  • -c 100: 同時接続数100(つまり100件が一斉にリクエスト)

3. オリジンログを監視

tail -f /var/log/nginx/access.log | grep "ana-1.jpg"

検証結果

heyの出力

Summary:
  Total:        20.0036 secs
  Slowest:      20.0029 secs
  Fastest:      20.0000 secs
  Average:      20.0014 secs
  Requests/sec: 4.9991
  
  Total data:   1785190500 bytes
  Size/request: 17851905 bytes

Response time histogram:
  20.000 [1]  |■■■
  20.000 [10] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  20.001 [7]  |■■■■■■■■■■■■■■■■■■■■■■
  20.001 [13] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  ...

Status code distribution:
  [200] 100 responses

100件全てのリクエストがほぼ同じ時間(約20秒)で完了しています。

オリジンのログ

162.158.118.165	16/Dec/2025:20:07:41 +0900 GET /ana-1.jpg HTTP/1.1	200	{\"Host\":\"taka-cloudflare.win\",\"Connection\":\"Keep-Alive\",\"accept-encoding\":\"gzip, br\",\"X-Forwarded-For\":\"2400:4050:33a3:4a00:be24:\",\"CF-RAY\":\"9aedb7e32b1bfca6-NRT\",\"User-Agent\":\"hey/0.0.1\",\"Content-Type\":\"text/html\",\"cdn-loop\":\"cloudflare; loops=1\",\"CF-Connecting-IP\":\"2400:4050:33a3:4a00:be24:\",\"True-Client-IP\":\"2400:4050:33a3:4a00:be24:\",\"CF-IPCountry\":\"JP\",\"CF-Visitor\":\"{\\\"scheme\\\":\\\"https\\\"}\",\"X-Forwarded-Proto\":\"https\",\"X-Cloudflare-Debug\":\"sample-pass\",\"X-Takaaki-Request-Header\":\"2606:4700::6812:17aa\",\"X-Takaaki-Request-Port\":\"443\",\"taka-header\":\"takaakisuzuki\"}

オリジンに届いたリクエストは1件だけでした、ちゃんとコラプシングが動作してますね

100件のリクエストを送ったのに、オリジンサーバーには1件しかログが残っていません。
これがRequest Collapsingの効果となります。

なぜ全リクエストが同じ時間で完了するのか

ここで疑問が生まれます。
99件はキャッシュから返されるなら、もっと速く完了するのでは
答えはストリーミング配信にあります。

実際の動作(ストリーミング配信)

時間 →→→→→→→→→→→→→→→→→→→→→→→→→→→→→

オリジン:    [====17MBを少しずつ送信====]
                ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
Cloudflare:  受信しながら同時に全員へ配信
                ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
リクエスト1:   [====受信中====]
リクエスト2:   [====受信中====]
リクエスト3:   [====受信中====]
    ...
リクエスト100: [====受信中====]

→ 全員がほぼ同時に完了

Cloudflareはオリジンからデータを受信しながら、待機中の全リクエストに対してリアルタイムでストリーミング配信しています。だから全てのリクエストが同じタイミングで完了しています。

heyのデータでなんとなくわかる

Details (average, fastest, slowest):
  resp wait:  0.1318 secs, 0.0247 secs, 1.0221 secs
  resp read: 19.8295 secs, 18.9381 secs, 19.9368 secs
  • resp wait(待機時間): 最大でも約1秒。99件のリクエストはすぐにストリーミングに参加
  • resp read(データ受信時間): 全リクエストで約19〜20秒。全員が同時にデータを受け取った証拠

GraphQLで詳細分析

Cloudflare GraphQL APIを使うと、各リクエストの詳細を確認できます。

{
  viewer {
    zones(filter: {zoneTag: "YOUR_ZONE_ID"}) {
      httpRequestsAdaptive(
        filter: {
          datetime_gt: "2025-12-16T11:07:00Z"
          datetime_lt: "2025-12-16T11:08:00Z"
          clientRequestPath: "/ana-1.jpg"
        }
        limit: 10
      ) {
        datetime
        cacheStatus
        originResponseDurationMs
        coloCode
      }
    }
  }
}

結果を見ると

{
  "data": {
    "viewer": {
      "zones": [
        {
          "httpRequestsAdaptive": [
            {
              "cacheStatus": "hit",
              "coloCode": "NRT",
              "datetime": "2025-12-16T11:07:39Z",
              "originResponseDurationMs": 0
            },
            {
              "cacheStatus": "hit",
              "coloCode": "NRT",
              "datetime": "2025-12-16T11:07:39Z",
              "originResponseDurationMs": 0
            },
            {
              "cacheStatus": "miss",
              "coloCode": "NRT",
              "datetime": "2025-12-16T11:07:39Z",
              "originResponseDurationMs": 7
            },
  • cacheStatus: 最初の1件がmiss、残りはhit
  • originResponseDurationMs: missの1件だけ値があり、hitは0

expiredの場合は少し違う

今回はキャッシュパージ後のmiss状態でテストしましたが
キャッシュが期限切れ(expired)の場合は動作が少し異なります。

expired時の動作(stale-while-revalidate)

リクエスト1〜100 → 期限切れキャッシュから即座に配信(stale)

                  同時にrevalidationをオリジンへ送信

                  オリジンが「変更なし」と返答

                  キャッシュのTTLを更新

この場合、オリジンへのrevalidationリクエストが複数送られることがあります(同一データセンター内の複数サーバーから)。ただし、フルコンテンツの取得ではなく条件付きリクエスト(If-Modified-Since)なので、負荷は軽微です。

まとめ

Request Collapsingは、特に以下のような状況で威力を発揮します

  • 大規模なキャッシュパージ後のアクセス集中
  • 人気コンテンツの公開直後
  • TTL切れのタイミングでの同時アクセス

17MBの画像を100回ダウンロードしても、オリジンサーバーへの負荷は1回分
これがCloudflare CDNでデフォルトでonになっている機能になります。

検証に使用したコマンド

# 負荷テストツールのインストール
go install github.com/rakyll/hey@latest

# キャッシュパージ後に同時リクエスト
hey -n 100 -c 100 https://your-domain.com/large-image.jpg

# オリジンログの監視
tail -f /var/log/nginx/access.log | grep "large-image"

ぜひ皆さんも気になったら自分の環境で試してみてください。

Discussion