共用方式為


託管和部署伺服器端 Blazor 應用程式

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .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 應用程式的網路伺服器。 Visual Studio 包含伺服器端應用程式專案範本。 如需 Blazor 專案範本的詳細資訊,請參閱 ASP.NET Core Blazor 專案結構

在發行組態中發佈應用程式並部署 bin/Release/{TARGET FRAMEWORK}/publish 資料夾的內容,其中 {TARGET FRAMEWORK} 預留位置是目標架構。

延展性

在考慮單一伺服器的可擴縮性 (擴大) 時,隨著使用者需求的增加,應用程式可用的記憶體可能是應用程式耗盡的第一個資源。 伺服器上的可用記憶體會影響:

  • 伺服器可支援的作用中線路數目。
  • 用戶端上的 UI 延遲。

如需建置安全且可調整伺服器端 Blazor 應用程式的指導,請參閱下列資源:

對於最小的 Hello World 樣式的應用程式,每個線路使用大約 250 KB 的記憶體。 線路的大小取決於應用程式的程式碼,以及與每個元件相關聯的狀態維護需求。 建議您在應用程式和基礎結構的開發期間測量資源的需求量,但以下基準可以作為規劃部署目標的起點:如果您預期應用程式支援 5,000 位並行使用者,請考慮為應用程式預定至少 1.3 GB 的伺服器記憶體 (或每位使用者約 273 KB)。

SignalR 設定

SignalR 的裝載和縮放情況適用於使用 SignalR 的 Blazor 應用程式。

如需 Blazor 應用程式中 SignalR 的詳細資訊,包括設定指導,請參閱 ASP.NET Core BlazorSignalR 指導

傳輸

使用 WebSocket 作為 Blazor 傳輸時,因為延遲會較低、可靠性會更好且安全性會改善,因此 SignalR 會有最佳的運作情況。 當 WebSocket 無法使用或當應用程式明確設定為使用長輪詢時,SignalR 就會使用長輪詢

如果使用長輪詢,就會出現主控台警告:

無法使用長輪詢後援傳輸來透過 WebSockets 連線。 這可能是因為 VPN 或 Proxy 封鎖該連線。

全域部署和連線失敗

全域部署至地理資料中心的建議:

Azure App Service

在 Azure App Service 上託管需要設定 WebSockets 和工作階段親和性,也稱為應用程式要求路由 (ARR) 親和性。

注意

Azure App Service 上的 Blazor 應用程式不需要 Azure SignalR Service

在 Azure App Service 中為應用程式註冊啟用下列項目:

  • WebSockets 以允許 WebSockets 傳輸運作。 預設設定是 Off
  • 會話親和性可將使用者的要求路由回相同的 App 服務實體。 預設設定是 On
  1. 在 Azure 入口網站中,瀏覽至 [應用程式服務] 中的 Web 應用程式。
  2. 開啟 設定>設定
  3. 將 [Web 通訊端] 設定為 On
  4. 確認 Session affinity 已設定為 On

Azure SignalR Service

可選的 Azure SignalR Service 與應用程式的 SignalR 集線器配合使用,可將伺服器端應用程式擴充至大量的同時連線。 此外,服務的全球觸達和高效能資料中心可大幅協助降低因地理位置造成的延遲。

在 Azure App Service 或 Azure Container Apps 中託管的 Blazor 應用程式不需要這項服務,但在其他託管環境中可能會有所幫助:

  • 為了方便連線向外延展。
  • 處理全球配送。

注意

具狀態重新連線 (WithStatefulReconnect) 已隨 .NET 8 一起發佈,但目前不支援 Azure SignalR 服務。 如需詳細資訊,請參閱具狀態重新連線支援?(Azure/azure-signalr #1878)

如果應用程式使用 Long Polling 或回退到 Long Polling 而不是 WebSockets ,您可能需要設定最大輪詢間隔 (MaxPollIntervalInSeconds,預設值:5 秒,限制:1-300 秒),它定義了 Azure SignalR 服務中 Long Polling 連線允許的最大輪詢間隔。 如果下一次輪詢請求未在最大輪詢時間間隔內到達,服務會關閉用戶端連線。。

有關如何將服務新增為生產部署的依賴,請參閱 Publish an ASP.NET Core SignalR app to Azure App Service

如需詳細資訊,請參閱

Azure 容器應用程式

如需在 Azure 容器應用程式服務上調整伺服器端 Blazor 應用程式的更深入探索,請參閱在 Azure 上調整 ASP.NET Core 應用程式。 本教學課程說明如何建立和整合在 Azure 容器應用程式上託管應用程式所需的服務。 本節也提供了基本步驟。

  1. 遵循 Azure 容器應用程式中的工作階段親和性 (Azure 文件) 中的指導來設定 Azure 容器應用程式服務的工作階段親和性。

  2. 必須設定 ASP.NET Core 資料保護服務 (DP),以將金鑰保存在一個所有容器執行個體都可以存取的集中位置。 金鑰可以儲存在 Azure Blob 儲存體中,並使用 Azure Key Vault 進行保護。 DP 服務使用鍵來還原序列化 Razor 元件。 若要設定 DP 服務以使用 Azure Blob 儲存體 和 Azure Key Vault,請參考下列 NuGet 套件:

    注意

    如需將套件新增至 .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 儲存體和應用程式的金鑰保存庫。

  4. 若要建立容器應用程式管理的 identity,並授與它可存取 Blob 儲存體和金鑰保存庫,請完成下列步驟:

    1. 在 Azure 入口網站中,瀏覽至容器應用程式的概觀頁面。
    2. 從左側導覽中選取 [服務連接器]
    3. 從頂端導覽中選取 [+ 建立]
    4. 在 [建立連線] 飛出視窗中,輸入下列值:
      • 容器:選取您建立用於託管應用程式的容器應用程式。
      • 服務類型:選取 [Blob 儲存體]
      • 訂用帳戶:選取擁有容器應用程式的訂用帳戶。
      • 連線名稱:輸入 scalablerazorstorage 的名稱。
      • 用戶端類型:選取 [.NET],然後選取 [下一步]
    5. 選取 [系統指派的受控的identity],然後選取 [下一步]
    6. 使用預設的網路設定,然後選取 [下一步]
    7. 在 Azure 驗證設定之後,選取 [建立]

    對金鑰保存庫重複上述設定。 在 [基本] 索引標籤中選取適當的金鑰保存庫服務和金鑰。

IIS

使用 IIS 時,請啟用:

如需詳細資訊,請參閱將 ASP.NET Core 應用程式發佈到 IIS 中的指導和外部 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 緩衝的組態 (proxy_buffering off;),因為此設定僅適用於伺服器傳送的事件 (SSE),而這與 Blazor 應用程式的用戶端伺服器互動無關。

如需詳細資訊和組態方面的指導,請參閱下列資源:

使用 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

檢查瀏覽器主控台是否有 WebSocket 錯誤。 範例錯誤:

  • Firefox 無法與位於 ws://the-domain-name.tld/_blazor?id=XXX 的伺服器建立連線
  • 錯誤:無法啟動傳輸 'WebSockets':錯誤:傳輸發生錯誤。
  • 錯誤:無法啟動傳輸 'LongPolling': TypeError: this.transport 未定義
  • 錯誤:無法使用任何可用的傳輸連接到伺服器。 WebSocket 失敗
  • 錯誤:如果連線未處於「已連線」狀態,則無法傳送資料。

如需詳細資訊和組態方面的指導,請參閱下列資源:

測量網路延遲

JS Interop 可用來測量網路延遲,如下列範例所示。

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

<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 毫秒或更短。

記憶體管理

在伺服器上,為每個使用者工作階段建立一個新線路。 每個使用者工作階段對應於在瀏覽器中呈現單一文件。 例如,多個索引標籤會建立多個工作階段。

Blazor 維護與起始工作階段的瀏覽器的持續連線 (稱為線路 (circuit))。 連線可能隨時會因多種原因而遺失 (例如當使用者遺失網路連線或突然關閉瀏覽器時)。 當連線遺失時,Blazor 會有一個復原機制,可將有限的線路數放入「已斷線」的集區中,並為用戶端提供一段有限的時間來重新連線並重新建立工作階段 (預設值:3 分鐘)。

在那段時間之後,Blazor 會釋放線路並捨棄工作階段。 從該點起,線路就符合進行記憶體回收 (GC) 的資格,且會在觸發線路的 GC 世代的回收時被回收。 需要了解的一個重要方面是線路具有一段很長的存留期 (這意味著線路所產生的大部分物件最終都會到達 Gen 2)。 因此,在進行 Gen 2 回收之前,您可能不會看到這些物件被釋放。

測量一般記憶體使用量

先決條件:

  • 應用程式必須在發行組態中發佈。 偵錯組態測量並不相關,因為產生的程式碼並不代表用於生產部署的程式碼。
  • 應用程式必須在未附加偵錯工具的情況下執行,因為這也可能會影響應用程式的行為並破壞結果。 在 Visual Studio 中,從功能表列選取 [偵錯>啟動但不偵錯] 或使用鍵盤按 Ctrl+F5,以啟動應用程式而不進行偵錯。
  • 思考不同的記憶體類型以了解 .NET 實際使用了多少記憶體。 通常,開發人員會在 Windows OS 上的 [作管理員] 中檢查應用程式的記憶體使用量 (這通常會提供實際使用記憶體的上限)。 如需詳細資訊,請參閱下列文章:

套用至 Blazor 的記憶體使用量

我們計算了 blazor 所使用的記憶體,如下所示:

(作用中的線路數 × 每條線路的記憶體量) + (中斷連線的線路數 × 每條線路的記憶體量)

線路使用的記憶體量以及應用程式可以維護的最大可能的作用中線路數,主要取決於應用程式的撰編寫方式。 可能的使用中線路數目上限大致描述如下:

最大可用的記憶體量 / 每條線路的記憶體量 = 最大可能的作用中線路數

對於 Blazor 中發生記憶體洩漏,必須滿足以下條件:

  • 記憶體必須由架構配置,而不是應用程式。 如果您在應用程式中配置 1 GB 陣列,則應用程式必須管理該陣列的處置。
  • 記憶體不得被主動使用 (這意味著線路未處於作用中的狀態,而且已從中斷連線的線路快取中收回)。 如果您執行了最大的作用中線路數,則記憶體不足是一個縮放的問題,而不是記憶體洩漏的問題。
  • 線路 GC 世代的記憶體回收 (GC) 已經執行,但是記憶體回收行程無法回收線路,這是因為架構中的另一個物件持有對該線路的強烈引用。

在其他情況下,不存在記憶體洩漏的問題。 如果線路處於作用中的狀態 (已連線或已中斷連線),則該線路仍在使用中。

如果線路的 GC 世代回收未執行,則記憶體不會被釋放,因為記憶體回收行程當時不需要釋放記憶體。

如果 GC 世代的回收已執行並釋放線路,則您必須根據 GC 統計資料來驗證記憶體 (而不是處理程序),因為 .NET 可能會決定讓虛擬記憶體保持作用中。

如果記憶體未釋放,則您必須找到一條既不處於作用中也不是中斷連線,並且被架構中的另一個物件根引用的線路。 在任何其他情況下,無法釋放記憶體是開發人員程式碼中的應用程式問題。

減少記憶體使用量

採用下列任何策略來減少應用程式的記憶體使用量:

  • 限制 .NET 處理程序所使用的記憶體總量。 如需詳細資訊,請參閱記憶體回收的執行階段組態選項
  • 減少已中斷連線的線路數目。
  • 減少允許線路處於中斷連線狀態的時間。
  • 手動觸發記憶體回收,以在停機期間執行回收。
  • 在工作站模式 (而不是伺服器模式) 中設定記憶體回收 (這會主動觸發記憶體回收)。

某些行動裝置瀏覽器的堆積大小

建置在用戶端上執行,且以行動裝置瀏覽器 (尤其是 iOS 上的 Safari) 為目標的 Blazor 應用程式時,可能需要使用 MSBuild 屬性 EmccMaximumHeapSize 來減少應用程式的最大記憶體。 如需詳細資訊,請參閱裝載和部署 ASP.NET Core Blazor WebAssembly

其他動作和考量事項

  • 在記憶體需求很高時擷取處理程序的記憶體傾印,並識別佔用最多記憶體的物件以及這些物件的根位置 (保存對它們的引用的物件)。
  • 您可以使用 dotnet-counters 檢查應用程式中記憶體使用情況的統計資料。 如需詳細資訊,請參閱調查效能計數器 (dotnet-counters)
  • 即使觸發記憶體回收,.NET 仍會保留記憶體,而不是立即還給 OS,因為其可能在不久後就會再次使用該記憶體。 這可避免不斷地認可和解除認可記憶體,這些動作費用高昂。 如果您使用 dotnet-counters 便會看到這種情況,因為當記憶體回收發生時,已使用的記憶體數量會降至 0 (零),但工作集計數器不會減少,這是 .NET 保留記憶體以重複使用的訊號。 如需專案檔 (.csproj) 設定以控制此行為的詳細資訊,請參閱記憶體回收的執行階段組態選項
  • 伺服器記憶體回收不會觸發記憶體回收,除非其判斷絕對有必要這樣做,藉此避免凍結您的應用程式,並將您的應用程式視為機器上唯一執行的項目,因此可使用系統中的所有記憶體。 如果系統有 50 GB,記憶體回收行程會在觸發 Gen 2 回收之前嘗試使用全部 50 GB 的可用記憶體。
  • 如需中斷連線的線路保留組態的資訊,請參閱 ASP.NET Core BlazorSignalR 指引

測量記憶體

  • 在發行組態中發佈應用程式。
  • 執行應用程式已發佈的版本。
  • 請勿將偵錯工具附加至執行中的應用程式。
  • 觸發 Gen 2 強制性壓縮集合(GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true)) 會釋放記憶體嗎?
  • 請考慮您的應用程式是否在大型物件堆積上配置物件。
  • 在應用程式準備好接受要求和處理後,您有測試記憶體增長嗎? 一般而言,當程式代碼第一次執行時會填入快取,將一定量的記憶體新增至應用程式的使用量。