EmDash が真っ白になるのは Free プランの CPU 上限
制作実績を 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日保持しています。CPU 対策ではなく、D1(EmDash のコンテンツを持つ Cloudflare のデータベース)の Free プランの読み取り行数の上限への対策で、経緯は Free プランの上限を守るために作らなかった仕組みにまとめています。キャッシュの寿命が CPU の障害まで運んでくるとは想定していませんでした。
死活監視は 200 が返るかしか見ておらず、本文が数キロバイトに縮んだことは検知しませんでした。実害はトップページの表示までで、他は正常でした。壊れたレスポンスがキャッシュに残る挙動は、公開した記事が 404 のままになる原因で見た構図と同じです。
10ms は速度の上限ではなく割り当ての名前で、公式にも稀な超過には猶予があると書かれている
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 を実行している時間だけです。
- ルーティングとテンプレートの描画
- D1 から返った行の変換
- JSON-LD の生成
D1 や外部リクエストへの応答待ちは数えられません。判定は1回の起動の合計で、待ちを挟んでも枠は回復しません。トップページの描画は実時間 440〜1,100ms に対し、CPU は 12〜57ms でした。
上限に触れるのはキャッシュを外れた描画だけです。キャッシュに当たっている間 Workers は起動しないため、同じ URL を MISS 1回・HIT 5回で叩いても起動ログは MISS の 1件だけです。危険度はアクセス数ではなく MISS の回数に比例し、次の場面で増えます。
- コンテンツを更新してキャッシュを失効させた直後
- 拠点ごとの初回アクセス
- クローラーによる巡回
件数が原因だと思って、先に直した
トップページは制作実績を全件(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 に収める桁ではありません。
テンプレートを直しても届かない場所にいる
EmDash の API ルートだけで認証確認に 3〜5ms、マニフェスト取得に 6〜8ms かかり、記事を組み立てる前に 10ms の枠の 3〜8割が消えます。同じ EmDash で動く 5 サイト(このサイトを含む)の CPU 中央値も 24.0〜36.0ms に揃っており、上限の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 を選んだ対価がここに来る、という理解が正しいかを、数字を重ねてから決めます。いまはまだ、上げていません。