概要
最近、個人開発のホスト先としてよく Cloudflare Workers を利用しています。
ホスト先としては定番の選択肢の1つですが、僕自身は使い始めたばかりなのもあり裏側のアーキテクチャまではよく分からないまま触っていました。
そこで、一度しっかり仕組みを理解したいなと思い、普段使い慣れているサーバーレスサービス(Cloud Run, Cloud Run Functions)と比較しながら、Cloudflare Workers の優れている点を整理してみました。
Cloudflare Workers について
Cloudflare Workers とは Cloudflare が提供するサーバーレスサービスの1つです。
特筆すべき特徴としては以下です。
グローバルエッジでの分散実行
世界数百箇所にある Cloudflare のデータセンター(エッジネットワーク)にコードが自動で一斉デプロイされます。
ユーザーがアクセスする際は最も近い場所で処理が実行されるため、地理的な位置に関わらず高速にレスポンスを返すことが可能です。
コールドスタートが実質ゼロ
従来のコンテナベースのサーバーレス(Cloud RunやAWS Lambdaなど)で課題になりがちな「初回起動時の遅延(コールドスタート)」がありません。
V8エンジンという軽量で隔離された実行環境(Isolate)を利用しているため、アクセスが不定期な環境でも常に高速のレスポンスが期待できます。
詳しいインフラ内部の挙動については、以下公式の解説ドキュメントが参考になりました。
・How Workers works · Cloudflare Workers Docs
・Workers concepts・Build applications with Cloudflare Workers (Learning Paths)
【-比較してみた-】初期リクエストの応答速度 & スパイク耐性
実際にどれくらいの差が出るのかを検証してみます。
比較対象にはコンテナベースのサーバーレスサービスである「Cloud Run」と「Cloud Run Functions」を選びました。
(※ Cloud Run Functions は旧 Cloud Functions の第 2 世代のことで、現在は Cloud Run のインフラに統合されています。そのため今回は Cloud Run で『関数モード( CPU をリクエスト処理中のみ割り当てる設定)』にして検証を行います)
検証環境
以下の通り検証環境を設定しました。
1. アプリケーション
検証にあたり、Honoを用いて各プラットフォームで共通して動作するコードを用意しました。
(Antigravity CLI で雑に作りました)
機能としては以下2つのエンドポイントを用意しています。
-
ルート(
/): 主に起動確認用です。ステータスとプラットフォーム名を返します -
/bench: 三角関数を使ったループ計算を回し、CPUにしっかり負荷をかけるベンチマーク処理を実行します
一応、コードは以下の GitHub リポジトリに公開しています。
→ yuuzin217/workers-boot-time-test
もともと /bench の処理には「フィボナッチ数計算」を使用していたんですが、これだと Cloudflare Workers の「CPU時間の無料枠(10ms)」を超えてしまうため、処理時間が線形にスケールする「三角関数の連続加算処理」に変えています。
2. インフラ設定
検証対象となる3つの環境の設定パラメーターです。
Cloudflare は無料枠で利用しています。
Cloud Run(+ Functions)はアクセスがない場合に完全にインスタンスが 0 台(コールドスタート状態)になるよう設定しています。
また、自分は関西に住んでいるのでリージョンは大阪に設定しています。
| 項目 | Cloudflare Workers(無料枠) | Cloud Run (関数モード) | Cloud Run (通常モード) |
|---|---|---|---|
| アーキテクチャ | V8 Isolate | コンテナベース (Knative) | コンテナベース (Knative) |
| デプロイ地域 | グローバルエッジ |
asia-northeast2 (大阪) |
asia-northeast2 (大阪) |
| 最小インスタンス数 | - (自動) |
0 (--min-instances 0) |
0 (--min-instances 0) |
| CPU制御 | - (自動) |
リクエスト時のみ割り当て--cpu-throttling
|
常に割り当て(待機中も維持)--no-cpu-throttling
|
3. クライアント側
- 実行環境: WSL2 (Ubuntu)
-
使用ツール:
*curl: 単発リクエスト用
*hey: スパイクアクセス時の挙動観測用
比較1: 初期リクエストの応答速度
負荷テストの前に curl でリクエストして初期リクエストの応答速度を確認してみます。
(Cloud Run, Cloud Functions はインスタンス数0 の状態です)
{
# Cloudflare Workers の計測
curl -o /dev/null -s -w "Workers: Status=%{http_code}, Time=%{time_total}s\n" \
https://boot-time-test.***.workers.dev/ &
# Cloud Run (関数モード) の計測
curl -o /dev/null -s -w "Functions: Status=%{http_code}, Time=%{time_total}s\n" \
https://hono-functions-mode-***.asia-northeast2.run.app/ &
# 通常の Cloud Run の計測
curl -o /dev/null -s -w "Cloud Run: Status=%{http_code}, Time=%{time_total}s\n" \
https://hono-cloudrun-mode-***.asia-northeast2.run.app/ &
# すべての並列処理の完了を待つ
wait
} > initial_startup_bench.txt
出力結果
Workers: Status=200, Time=0.139246s
Functions: Status=200, Time=0.749072s
Cloud Run: Status=200, Time=0.885163s
コールドスタートが実質存在しないと謳っているだけあり、やはり Workers が抜きん出てめちゃめちゃ速いですね。
Cloud Run, Functions もコールドスタートで 1 秒以内とはいえ、Workers とは約 5 ~ 6 倍くらいの差があります。
比較2: スパイクアクセスの比較
次に各環境に対して、Go 製の HTTP 負荷テストツール hey を使用し、同時に多数のリクエストが集中する「スパイクアクセス」を模した負荷をかけ応答性能の差を計測します。
負荷パラメーター
- 同時接続数 (Concurrency): 20
- 総リクエスト数 (Total Requests): 1000
-
検証コマンド:
hey -n 1000 -c 20 <対象URL>
# 1. Cloudflare Workers に1000発ノック
hey -n 1000 -c 20 "https://boot-time-test.***.workers.dev/bench?iter=500000" > workers_load_test.txt
# 2. Cloud Run (関数モード) に1000発ノック
hey -n 1000 -c 20 "https://hono-functions-mode-***.asia-northeast2.run.app/bench?iter=500000" > functions_load_test.txt
# 3. 通常の Cloud Run に1000発ノック
hey -n 1000 -c 20 "https://hono-cloudrun-mode-***.asia-northeast2.run.app/bench?iter=500000" > cloudrun_load_test.txt
出力内容についての説明
hey の実行結果には以下が出力されます。
Summary(全体の概要)
- Total: リクエストすべて(今回で言えば1000回)を処理し終えるまでにかかった「合計時間」です
- Slowest: 1000回の中で「最も応答が遅かった1発」にかかった時間です
- Fastest: 1000回の中で「最も応答が速かった1発」にかかった時間です
- Average: リクエスト1発あたりにかかった「平均応答時間」です
- Requests/sec: 1秒間に何回リクエストを捌けたかという「処理能力(スループット)」です
Response time histogram(応答時間のヒストグラム)
- どの応答時間の範囲に、どれだけの数のリクエストが返ってきたかを視覚的に表したグラフです
Latency distribution(応答時間のばらつき)
- 「全体の〇%のリクエストが、何秒以内に返ってきたか」を段階別に表しています
Details(通信プロセスの詳細内訳)
- 1回のリクエストが始まってから終わるまでの「通信全体のステップ」を細かく分解し、どこにどれだけの時間がかかったのかをミリ秒単位で示したデータです
- それぞれの行に、左から 「平均値 (average)」「最速値 (fastest)」「最遅値 (slowest)」 が並んでいます
出力結果
Cloudflare Workers
Summary:
Total: 4.3303 secs
Slowest: 0.2439 secs
Fastest: 0.0219 secs
Average: 0.0772 secs
Requests/sec: 230.9322
Response time histogram:
0.022 [1] |
0.044 [125] |■■■■■■■■■■■■■■■
0.066 [328] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.089 [186] |■■■■■■■■■■■■■■■■■■■■■■■
0.111 [255] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.133 [52] |■■■■■■
0.155 [28] |■■■
0.177 [14] |■■
0.200 [6] |■
0.222 [0] |
0.244 [5] |■
Latency distribution:
10% in 0.0406 secs
25% in 0.0541 secs
50% in 0.0722 secs
75% in 0.0986 secs
90% in 0.1124 secs
95% in 0.1358 secs
99% in 0.1807 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0014 secs, 0.0000 secs, 0.0813 secs
DNS-lookup: 0.0007 secs, 0.0000 secs, 0.0312 secs
req write: 0.0001 secs, 0.0000 secs, 0.0017 secs
resp wait: 0.0753 secs, 0.0218 secs, 0.1928 secs
resp read: 0.0003 secs, 0.0000 secs, 0.0044 secs
Status code distribution:
[200] 1000 responses
Cloud Run
Summary:
Total: 23.9047 secs
Slowest: 0.8829 secs
Fastest: 0.1176 secs
Average: 0.4723 secs
Requests/sec: 41.8328
Total data: 108894 bytes
Size/request: 108 bytes
Response time histogram:
0.118 [1] |
0.194 [4] |
0.271 [3] |
0.347 [5] |
0.424 [36] |■■
0.500 [752] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.577 [196] |■■■■■■■■■■
0.653 [1] |
0.730 [1] |
0.806 [0] |
0.883 [1] |
Latency distribution:
10% in 0.4359 secs
25% in 0.4561 secs
50% in 0.4743 secs
75% in 0.4929 secs
90% in 0.5091 secs
95% in 0.5181 secs
99% in 0.5564 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0015 secs, 0.0000 secs, 0.1196 secs
DNS-lookup: 0.0006 secs, 0.0000 secs, 0.0334 secs
req write: 0.0000 secs, 0.0000 secs, 0.0011 secs
resp wait: 0.4704 secs, 0.0549 secs, 0.7630 secs
resp read: 0.0001 secs, 0.0000 secs, 0.0009 secs
Status code distribution:
[200] 1000 responses
Cloud Functions
Summary:
Total: 23.3889 secs
Slowest: 0.8937 secs
Fastest: 0.1092 secs
Average: 0.4619 secs
Requests/sec: 42.7553
Total data: 108897 bytes
Size/request: 108 bytes
Response time histogram:
0.109 [1] |
0.188 [3] |
0.266 [4] |
0.345 [10] |
0.423 [53] |■■
0.501 [853] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.580 [70] |■■■
0.658 [3] |
0.737 [2] |
0.815 [0] |
0.894 [1] |
Latency distribution:
10% in 0.4282 secs
25% in 0.4462 secs
50% in 0.4661 secs
75% in 0.4824 secs
90% in 0.4983 secs
95% in 0.5034 secs
99% in 0.5372 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0019 secs, 0.0000 secs, 0.1060 secs
DNS-lookup: 0.0005 secs, 0.0000 secs, 0.0263 secs
req write: 0.0000 secs, 0.0000 secs, 0.0039 secs
resp wait: 0.4598 secs, 0.0420 secs, 0.7933 secs
resp read: 0.0001 secs, 0.0000 secs, 0.0009 secs
Status code distribution:
[200] 1000 responses
スパイクアクセス検証のまとめ、考察
3つの環境に対して同時に1000リクエスト(20並列)を叩き込んでみましたが、やはり Cloudflare Workers の性能が頭一つ抜けているように思えます。
Cloudflare Workers
平均応答時間 0.0772秒(約77ms)、スループット 230.9 Requests/sec という数値は、他の2つに対して約6倍の速度、5.5倍の処理能力を叩き出していることになります。
特筆すべきは最遅クラスのリクエストですら 0.18秒 に抑え込んでいる点です。
コンテナの起動等のオーバーヘッドが一切存在しない V8 Isolate ならではの強みがここに表れているのかなと感じます。
Cloud Run(+ Functions)
Cloud Run(通常モード)および Cloud Run Functions(関数モード)も、平均応答時間 0.46〜0.47秒、スループット 約42 Requests/sec という数値になりました。
コンテナ型インフラとしては十分に優秀で、実用上も全く問題のない安定感に見えます。
ただし、コンテナというアーキテクチャの性質上、並列アクセスに対してインスタンスを割り当てる際の物理的なオーバーヘッドが発生し、それが Workers との処理遅延の差として現れているように思います。
そもそも「V8 Isolate」とは何か?
速いことはわかったので、Cloudflare が採用した「V8 Isolate」とは、一体どのような仕組みなのか整理してみました。
まず、「Isolate(アイソレート)」という言葉は、Googleが開発した C++ 製の JavaScript エンジン「V8」のコア概念のことです。
V8の公式ドキュメントおよび C++ の API リファレンスでは、以下のように説明されています。
"An isolate is a VM instance with its own heap."
(Isolateとは、独自のヒープ領域を持つJavaScript仮想マシンのインスタンスである)
ーー V8公式ドキュメント(Getting started with embedding V8)より引用
"Isolate represents an isolated instance of the V8 engine. V8 isolates have completely separate states. Objects from one isolate must not be used in other isolates."
(IsolateはV8エンジンの隔離されたインスタンスを表す。完全に分離された状態を持ち、あるIsolateのオブジェクトを他のIsolateで使用してはならない)
ーー V8 C++ APIリファレンス(v8::Isolate Class Reference)より引用
公式の言葉を借りると難しく聞こえますが、要するに
「1つの実行エンジン(V8)の中に、メモリ(ヒープ領域)や状態が完全に区切られた『独立した個室(インスタンス)』をたくさん作る仕組み」 のことです。
従来のサーバーレスが「OSプロセスごとに環境を分ける(一軒家を何軒も建てるイメージ)」のに対して、V8 Isolateは「1つの巨大なプロセスのなかに、完全に壁で隔離された個室(部屋)を大量に作る」というアプローチを取っています。
コンテナと比較して簡単に図解すると以下のような感じでしょうか。
【コンテナ型(Cloud Runなど)】
[ 物理サーバー ]
└── [ ホストOS ]
└── [ コンテナランタイム (Docker等) ]
├── [ コンテナ1: OS環境 + Node.js等 + コード ] (数十〜数百MB)
└── [ コンテナ2: OS環境 + Node.js等 + コード ] (数十〜数百MB)
【V8 Isolate型(Cloudflare Workers)】
[ 物理サーバー ]
└── [ ホストOS ]
└── [ 1つのV8プロセス (常に起動して待機) ]
├── [ Isolate 1: 隔離されたメモリ空間 + コード ] (数MB)
├── [ Isolate 2: 隔離されたメモリ空間 + コード ] (数MB)
└── [ Isolate 3: 隔離されたメモリ空間 + コード ] (数MB)
「OS環境ごと新しく立ち上げるコンテナ」に対して、Workersは「すでに動いているエンジンの中に、メモリの壁を作ってコードを流し込むだけのIsolate」を採用しています。
そのため、OSを立ち上げるような無駄なオーバーヘッドが一切なく、あのコールドスタートを感じさせない起動速度を実現できているんだと思います。
また、この構造の差はスパイク(突発的なアクセス集中)が発生したときにも有効です。
コンテナの場合、急激な負荷を捌くために「新しいコンテナを立ち上げる(スケールアウトする)」必要があり、これに約数百ミリ秒〜数秒のラグが発生してしまいます。
一方、Isolateはすでに起動しているV8エンジンの中に「新しい個室(メモリ空間)」を切り出すだけなので、わずか数ミリ秒(公式いわく5ms未満)で準備が完了します。急激なスパイクが来ても、一瞬で Isolate を作成しスケールすることが可能です。
実際、Cloudflareの公式ブログ(Cloud Computing without Containers)でもコールドスタートやコンテナとの構造的な違いについて触れられており、単一のプロセス内で数百〜数千のIsolateをシームレスに切り替えて動かせる効率の良さが語られています。
おわりに
Cloudflare Workers 最高です!!
ただ唯一ネックかなと思う点は 1 リクエストのCPU上限かなと思います。
以下の資料を読む限り、有料版にしても 1 リクエストのCPU上限は最大で 5 分とのことなので、当然ですが重い処理を実行させるのには向かないと思います。
limits/#account-plan-limits
そのため、それぞれの特性を理解した上で、
- 軽量で高速なレスポンスが求められるAPIや、ユーザーに近いエッジでの処理: Cloudflare Workers
- 長時間の計算・重い処理が必要な処理: AWS Lambda や Cloud Run
のように使い分けるのが適切かなと思います。