サーバー側の Blazor アプリのホストおよびデプロイ

Note

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、ASP.NET Core を使ってサーバー側の Blazor アプリ (Blazor Web App アプリと Blazor Server アプリ) をホストおよびデプロイする方法について説明します。

ホストの構成値

サーバー側の Blazor アプリでは、汎用ホスト構成値を受け入れることができます。

展開

サーバー側のホスティング モデルを使用する場合、Blazor はサーバー上で ASP.NET Core アプリ内から実行されます。 UI の更新、イベント処理、JavaScript の呼び出しは、SignalR 接続経由で処理されます。

ASP.NET Core アプリをホストできる Web サーバーが必要です。 Visual Studio には、サーバー側のアプリ プロジェクト テンプレートが含まれています。 Blazor プロジェクト テンプレートについて詳しくは、「ASP.NET Core Blazor プロジェクトの構造」をご覧ください。

リリース構成でアプリを発行し、bin/Release/{TARGET FRAMEWORK}/publish フォルダーの内容をデプロイします。この {TARGET FRAMEWORK} プレースホルダーはターゲット フレームワークです。

スケーラビリティ

1 台のサーバーのスケーラビリティ (スケールアップ) を検討する場合、アプリに使用できるメモリは、ユーザーの要求が増えたときにアプリによって使い果たされる最初のリソースになります。 サーバー上の使用可能なメモリは、以下の影響を受けます。

  • サーバーがサポートできるアクティブ回線の数。
  • クライアントでの UI の待機時間。

セキュリティで保護されたスケーラブルなサーバー側 Blazor アプリをビルドするためのガイダンスについては、次のリソースを参照してください。

各回線では、最小限の Hello World スタイルのアプリに約 250 KB のメモリが使用されます。 回線のサイズは、アプリのコードと各コンポーネントに関連付けられている状態の保守要件によって変わります。 アプリとインフラストラクチャの開発時にはリソースのニーズを測定することをお勧めしますが、展開ターゲットを計画する際に、次のベースラインを出発点にすることができます。アプリで 5,000 人の同時ユーザーをサポートすることを想定している場合は、アプリに対して少なくとも 1.3 GB のサーバー メモリ (またはユーザーあたり最大 273 KB) の予算を割り当てること検討してください。

SignalR 構成

SignalR のホスティングとスケーリングの条件は、SignalR を使用する Blazor アプリに適用されます。

Blazor アプリでの SignalR の構成ガイダンスなどの詳細については、「ASP.NET Core BlazorSignalR ガイダンス」を参照してください。

トランスポート

Blazor は、待ち時間の短縮、信頼性の向上、およびセキュリティの強化のために、WebSocket を SignalR トランスポートとして使用すれば最も優れた性能を発揮します。 WebSocket が使用できない場合や、ロング ポーリングを使用するようにアプリが明示的に構成されている場合は、SignalR によってロング ポーリングが使用されます。

ロング ポーリングが利用されている場合、コンソール警告が表示されます。

ロング ポーリング フォールバック トランスポートを使用して WebSocket 経由で接続できませんでした。 接続をブロックしている VPN またはプロキシが原因である可能性があります。

グローバル展開と接続エラー

地理的データ センターへのグローバル デプロイに関する推奨事項は次のとおりです。

  • ユーザーの大部分が存在しているリージョンにアプリをデプロイします。
  • 大陸間のトラフィックの待機時間の増加を考慮します。 再接続 UI の表示を制御するには、「ASP.NET Core BlazorSignalR ガイダンス」を参照してください。
  • Azure SignalR Service の使用を検討してください。

Azure App Service

Azure App Service 上でホストするには、WebSocket の他に、アプリケーション要求ルーティング処理 (ARR) アフィニティとも呼ばれる セッション アフィニティの構成が必要です。

Note

Azure App Service の Blazor アプリでは、Azure SignalR Service は必要ありません。

Azure App Service でのアプリの登録のために、次を有効にします。

  • WebSocket: WebSocket トランスポートを機能させることができます。 既定の設定は [オフ] です。
  • セッション アフィニティ: ユーザーからの要求を同じ App Service インスタンスにルーティングします。 既定の設定は [オン] です。
  1. Azure portal の [App Services] にある Web アプリに移動します。
  2. [設定]>[構成] を開きます。
  3. [Web ソケット][オン] に設定します。
  4. [セッション アフィニティ][オン] に設定されていることを確認します。

Azure SignalR Service

オプションの Azure SignalR Service はアプリの SignalR ハブと連携して、サーバー側のアプリを多数の同時接続にスケールアップします。 さらに、 サービスのグローバル リーチとハイパフォーマンスのデータ センターは、地理的条件による待機時間の短縮に役立ちます。

このサービスは、Azure App Service または Azure Container Apps でホストされている Blazor アプリには必要ありませんが、他のホスティング環境で役立つ場合があります。

  • 接続のスケールアウトを容易にするには。
  • グローバル分散を実行します。

Note

ステートフル再接続 (WithStatefulReconnect) は .NET 8 でリリースされましたが、Azure SignalR サービスでは現在サポートされていません。 詳細については、「ステートフル再接続のサポート (Azure/azure-signalr #1878)」を参照してください。

アプリが WebSocket ではなく、ロング ポーリングを使用する場合やロング ポーリングにフォールバックする場合は、最大ポーリング間隔 (MaxPollIntervalInSeconds、デフォルト: 5 秒、制限: 1 秒から 300 秒) の構成が必要になる可能性があります。これは、Azure SignalR Service のロング ポーリング接続で許可される最大ポーリング間隔を定義します。 次のポーリング要求が最大ポーリング間隔内に来ない場合、サービスはクライアント接続を閉じます。

運用デプロイに依存関係としてサービスを追加する方法のガイダンスについては、「Azure App Service に ASP.NET Core SignalR アプリを発行する」をご覧ください。

詳細については、以下を参照してください:

Azure Container Apps

Azure Container Apps サービスでのサーバー側 Blazor アプリのスケーリングの詳細については、「Azure での ASP.NET Core アプリのスケーリング」を参照してください。 このチュートリアルでは、Azure Container Apps でアプリをホストするために必要なサービスを作成して統合する方法について説明しています。 基本的な手順についてはこのセクションでも説明します。

  1. Azure Container Apps のセッション アフィニティ (Azure ドキュメント)」内のガイダンスに従って、Azure Container Apps サービスのセッション アフィニティを構成します。

  2. すべてのコンテナー インスタンスがアクセスできる一元的な場所にキーを保持するように ASP.NET Core データ保護 (DP) サービスを構成する必要があります。 キーは Azure Blob Storage に格納し、Azure Key Vault を使って保護できます。 データ保護サービスでは、そのキーを使用して Razor コンポーネントを逆シリアル化します。 Azure Blob Storage と Azure Key Vault を使用するように DP サービスを構成するには、次の NuGet パッケージをご覧ください。

    Note

    .NET アプリへのパッケージの追加に関するガイダンスについては、「パッケージ利用のワークフロー」 (NuGet ドキュメント) の "パッケージのインストールと管理" に関する記事を参照してください。 NuGet.org で正しいパッケージ バージョンを確認します。

  3. 次の強調表示されているコードを使用して、Program.cs を更新します。

    using Azure.Identity;
    using Microsoft.AspNetCore.DataProtection;
    using Microsoft.Extensions.Azure;
    
    var builder = WebApplication.CreateBuilder(args);
    var BlobStorageUri = builder.Configuration["AzureURIs:BlobStorage"];
    var KeyVaultURI = builder.Configuration["AzureURIs:KeyVault"];
    
    builder.Services.AddRazorPages();
    builder.Services.AddHttpClient();
    builder.Services.AddServerSideBlazor();
    
    builder.Services.AddAzureClientsCore();
    
    builder.Services.AddDataProtection()
                    .PersistKeysToAzureBlobStorage(new Uri(BlobStorageUri),
                                                    new DefaultAzureCredential())
                    .ProtectKeysWithAzureKeyVault(new Uri(KeyVaultURI),
                                                    new DefaultAzureCredential());
    var app = builder.Build();
    
    if (!app.Environment.IsDevelopment())
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }
    
    app.UseHttpsRedirection();
    app.UseStaticFiles();
    
    app.UseRouting();
    
    app.UseAuthorization();
    
    app.MapRazorPages();
    
    app.Run();
    

    上記の変更により、アプリでは一元化されたスケーラブルなアーキテクチャを使用して DP サービスを管理できるようになります。 DefaultAzureCredential は、コードが Azure にデプロイされた後にコンテナー アプリのマネージド identity を検出し、それを使用して Blob Storageとアプリのキー コンテナーに接続します。

  4. コンテナー アプリのマネージド identity を作成し、Blob Strage とキー コンテナーへのアクセス権を付与するには、次の手順を実行します。

    1. Azure portal で、コンテナー アプリの概要ページに移動します。
    2. 左側のナビゲーションから [サービス コネクタ] を選択します
    3. 上部のナビゲーションから [+ 作成] を選択します。
    4. [接続の作成] ポップアップ メニューで、次の値を入力します。
      • [コンテナー]: アプリをホストするために作成したコンテナー アプリを選択します。
      • [サービスの種類]: [Blob Storage] を選択します。
      • [サブスクリプション]: コンテナー アプリを所有するサブスクリプションを選択します。
      • [接続名]: scalablerazorstorage の名前を入力します。
      • [クライアントの種類]: [.NET] を選択し、[次へ] を選択します。
    5. [システム割り当てマネージド identity] を選択し、[次へ] を選択します。
    6. 既定のネットワーク設定を使用し、[次へ] を選択します。
    7. Azure によって設定が検証された後、[作成] を選択します。

    キー コンテナーに対して上記の設定を繰り返します。 [基本] タブで、適切なキー コンテナー サービスとキーを選択します。

IIS

IIS を使用する場合は、次を有効にします。

詳細については、「IIS に ASP.NET Core アプリを発行する」のガイダンスは外部 IIS リソースのクロスリンクを参照してください。

Kubernetes

次のセッション アフィニティに対する Kubernetes を使用して、イングレス定義を作成します。

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: <ingress-name>
  annotations:
    nginx.ingress.kubernetes.io/affinity: "cookie"
    nginx.ingress.kubernetes.io/session-cookie-name: "affinity"
    nginx.ingress.kubernetes.io/session-cookie-expires: "14400"
    nginx.ingress.kubernetes.io/session-cookie-max-age: "14400"

Nginx を使用した Linux

ASP.NET Core SignalR アプリ」のガイダンスに従って、次のように変更します。

  • location のパスを /hubroute (location /hubroute { ... }) からルート パス / (location / { ... }) に変更します。
  • プロキシ バッファーリング (proxy_buffering off;) の構成は削除します。この設定は、Blazor アプリのクライアントとサーバー間の通信に関連しないサーバー送信イベント (SSE) にのみ適用されるためです。

詳細および構成のガイダンスについては、次のリソースを参照してください。

Apache を使用した Linux

Linux 上の Apache の背後に Blazor アプリをホストするには、HTTP および Websocket トラフィック用に ProxyPass を構成します。

次に例を示します。

  • Kestrel サーバーは、ホスト コンピューター上で実行されています。
  • このアプリは、ポート 5000 でトラフィックをリッスンします。
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

次のモジュールを有効にします。

a2enmod   proxy
a2enmod   proxy_wstunnel

WebSockets エラーをブラウザ コンソールで確認します。 エラーの例:

  • Firefox からサーバー (ws://the-domain-name.tld/_blazor?id=XXX) への接続を確立できません。
  • エラー :'WebSockets' の転送の開始に失敗しました。エラー :転送のエラーがありました。
  • エラー :'LongPolling' の転送の開始に失敗しました。TypeError: this.transport は定義されていません
  • エラー :利用可能ないかなる転送を使用しても、サーバーに接続できませんでした。 WebSockets が失敗しました
  • エラー :接続が 'Connected' 状態ではない場合、データは送信できません。

詳細および構成のガイダンスについては、次のリソースを参照してください。

ネットワーク待機時間の測定

次の例で示すように、JS 相互運用を使用してネットワーク待機時間を測定できます。

MeasureLatency.razor:

@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

<h2>Measure Latency</h2>

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}
@inject IJSRuntime JS

@if (latency is null)
{
    <span>Calculating...</span>
}
else
{
    <span>@(latency.Value.TotalMilliseconds)ms</span>
}

@code {
    private DateTime startTime;
    private TimeSpan? latency;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            startTime = DateTime.UtcNow;
            var _ = await JS.InvokeAsync<string>("toString");
            latency = DateTime.UtcNow - startTime;
            StateHasChanged();
        }
    }
}

妥当な UI エクスペリエンスには、UI の待機時間を 250 ミリ秒以下にすることをお勧めします。

メモリ管理

サーバーでは、ユーザー セッションごとに新しい回線が作成されます。 各ユーザー セッションは、ブラウザーでの 1 つのドキュメントのレンダリングに対応します。 たとえば、複数のタブで複数のセッションが作成されます。

Blazor は、セッションを開始した "回線" と呼ばれるブラウザーへの一定の接続を維持します。 ユーザーがネットワーク接続を失ったり、ブラウザーを突然閉じたりした場合など、いくつかの理由により、いつでも接続が失われる可能性があります。 接続が失われた場合、Blazor には、"切断された" プールに限られた数の回線を配置する復旧メカニズムがあり、クライアントには再接続してセッションを再確立するための制限時間が与えられます (既定値: 3 分)。

その後、Blazor は回線を解放し、セッションを破棄します。 その時点から、回線はガベージ コレクション (GC) の対象となり、回線の GC 生成のコレクションがトリガーされたときに要求されます。 理解すべき重要な側面の 1 つは、回線の有効期間が長いことです。つまり、回線によってルート指定されたオブジェクトのほとんどが、最終的に Gen 2 に到達します。 その結果、Gen 2 コレクションが発生するまで、これらのオブジェクトが解放されない場合があります。

一般的なメモリ使用量を測定する

前提条件:

  • アプリはリリース構成で発行する必要があります。 生成されたコードは運用環境のデプロイに使用されるコードを表していないので、デバッグ構成の測定値は関係ありません。
  • デバッガーをアタッチせずにアプリを実行する必要があります。これが、アプリの動作に影響を与え、結果を損なう可能性があるためです。 Visual Studio で、メニュー バーから [デバッグ]>[デバッグなしで開始] を選択するか、キーボードを使用して Ctrl+F5 キーを押して、デバッグなしでアプリを起動します。
  • .NET で実際に使用されるメモリの量を理解するには、さまざまな種類のメモリを検討してください。 一般に、開発者は Windows OS 上のタスク マネージャーでアプリのメモリ使用量を検査します。これで、通常は、使用中の実際のメモリの上限がわかります。 詳細については、次の記事を参照してください。

Blazor に適用されるメモリ使用量

blazor によって使用されるメモリは次のように計算します。

(アクティブな回線 × 回線ごとのメモリ) + (切断された回線 × 回線ごとのメモリ)

回線で使用されるメモリの量と、アプリが維持できる潜在的な最大アクティブ回線数は、アプリの記述方法によって大きく異なります。 使用可能な最大アクティブ回線数は、次の方法で大まかにわかります。

使用可能な最大メモリ数 / 回線ごとのメモリ = 潜在的な最大アクティブ回線数

Blazor でメモリ リークが発生するには、次の条件を満たす必要があります。

  • メモリが、アプリではなくフレームワークによって割り当てられる必要がある。 アプリで 1 GB の配列を割り当てる場合、アプリが配列の破棄を管理する必要があります。
  • メモリがアクティブに使用されていない。つまり、回線がアクティブではなく、切断された回線のキャッシュから削除されている。 最大アクティブ回線数が実行されている場合、メモリ不足は、メモリ リークではなく、スケールの問題です。
  • 回線の GC 生成用のガベージ コレクション (GC) が実行されたが、フレームワーク内の別のオブジェクトが回線への強い参照を保持しているため、ガベージ コレクターが回線を要求できない。

それ以外の場合は、メモリ リークはありません。 回線がアクティブ (接続でも切断でも) の場合、回線はまだ使用中です。

回線の GC 生成のコレクションが実行されない場合、ガベージ コレクターがその時点でメモリを解放する必要がないため、メモリは解放されません。

GC 生成のコレクションが実行され、回線が解放された場合は、.NET が仮想メモリをアクティブな状態に維持することを決定する可能性があるため、プロセスではなく GC 統計に対してメモリを検証する必要があります。

メモリが解放されていない場合は、アクティブまたは切断された状態でなく、フレームワーク内の別のオブジェクトによってルート指定されている回線が見つかるはずです。 それ以外の場合、メモリを解放できないのは、開発者コードのアプリの問題です。

メモリ使用量を削減する

アプリのメモリ使用量を削減するには、次のいずれかの方法を採用してください。

  • .NET プロセスで使用されるメモリの総量を制限する。 詳細については、「ガベージ コレクションの実行時構成オプション」を参照してください。
  • 切断された回線の数を減らす。
  • 回線が切断状態になるまでの時間を短縮する。
  • ダウンタイム期間中にガベージ コレクションを手動でトリガーしてコレクションを実行する。
  • サーバー モードではなく、ガベージ コレクションを積極的にトリガーするワークステーション モードでガベージ コレクションを構成する。

一部のモバイル デバイスのブラウザーのヒープ サイズ

クライアント上で実行され、モバイル デバイスのブラウザー (特に iOS 上の Safari) を対象とする Blazor アプリをビルドする際には、MSBuild プロパティ EmccMaximumHeapSize を使用してアプリの最大メモリを減らすことが求められる場合があります。 詳しくは、「ASP.NET Core Blazor WebAssembly のホストと展開」をご覧ください。

その他のアクションと考慮事項

  • メモリ要求が高い場合はプロセスのメモリ ダンプをキャプチャし、最も多くのメモリを使用しているオブジェクトと、それらのオブジェクトがルート指定されている場所 (それらに対する参照を保持するもの) を特定します。
  • dotnet-counters を使って、アプリでのメモリの動作状況に関する統計情報を調べることができます。 詳しくは、「パフォーマンス カウンターを調べる (dotnet-counters)」をご覧ください。.
  • GC がトリガーされた場合でも、.NET は、近いうちにメモリを再利用する可能性があるため、メモリをすぐに OS に返さずに保持し続けます。 これにより、メモリのコミットとデコミットを絶えず行う必要はなくなりますが、コストがかかります。 dotnet-counters を使っている場合、GC が発生し、使用済みメモリの量が 0 (ゼロ) に低下したのに、ワーキング セット カウンターが減らないのは、このことを反映しており、これは .NET がメモリを再利用のために保持していることを示します。 この動作を制御するためのプロジェクト ファイル (.csproj) 設定の詳細については、「ガベージ コレクションの実行時構成オプション」を参照してください。
  • サーバー GC は、アプリのフリーズを回避するためにそれが絶対に必要であると判断し、ユーザーのアプリだけがマシン上で実行していて、システム内のすべてのメモリを使用できると見なすまで、ガベージ コレクションをトリガーしません。 たとえば、システムに 50 GB がある場合、ガベージ コレクターは、Gen 2 コレクションをトリガーする前に、使用可能なメモリの 50 GB をすべて使用しようとします。
  • 切断された回線の保持構成について詳しくは、「ASP.NET Core BlazorSignalR ガイダンス」を参照してください。

メモリの測定

  • リリース構成でアプリを発行します。
  • アプリの発行済みのバージョンを実行します。
  • 実行中のアプリにデバッガーをアタッチしないでください。
  • Gen 2 を強制的にトリガーしてコレクションを圧縮すると (GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true))、メモリを解放しますか?
  • アプリが大きなオブジェクト ヒープにオブジェクトを割り当てているかどうかを検討します。
  • アプリが要求と処理でウォームアップされた後、メモリの増加をテストしていますか? 通常、コードの初回実行時に設定されて、アプリの占有領域に一定量のメモリを追加するキャッシュがあります。