ブログ一覧

EmDash が真っ白になるのは Free プランの CPU 上限

更新: Web開発者向け

制作実績を 22件まとめて更新した直後、自分のサイトのトップが 503 を返しました。数十秒後には 200 に戻ったのですが、届いた HTML は 2,165 バイトで <head> の途中で切れており、画面は真っ白でした。死活監視は 200 を見て正常と判定したままでした。エッジキャッシュ(Cloudflare が拠点ごとに持つレスポンスの保存領域)は 30日保持する設定なので、気付かなければ 1か月そのままだったはずです。

原因は Cloudflare Workers(EmDash のサイトが動くサーバーレス実行環境)の Free プランが課す CPU 時間の上限、1 リクエスト 10ms でした。サイト運営で使っている EmDash(Astro 製の CMS)は、この上限をふだんから超えて動いています。テンプレートを直しても上限の内には収まらず、上位のプランに上げると打ち切られなくなるだけで、描画は 1 ミリ秒も速くなりません。最初は公開した実績の件数が増えたことを原因と決めて修正を先に入れましたが、実測でその前提は崩れました。

WordPress では「重い=遅い」だったのが、Workers では「重い=切られる」になる

WordPress を運用していた経験では、重いページは表示が遅くなるだけで、途中で切れた HTML が届いた経験はありませんでした。EmDash が動く Cloudflare Workers では、重いページは処理の途中で打ち切られます。

HTML は SSR(リクエストのたびにサーバー側で HTML を組み立てること)で、ストリーミング応答(本文を作りながら送る返し方で、ステータスとヘッダーは本文より先に送られる)として返ります。CPU の上限で途中で打ち切られても 200 はすでに送り終えているため、エラーページには差し替えられません。クエリを付けてキャッシュを迂回すると 52,635 バイトの正常な HTML が返り、壊れていたのはキャッシュに入ったレスポンスだけでした。

打ち切られた本文はエラーに差し替わらず、正常な 200 としてキャッシュに残り続ける

エッジキャッシュの 30日保持は D1(EmDash のコンテンツを持つ Cloudflare のデータベース)の Free プランの読み取り行数の上限への対策として設定した物で、経緯は Free プランの上限を守るために作らなかった仕組みにまとめています。CPU の上限で壊れたレスポンスまで 30日残るとは想定していませんでした。

死活監視は 200 が返るかしか見ておらず、本文が数キロバイトに縮んだことは検知しませんでした。実害はトップページの表示までで、他は正常でした。壊れたレスポンスがキャッシュに残る挙動は、公開した記事が 404 のままになる原因で見た構図と同じです。

10ms は 1 リクエストに割り当てる CPU 時間で、公式にも稀な超過には猶予があると書かれている

Cloudflare の公式ドキュメントは、isolate(Workers がコードを実行する単位で、1つが複数のリクエストを処理する)についてこう説明しています。

Each isolate has some built-in flexibility to allow for cases where your Worker infrequently runs over the configured limit. If your Worker starts hitting the limit consistently, its execution will be terminated according to the limit configured.

isolate には稀に上限を超える分の余裕があり、超過が続くようになって初めて打ち切られ、Error 1102 が返ります。同じページは平均的な Workers が 1 リクエスト 2.2ms しか使わない一方、SSR は 10〜20ms を使うとも書いており、Free プランの 10ms は SSR が使う時間を見込んだ値ではありません。

実測でも、成功した起動の CPU は 316ms まで伸びる一方、打ち切られた起動は 9〜11ms に集中していた

直近7日・アカウント全体の起動ログでは、成功した起動の CPU は最大 316ms まで伸びていた一方、打ち切られた起動は例外なく 9〜11ms でした。

10ms に数えられるのは、次のように JavaScript を実行している時間だけです。

  1. ルーティングとテンプレートの描画
  2. D1 から返った行の変換
  3. JSON-LD の生成

D1 や外部リクエストへの応答待ちは数えられません。上限との比較は 1回の起動で使った CPU 時間の合計で行い、待ちを挟んでも数え直しにはなりません。トップページの描画は実時間 440〜1,100ms に対し、CPU は 12〜57ms でした。

上限に触れるのはキャッシュを外れた描画だけです。キャッシュに当たっている間 Workers は起動しないため、同じ URL を MISS 1回・HIT 5回で叩いても起動ログは MISS の 1件だけです。危険度はアクセス数ではなく MISS の回数に比例し、次の場面で増えます。

  1. コンテンツを更新してキャッシュを失効させた直後
  2. 拠点ごとの初回アクセス
  3. クローラーによる巡回

件数が原因だと思って先に直した

トップページは制作実績を全件(22件)取得してから4件だけ描画していたため、「22件に増えたから落ちた」と決め、取得を4件に絞る修正を先に入れましたが、測り直すと件数が原因という前提は崩れました。

  • トップページ(4件取得): 中央値 51.5ms・最大 113ms
  • 一覧ページ(22件取得): 中央値 28.0ms・最大 99ms
  • 問い合わせページ(実績を取らない): 中央値 12.0ms・最大 69ms

22件取る一覧ページの方が4件しか取らないトップより軽く、重いのはテンプレートの描画そのものでした。

同時に24本の冷えた描画を投げても全て成功し(中央値 23.5ms・最大 158ms)、キャッシュを一斉に失効させた直後の28本でも全て成功しました。切られるかどうかはそのときの余裕次第で、4件に絞った修正は CPU 時間を減らす方向としては正しいものの、上限の5倍にあたる描画を 10ms に収めるほどの効果はありません。

テンプレートを直しても CPU 時間は 10ms に収まらない

EmDash の API ルートだけで認証確認に 3〜5ms、マニフェスト取得に 6〜8ms かかり、記事を組み立てる前に 10ms の 3〜8割を使います。同じ EmDash で動く 5 サイト(このサイトを含む)の CPU 中央値も 24.0〜36.0ms に揃っており、CPU 時間が上限の2〜4倍になるのはテンプレートの書き方の問題ではありません。

Cloudflare Pages に移しても変わりません。Pages の公式ドキュメントにはこうあります。

Requests to Pages functions count towards your quota for Workers plans

Pages Functions は Workers のプランの枠をそのまま使うという意味です。静的サイトにすれば CPU も D1 の読み取りも消えますが、ビルド時にコンテンツを読む経路を自前で持つことになり、その経路を持たずに済むことが CMS を入れた理由なので採りません。

払うと切られなくなるだけで、描画は速くならない

分かっているのは、月 5 ドルの Paid プランに上げれば上限が 30秒になり、この記事に書いた打ち切りは起きなくなることです。それでも描画は 1 ミリ秒も速くならず、いまと同じ CPU 時間を打ち切られずに使えるようになるだけです。払っていないのは、性能を金で買うのが嫌だからではありません。編集したらすぐ反映される CMS を選んだ対価がこの CPU 時間だという理解が正しいかを数字を重ねて確かめてから決めるため、いまはまだ上げていません。

参考リンク

この記事をシェア