PaaSやマルチテナント型のWebホスティングでは、同じ入口に大量のホスト名とアクセスが集まります。
Workerを軽く書くだけでは不十分で、KV Read、静的ファイル取得、キャッシュの反映遅延、存在しないホストへのアクセスまで、ひとつの経路として設計する必要がありました。
KamuiDashの公開入口では、Cloudflare WorkerがHostからテナントアプリを解決します。解決したルートに応じて、動的アプリまたは静的配信基盤へリクエストを振り分けます。基本構成はCloudflareを入り口としたマルチテナントPaaSの設計で紹介しました。
今回は、その入口で増え続けるKV Readをどう減らしたかに絞ります。結論から言うと、最終的な狙いは「1アクセスごとにKVを読む」状態をやめ、同じCloudflareデータセンター内では、短時間ルーティング結果を共有することでした。
最初の問題は「正しいが、毎回読む」こと
初期のルーティングは単純でした。独自ドメインならドメイン対応をKVから読み、そこから得た内部アプリ名でもう一度KVを読みます。プラットフォームのサブドメインでも、アプリの実行先や静的サイト情報をKVから解決していました。
この構成は分かりやすく、低トラフィックでは十分です。しかしPaaS全体のアクセスが増えると、ユーザーのアプリが何も変更されていない時間にも、同じルーティング情報を読み続けます。
1. プラットフォーム所有のHostはドメイン対応KVを読まない
最初の削減は、KVを使わなくても分かることをKVへ聞かないことでした。プラットフォームのapexとワイルドカード配下は、すでに内部アプリ名として扱えるHostです。独自ドメインだけが、公開Hostから内部Hostへの変換を必要とします。
2. 動的・静的のルーティングKVを統合する
次に、動的アプリと静的サイトで分かれていた対応表を、一つのルーティングKVへ統合しました。Workerはアプリ名を一度引けば、配信種別と必要な情報を同時に取得できます。
一つのレコードへ
移行時には、旧KVから新KVへレコードをコピーし、件数と重複を確認してからWorkerを切り替えました。新旧の読み取りフォールバックは残していません。移行途中の分岐が長く残ると、どのKVが正なのか分からなくなるためです。
3. 静的本文キャッシュをKVより前へ移す
静的サイトの同じURLが何度も読まれるなら、Hostの解決より先に本文を返せます。そこで、公開URLをキーにした静的コンテンツキャッシュを、すべてのルーティング処理より前へ移しました。
- Host解決
- ルーティングKV
- 静的か判定
- 本文キャッシュ
- 静的ファイル取得
- 本文キャッシュ
- Host解決
- ルーティングKV
- 静的ファイル取得
共有キャッシュには他の応答が入る可能性があるため、Worker自身が保存した応答だけに内部マーカーを付けています。HIT時にはマーカーを確認し、利用者へ返す前に削除します。後段から同名ヘッダーが渡ってきても削除し、内部状態が公開レスポンスへ漏れないようにしています。
4. Route Cacheでルーティング結果そのものを共有する
静的本文以外にも、Hostから最終的なルーティングレコードまでをまとめてCache APIへ保存しました。動的アプリでは本文をキャッシュせず、経路だけをキャッシュします。
Cache keyは正規化したHostから生成し、プラットフォームが管理する範囲へ閉じています。具体的な内部パスと識別子は公開していません。
保存するのは、状態と配信先情報を検証した正常なレコードだけです。必要な値が欠けている場合は、暗黙の既定値へフォールバックさせず、エラーとして安全側に閉じます。内部のフィールド名と識別子は省略します。
5. 存在しないHostも短時間だけキャッシュする
正常なルートだけをキャッシュすると、存在しないHostへのアクセスは毎回KVを読みます。Cloudflare KVでは、存在しないキーを読んだ操作も課金対象です。
そこで、存在しないことが確定した結果だけを短時間negative cacheするようにしました。処理中の状態、壊れたレコード、上流エラーはキャッシュしません。
副作用は、新規アプリや新規独自ドメインがKVへ登録される前にアクセスされると、登録後もしばらく404が残ることです。Route CacheとKV内蔵キャッシュの保持時間をそろえ、既存の反映幅を大きく変えずに、課金対象のKV操作だけを減らしています。
TTLは短ければよいわけではない
TTLはコストだけで決めず、デプロイ後に新しいルートとコンテンツが反映されるまでの許容時間から決めます。KV内蔵キャッシュ、Route Cache、静的本文キャッシュを独立に伸ばすのではなく、重なったときの反映遅延を一つのfreshness budgetとして管理しています。具体的な値は公開していません。
Cache APIとWorkers Cachingを混同しない
今回利用したのは、Workerコード内から操作するCache APIです。CloudflareにはWorker実行前に応答を返すWorkers Cachingもありますが、両者は別の機能です。
- Worker request課金あり
- CPU実行あり
- KV・後段ReadはHIT時0
- Worker request課金あり
- CPU実行なし
- KV・後段処理は0
現在のWorkerは静的・動的共通のゲートウェイです。動的応答はprivateなのでWorkers Cachingへ保存されませんが、キャッシュ確認の段階は追加されます。Workerコードが軽い現在は、まずRoute CacheでKV Readを減らす方を優先しました。
キャッシュは入力検証とセットで実装する
キャッシュは処理を高速化しますが、間違った値も高速に配布できます。そのため、保存前と読取後の両方でレコードを検証しています。
- スキーマの世代を検証する — 将来の形式変更を暗黙に解釈しません。
- 有効な状態だけを正常ルートとして保存する — 処理中の状態はキャッシュしません。
- 配信に必要な値を検証する — 欠損時に暗黙の既定値へ流しません。
- 内部マーカーを外部へ返さない — 後段由来の同名ヘッダーも削除します。
- 動的応答を共有キャッシュしない — 既定で
Cache-Control: privateを付けます。 - 404へ利用者のpathを反射しない — 固定文言
Page not found.を返します。
devから段階的に適用する
ルーターはすべてのテナントに影響するため、ローカル、dev、本番の順に確認しました。Terraformのplanでは、毎回Workerだけがin-place updateされることを確認し、KV、静的配信、DNS、Tunnelに変更が混ざっていない状態で適用しています。
- 01ローカルテスト正常、404、pending、不正レコード、cache error
- 02dev plan0 add / 1 change / 0 destroy
- 03dev実URL動的、静的、未知Hostを確認
- 04本番 planWorkerだけの更新を再確認
- 05本番実URLplatform、custom、static、404
# cloudflare_workers_script.router will be updated in-place
Plan: 0 to add, 1 to change, 0 to destroy.ローカルでは21ケースを実行し、正常なplatform hostとcustom domain、静的サイト、positive cache、negative cache、pending、不正レコード、KV missを確認しました。本番では動的アプリ、独自ドメイン、静的サイトの2回目HIT、未知Hostの連続404を実URLで確認しています。
残っている最適化
ルーティングKVについては、主要な削減を終えました。次に効くのは、実測に基づく以下の対策です。
- 正常ログのサンプリング — 高トラフィックではログ量そのものがコストになります。
- 更新時の明示的なpurge — 反映時間を保ったまま、安全にキャッシュ効率を高めます。
- 異常トラフィックへの多層防御 — キャッシュだけに依存せず、エッジ側でもリソースを保護します。
まとめ
今回の最適化で重要だったのは、キャッシュ機能を一つ追加することではなく、安い判断から順に並べることでした。
- 01分かっていることはKVへ聞かないプラットフォーム所有Hostは文字列だけで判定します。
- 02同じ意味のレコードを統合する一つのReadで動的・静的を決めます。
- 03本文キャッシュを最初に見る静的HITではルーティング処理そのものを省略します。
- 04正しいルートと404を短時間共有する同じ判断をリクエストごとに繰り返さない構造へ変えます。
- 05反映遅延をコストとして扱うTTLを伸ばす前に、許容できる更新時間とpurgeを設計します。
マルチテナントの入口では、小さなReadが全テナントの全アクセスに掛け算されます。Worker、KV、Cache API、静的配信を別々に見るのではなく、一つのリクエストがどこで止まるかを設計することで、構成を複雑にしすぎずにReadを減らせました。