静的サイトは、再公開の間も稼働し続けるようになりました
Mythexで静的サイトを再公開すると、以前は、新しいビルドをアップロードする前に公開中のファイルを削除していました。そのため、アップロードの間、一部の訪問者は404ページや、スクリプトが欠けたページを見ることになっていました。アップロードに失敗すると、サイトが空になるか、一部だけ新しい状態で残ることもありました。現在は、新しいビルドを古いものの上に書き込み、ページは最後にアップロードし、残ったファイルは数分後に削除します。さらに2つ目の修正として、再公開が1分ではなく数秒で反映されるようにしました。ロゴのようなファイルについては、最大で1日かかっていたものが、すぐに反映されるようになりました。
静的サイトの配信の仕組み
静的サイトとは、HTMLページ、JavaScript、CSS、画像といったファイルだけで構成されたサイトです。Mythexで静的サイトを公開すると、ビルドを行い、その出力をサイト用のフォルダーの下にあるオブジェクトストレージへアップロードします。そして、エッジネットワークを通して配信します。エッジネットワークとは、訪問者の近くにあるサーバーのことで、ストレージからファイルを取得し、しばらくの間コピーを保持します。(初めての方は、CDNとは何かのガイドで、この考え方を説明しています。)
問題は2つありました。再公開でファイルが変わる順序と、古いコピーをどれだけ保持するかです。
問題1:アップロードの前にサイトが空になっていた
再公開では、サイトのフォルダー内のすべてを削除してから、新しいビルドをアップロードしていました。アップロードにかかる時間の間、エッジにまだキャッシュされていないリクエストの訪問者は404を受け取るか、スクリプトが404を返すHTMLページを見ることになっていました。ローカルテストでは、250ファイルのサイトで12秒でした。
アップロードの失敗は、さらに深刻でした。フォルダーはすでに空になっており、2つあるアップロード経路のうち一方では、失敗したファイルより前に新しいindex.htmlが配置されてしまうことがありました。失敗と報告された公開が、一部だけ新しいサイトを公開したままになることもあったのです。
修正:上書きし、ページは最後に、削除は後で
- 先に削除しません。 新しいビルドは、古いものの上にアップロードされます。
- アセットはページより先に。 ページは必要なスクリプトやスタイルを指定するので、ページは最後にアップロードします。新しいページが配置されるまでは、訪問者は古いページを見ます。古いページは古いアセットを指定しますが、そのアセットはまだ残っています。新しいページが配置されるときには、そのページが指定する新しいアセットはすでに配置されています。
- 残ったファイルは後から削除します。 アップロードの前に、サイトを構成しているものの一覧を取得します。アップロードが成功してから3分後に、その一覧にあるもののうち、新しいビルドで書き換えられなかったファイルを削除します。3分は、ページがエッジやブラウザでキャッシュされうる60秒より長いので、古いページを手元に持っている訪問者も、そのページが指定するアセットを見つけられます。
- 迷ったら、何も削除しません。 アップロード前の一覧取得に失敗した場合は、何も削除しません。残ったファイルは害がなく、次の公開でクリアされます。何があったかを知らずに削除すると、サイトが空になってしまいます。
これで、アップロードに失敗しても、以前のサイトがそのまま残るようになりました。
本番と同じアップロードスクリプト、本番と同じアップロード経路、ストレージからの配信を使ってローカルでエンドツーエンドのテストを行い、エッジのキャッシュが冷えた状態で訪問者が到着するケースを再現しました。旧コードでは、再公開中に91回中55回の訪問が壊れていました。失敗した公開では、92回中61回でした。新しいコードでは、2つのアップロード経路、失敗したアップロード、その後の公開を含む約3,500回の訪問で、壊れた訪問は0でした。また、削除されるのはまさに残ったファイルだけでした。
問題2:再公開が反映されるまで時間がかかりすぎていた
アップロード自体がうまくいっていても、変更が反映されるのは遅いままでした。エッジはページのコピーを60秒、それ以外のすべてのファイルを1日保持しており、ブラウザにも同じ扱いを指示していました。logo.png、フォント、public/フォルダーからのファイルのように、名前が固定されたファイルは、再公開後も最大1日、古いバージョンを表示し続けることがありました。
修正:ビルドごとにファイルのコピーを分ける
エッジがAPIに対してサイトの場所を問い合わせると、その応答に、公開のたびに変わるビルドマーカーが含まれるようになりました。エッジは、そのマーカーの下にコピーを格納します。そのため、エッジの30秒の参照が切れた時点で、公開は完全に新しいキャッシュキーの集合になります。古いコピーは二度と見つからないため、エッジは1日分のコピーを保持しても、その公開を置き換えた後に古いものを配信することはありません。
ブラウザには2種類の異なるルールが適用されます。
- ハッシュ付きのビルド出力(
assets/index-CzLcxDO8.jsのように、内容が変わるとファイル名も変わるファイル)は、1年間キャッシュされ、immutableとしてマークされます。その名前では、古くなることはありえません。 - それ以外(ページや、ロゴのような固定名のファイル)は
no-cacheとしてマークされます。これは「使う前に確認する」という意味です。何も変わっていなければ、本文のない小さな304 Not Modifiedが返ります。ブラウザのIf-None-Matchチェックを尊重するようになったためです。
この2種類を区別するには注意が必要です。public/assets/からコピーされたファイルは、元の名前を保持します。そのためassets/logo.pngを1年間キャッシュすると、古いロゴを1年間固定してしまいます。そこで、ファイルがビルド出力フォルダーにあり、かつその名前に、ありふれた単語ではなくハッシュのように見える部分がある場合にのみ、ハッシュ付きとみなすようにしました。
ローカルでは、新しいページとロゴは、公開がライブになってから6秒後に見えるようになりました。以前は、ページが60秒、ロゴが1日でした。
途中で行った2つの小さな修正
- HEADリクエストによる空白ページ。
HEADリクエストは、本文を含まずにページのヘッダーだけを求めます。その空の応答をキャッシュしていたため、次の訪問者がそのページを開くと空白ページが表示されていました。HEADの応答はキャッシュしなくなりました。 - 「何もない」ことを示す本物のページ。 アプリのないアドレスには、以前は「This published app is offline or the link was not found」というプレーンテキストと502エラーコードが返されていました。今は「There's no app here」という適切なページを、404ステータスとともに返し、オーナーがMythexに戻るための導線も表示します。登録されていないカスタムドメインにも、同じページを返します。
得られた教訓
- 順序は正しさの一部です。 同じファイルでも、アップロードの順序が違えば、壊れたサイトと動くサイトの違いになります。
- 削除は遅く、また把握しているものだけに。 アップロード前に取得した一覧を使って削除するのは安全です。フォルダーを空にして、アップロードが完了することを祈るのは安全ではありません。
- 時間ではなくバージョンでキャッシュしてください。 有効期限は、内容がいつ変わるかについての推測にすぎません。ビルドとともに変わるキーは、事実です。
関連記事:公開したアプリは、再公開の間も稼働し続けます。公開についての詳細は、ドキュメントをご覧ください。