ブログ一覧

Cloudflare D1 の無料枠を守るためだけに作らなかった 4 つの仕組み

更新: Web開発者向け

Cloudflare の 1 アカウントで 8 サイトを運用していて、D1 の読み取りが 1 日 2,110 万行(無料枠 500 万行の 422%)に達したことがあります(Cloudflare D1 の読み取りが 1 日 2,100 万行になった原因)。テンプレートとキャッシュの見直しで 190 万行前後(38〜42%)まで下げ、その水準で落ち着いています。

下げる過程で「次はこれを作れば、さらに減る」という仕組みの案が 4 つ出ましたが、4 つとも作らないことにしました。どれも半日で作れて、効果も数字で見えていた案です。作らなかった理由は 4 つに共通していて、入れる費用は安くても、持ち続ける費用が高いからです。その 4 つと判断の材料を書きます。

1. 公開時にキャッシュ済みの 404 を消す仕組み

何が起きていたか。 公開前に記事の URL を一度でも開くと、404 のレスポンスがエッジに 30 日キャッシュされ、記事を公開しても素の URL が 404 のまま残ります。公開時に失効させるキャッシュタグ(コレクション名と記事 ID)が、404 には付いていないためです。経緯は Astro + Cloudflare で公開した記事が 404 のままになる原因 に書きました。

作ろうとしたもの。 記事の公開 API への書き込みを捕まえて、レスポンスの slug から公開 URL のパスを組み立て、そのパスのタグだけを失効させるミドルウェア。テストを含めて実装し、CI も通り、PR まで出しました。

持ち続けるもの。

  • slug から URL を組み立てる規則。CMS が持っている url_pattern は実際のルーティングと食い違っているサイトがあり、結局コード側に規則を持つことになる。サイトを足すたび、URL の設計を変えるたびに見直しが要る
  • 予約公開は Cron から実行されるのでミドルウェアを通らず、この仕組みでは捕まえられない。予約公開の 404 は残るのに、対処済みに見える
  • purge のコードそのもの。EmDash のキャッシュの仕組みが変わるたびに追従が要る

守っているものの価値。 404 を 30 日キャッシュし続けることで節約できるのは、同じ存在しない URL が繰り返し踏まれたときの Worker 起動だけです。スキャナは毎回違う URL を試すのでキャッシュに当たらず、404 の描画は軽いので、キャッシュしても D1 の読み取り量は減っていませんでした。

代わりにしたこと。 404 ページで Astro.cache.set({ maxAge: 300, swr: 0 }) を呼び、404 のキャッシュだけ 5 分にする 1 行。公開・予約公開・復元・スラッグ変更のどの経路で起きても、最長 5 分で直り、保守するコードもありません。

2. 404 ページを D1 を読まない静的なページにする

何が起きていたか。 存在しない URL への 404 も、記事ページと同じ Base レイアウトで描画しています。ヘッダーのメニュー、サイト設定、フッターの固定ページ一覧を D1 から引くので、404 を 1 回返すのに数十行の読み取りが乗ります。スキャナは 1 日 1,000 件前後この 404 を踏みます。

作ろうとしたもの。 404 ページだけ Base を通さず、メニューもリンクも固定で書いた静的なページにする。読み取りは 0 行になり、1 の 404 キャッシュを消す仕組みも要らなくなる(キャッシュしなくても安いので)。

持ち続けるもの。

  • レイアウトが 2 系統になる。メニューやフッターを変えるたびに、404 側にも手で反映する。忘れると 404 ページだけ古いメニューやフッターのまま残る
  • 8 サイトぶん。サイトごとに Base の中身が違うので、静的版もサイトごとに書く

守っているものの価値。 1 日 1,000 件 × 数十行で、多くても 5 万行。日次の枠の 1% です。8 月に問題だったのは通常ページの集計処理で(キャッシュを 30 日にしても D1 の読み取りが減らない原因)、404 の読み取りは最初から主因ではありませんでした。

代わりにしたこと。 ウィジェット(タクソノミーの件数集計)だけは 404 で描画しないようにしてあります。これは 1 行の条件分岐で、レイアウトを分けずに済む範囲です。レイアウトを分けるかは、404 の件数が桁で増えたときに考えます。

3. リダイレクト表を Cloudflare の Bulk Redirects へ同期する仕組み

何が起きていたか。 EmDash のリダイレクトミドルウェアは、有効なリダイレクトを D1 から全件読んでから照合します。Worker の isolate が温まっていればメモリのコピーを使いますが、冷えた isolate では毎回読みます。8 サイトのうち WordPress から移行したサイトの 1 つは、リダイレクト行が多く、この select * from _emdash_redirects where enabled = ? が 1 回 350 行、そのサイトの読み取りの 55%、アカウント全体の約 18% を占めていました。

作ろうとしたもの。 D1 のリダイレクト行を Cloudflare の Bulk Redirects へ同期し、同期できた行を D1 側で無効にする仕組み。Bulk Redirects は Worker より前で処理されるので、旧 URL へのアクセスは Worker を起動しなくなり、読み取りも起動枠も両方減ります。コードの変更も patch も要りません。

持ち続けるもの。

  • 同期そのもの。サイトが増えたとき、スラッグ変更で CMS が自動生成するリダイレクトが増えたとき、同じ手順を回し続ける。一度の移行で終わらず、運用として続く
  • 2 つの場所にある同じ情報の整合。D1 側を無効化した行と Bulk 側の行がずれたとき、どちらが正しいかを判断する手順
  • Bulk Redirects 側の無料プランの上限(リストの件数)に収まるかの管理

守っているものの価値。 アカウント全体の 18% は小さくありません。ただし D1 の日次は 38〜42% で、枠の半分も使っていません。18% を消しても「40% が 33% になる」だけで、その差で新しくできるようになることはありません。

代わりにしたこと。 同期の仕組みは作らず、再訪の条件を数字で決めました。D1 の日次が 50% に近づいたら、このときの設計メモから着手し、それまでは同期を持ちません。

4. スキャナのアクセスをエッジで落とすルール

何が起きていたか。 WordPress 時代の URL(/wp-login.php、/wp-json/…、/xmlrpc.php)を狙うスキャナが、8 サイトのうちの 1 つだけで週 7,410 件来ていました。全部 404 ですが、Worker は起動します。

作ろうとしたもの。 Cloudflare の WAF カスタムルールで、スキャナのリクエストを Worker に届く前に遮断する。パスの列挙(ブロックリスト)はやらず、「正規の URL にはドットが出てこない」という性質を使った構造的な規則 1 本にする案でした。8 サイトの記事スラッグ・タクソノミー・リダイレクト元にドットを含むものがゼロで、静的ファイルの拡張子が 6 種類だけであることまで実測して、規則の形は決めていました。

持ち続けるもの。

  • 例外の管理。静的ファイルを 1 種類足すたび、ドットを含む正規の URL を 1 つ作るたびに、ルールを見直す。忘れると公開中のページへのアクセスを自分のルールで遮断する
  • 無料プランの WAF は正規表現が使えず、contains / starts_with / ends_with の組み合わせだけ。書ける条件が限られたまま例外を増やすと、どのリクエストを誤って遮断するかを予測できなくなる
  • 誤ブロックの切り分け。ルールに当たったリクエストは Worker に届かないので、アプリ側のログには何も残らない

守っているものの価値。 Worker の起動は日平均 34,000 回で、無料枠 10 万回の 34%。スキャナの分を全部消しても 30% を切る程度です。しかも以前アプリ側に入れた同種のブロック(.php と /wp-json を素の 404 で返すミドルウェア)は、狙っていた 503 を抑えられていませんでした。503 の原因はスキャナではなく通常ページの集計処理で、その処理を直したら 503 は消えました(Cloudflare Workers 無料プランで 503 が出る原因は CPU 上限)。原因でない物を止めるブロックだけが残った前例が、自分の手元にありました。

同じ目的で Bot Fight Mode も検討し、こちらは別の理由(無料プランでは例外が作れない)で見送っています。Bot Fight Mode を無料プランで使うと GitHub Actions が 403 になる に書きました。

代わりにしたこと。 ルールは入れず、再訪の条件を Worker の起動が 1 日 7 万回に近づいたときにしました。そのときも最初に取る手はルールではなく Workers Paid(月 5 ドルで 1,000 万回/月)で、副作用が無く確実に起動の上限が上がります。

4 つに共通していた判断の形

作らなかった 4 つを並べると、判断の材料は毎回同じ 3 つの問いでした。

  • 何を守るための仕組みか。 404 のキャッシュ、404 の描画コスト、D1 の 18%、Worker の起動枠
  • それは今、圧迫されているか。 D1 は 40%、Worker は 34%。どれも圧迫されていない
  • 持ち続けるものは何か。 URL の規則、同期の運用、例外の管理。どれも作った日の後にずっと発生し続ける

守る対象が圧迫されていないなら、仕組みを増やす理由がありません。圧迫されたときに取る手も、4 つとも課金の方が確実で副作用が無いという結論でした。1 人で 8 サイトを持っている構成では、仕組みの数がそのまま将来の自分の作業量になります。

作らない判断にも記録が要るので、数字が動いたときに同じ調査をやり直さずに済むよう、4 つとも Issue に「やらない理由」と「再訪する条件」を書いて閉じました。

仕組みを作るかは枠に対する今の比率で決める

  • 「作れば減る」ことと「減らす必要がある」ことは別で、減らす必要があるかは枠に対する今の比率で決める
  • 仕組みの費用は作った日ではなく、その後の見直しと切り分けにかかり、サイトや静的ファイルが増えるたび、CMS を上げるたびに払う
  • 圧迫されたときの第一手は課金。月 5 ドルで確実に効く手があるなら、それより保守の重い仕組みを先に入れない
  • 作らない判断は、再訪の条件(数字)と一緒に記録する。条件を書かずに閉じると、次に同じ調査をやり直す

この記事をシェア