Azure Web Apps + ASP.NET での初回ページ表示遅延対策

Azure Web Apps + ASP.NET の組み合わせでシステムを構築した場合、ページの初回表示が非常に遅い現象に見舞われる事がある。 どれぐらい遅いかと言うと、大体 10 ~ 30 秒ぐらい待つ感じ。 主な原因は下記 2 つ。

  • Azure Web Apps の「常時接続」設定がオフになっている
  • ASP.NET のページのプリコンパイルを行っていない

前者の常時接続設定についてはそれ自体も遅延の原因にはなっているが、どちらかと言うと問題のメインは後者のプリコンパイルで、これに常時接続の問題が加わると状況が更に悪化する感じになる。

なお、Web Apps を Azure App Service プランの Basic 以上で作成した場合は既定で常時接続オンになっているはずなので、Free プランで作った環境でなければ常時接続は最初から問題にはなっていない可能性が高い。 以降では、ASP.NETMVC 5 前提とするが、恐らく ASP.NET であれば基本考え方は同じはず。

解決法

Azure Web Apps の「常時接続」設定がオフになっている

Azure ポータルで当該 Web Apps を開き、[構成] - [全般設定] - [常時接続] の設定を「オン」にする。プランが Basic 以上でないとオンを選べないので、注意。

ASP.NET のページのプリコンパイルを行っていない

この問題の解決方法は、デプロイ時に ASP.NET のプリコンパイルを行った状態でデプロイすることだが、デプロイ方法に依存する。

  • Visual Studio の発行を使用してデプロイ

    • 「発行」時に作成するプロファイル設定で、「ファイル発行オプション」の「発行中にプリコンパイルする」にチェックを入れる。但しこれだけだと Global.asax しかプリコンパイルされないため、次の作業も行う

      f:id:poke_dev:20190806225404p:plain

    • 上記項目右にある「設定」リンクをクリックして、「プリコンパイル済みサイトを更新可能にする」のチェックを外す。これを外さないと、各ページのプリコンパイルが行われない

      f:id:poke_dev:20191023205312p:plain

  • Azure ポータルの「デプロイ センター」で、App Service Kudu エンジンを使用してデプロイ

  • Azure DevOps Pipeline を使用してデプロイ (本命)

    • Build の「MSBuild Arguments」に、/p:PrecompileBeforePublish=true を追加する

      f:id:poke_dev:20190806230743p:plain

根本的な要因

根本的な要因は、ASP.NET のページ表示時の仕様に起因している。 ASP.NET では各ページの初回表示時にコンパイルを行い、以降の表示時はコンパイル済みのキャッシュを利用することで、表示時間を短縮している。 この、

  • 各ページの初回表示時にコンパイルを行う
  • 2 回目以降の表示ではコンパイル済みのキャッシュを利用してページ表示を高速化する

の動作は、もともとオンプレミスの ASP.NET でも問題とはなっていたが、Azure Web Apps の仕組みと相性が悪く、Azure Web Apps では発生しやすい状況となっている。 具体的には、Azure Web Apps ではワーカープロセスが様々なタイミングで再起動されるが、そのタイミングでキャッシュファイルが全てなくなるため、ワーカープロセス再起動後は常に「初回表示」状態となり、本遅延問題が発生する。

ワーカープロセス再起動に関しては以下も参考

ASP.NET のページ初回表示時のキャッシュのされ方

キャッシュは、D:\local\Temporary ASP.NET Files 配下に作成される。 D:\local は Azure App Service の一時ストレージで、ワーカープロセス再起動時はここに保存されているファイルは全て消える、ので、ページ表示時に作成されたキャッシュも消える。

なお、App Service のストレージの状況は、ツールでは KUDU でしか見ることができないが、KUDU (= SCM) はそれ自体のワーカープロセス及び一時ストレージを持っており、KUDU で確認できる一時ストレージは KUDU 用の一時ストレージとなる。 つまり、ASP.NET のページ表示時に作成されたキャッシュファイルは、ツールでは通常確認することができない。 この挙動については、アプリ設定で「WEBSITE_DISABLE_SCM_SEPARATION」を追加し、値を「true」に設定することで、KUDU サイトをアプリのワーカープロセス内で実行するように変更できる。 こうすることで、ASP.NET のページ表示時に作成されるキャッシュファイルも、KUDU から確認することができるようになる。

WEBSITE_DISABLE_SCM_SEPARATION アプリ設定は正式にはサポートされていないので、挙動の確認以外では使わないこと

実際のキャッシュのされ方を確認 (常時接続はオン)

プリコンパイルなし

wwwroot/bin 直下の状態。プリコンパイルされたファイルなし。

f:id:poke_dev:20190807234126p:plain

ページの初回表示と、2 回目の表示にかかった時間。初回表示が 15.7 秒、2 回目の表示は 0.2 秒。

f:id:poke_dev:20190807234212p:plain

ページの初回表示後、キャッシュファイルが一時ストレージに作成される。2 回目以降の表示ではキャッシュファイルを使用するため表示が高速化されるが、ワーカープロセス再起動時にファイルはなくなる。

f:id:poke_dev:20190807234355p:plain

プリコンパイルあり (DevOps Pipeline MSBuild での設定)

wwwroot/bin 直下の状態。全ページのプリコンパイル済みキャッシュが、bin に配置されている。ワーカープロセス再起動で消えることはない。

f:id:poke_dev:20190807234527p:plain

ページの初回表示と、2 回目の表示にかかった時間。初回表示は 0.35 秒。2 回目の表示は 0.01 秒。

f:id:poke_dev:20190807234746p:plain

一時ストレージにキャッシュファイルは作成されない。ワーカープロセス再起動で一時フォルダのファイルが消えても、ページの表示速度には影響を及ぼさない。

f:id:poke_dev:20190807234807p:plain

補足

上述したように、Azure ポータルの「デプロイ センター」で自動デプロイ機能を利用する場合、プリコンパイルオプションがないことと、リポジトリにプッシュされる度に自動デプロイが走り、結果ワーカープロセスが再起動する流れになるため、ページ表示速度の面では Azure ポータルでの自動デプロイ機能と ASP.NET アプリはあまり相性が良くない。

開発速度に影響が出そうな場合は、Azure ポータルほどお手軽ではないが DevOps Pipeline を使った方が良いかもしれない。

参考情報