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

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

本文說明如何使用 ASP.NET Core 來託管和部署伺服器端 Blazor 應用程式。

主機組態值

伺服器端 Blazor 應用程式可以接受一般主機設定值

部署

Blazor 使用伺服器端託管模型,在伺服器上從 ASP.NET Core 應用程式中執行。 UI 更新、事件處理及 JavaScript 呼叫透過 SignalR 連線進行處理。

需要能夠裝載 ASP.NET Core 應用程式的網路伺服器。 Visual Studio 包含伺服器端應用程式專案範本。 如需 Blazor 專案範本的詳細資訊,請參閱 ASP.NET Core Blazor 專案結構

延展性

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

  • 伺服器可支援的作用中線路數目。
  • 用戶端上的 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 就會使用長輪詢。 在部署至 Azure App Service 時,請在服務的 Azure 入口網站設定中將應用程式設定為使用 WebSocket。 如需如何設定 Azure App Service 應用程式的詳細資訊,請參閱 SignalR 發行方針

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

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

全域部署和連線失敗

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

Azure SignalR Service

針對採用互動式伺服器端轉譯的 Blazor Web Apps,請考慮使用 Azure SignalR Service。 該服務可與應用程式的 Blazor 中樞搭配運作,以向上擴充至大量的並行 SignalR 連線。 此外,服務的全球觸達和高效能資料中心可大幅協助降低因地理位置造成的延遲。 如果您的裝載環境已經處理這些考慮,則不需要使用 Azure SignalR 服務。

請考慮使用 Azure SignalR Service,該服務可與應用程式的 Blazor 中樞搭配運作,以向上擴充至大量的並行 SignalR 連線。 此外,服務的全球觸達和高效能資料中心可大幅協助降低因地理位置造成的延遲。 如果您的裝載環境已經處理這些考慮,則不需要使用 Azure SignalR 服務。

重要

停用 WebSocket 時,Azure App Service 會使用 HTTP 長輪詢來模擬即時連線。 HTTP 長輪詢明顯比啟用 WebSocket 的情況 (不使用輪詢來模擬用戶端-伺服器連線) 執行的速度要慢。 如果必須使用長輪詢,則您可能需要設定最大的輪詢間隔 (MaxPollIntervalInSeconds),它定義了 Azure SignalR Service 中長輪詢連線允許的最大輪詢間隔 (如果該服務曾從 WebSocket 回退到長輪詢的話)。 如果在 MaxPollIntervalInSeconds 內未收到下一個輪詢要求,Azure SignalR Service 便會清除用戶端連線。 請注意,當快取等待寫入緩衝區大小大於 1 MB 時,Azure SignalR Service 也會清除連線以確保服務效能。 MaxPollIntervalInSeconds 的預設值為 5 秒。 此設定限制為 1-300 秒。

我們建議對部署至 Azure App Service 的伺服器端 Blazor 應用程式使用 WebSocket。 Azure SignalR Service 預設會使用 WebSocket。 如果應用程式不使用 Azure SignalR 服務,請參閱將 ASP.NET Core SignalR 應用程式發佈到 Azure App Service

如需詳細資訊,請參閱

組態

若要設定適用於 Azure SignalR Service 的應用程式,則應用程式必須支援黏性工作階段 (在當中用戶端會在預先呈現時被重新導向回到同一台伺服器)。 ServerStickyMode 選項或組態值會設定為 Required。 通常,應用程式會使用下列其中一種方法來建立組態:

  • Program.cs

    builder.Services.AddSignalR().AddAzureSignalR(options =>
    {
        options.ServerStickyMode = 
            Microsoft.Azure.SignalR.ServerStickyMode.Required;
    });
    
  • 組態 (使用下列其中一種方法):

    • appsettings.json 中:

      "Azure:SignalR:ServerStickyMode": "Required"
      
    • Azure 入口網站中應用程式服務的 [組態]>[應用程式設定] ([名稱]Azure__SignalR__ServerStickyMode[值]Required)。 如果您佈建 Azure SignalR Service,則應用程式會自動採用此方法。

注意

尚未對 Azure SignalR Service 啟用黏性工作階段的應用程式會擲回以下錯誤:

blazor.server.js:1 Uncaught (in promise) 錯誤:因基礎連線關閉而取消叫用。

佈建 Azure SignalR Service

若要在 Visual Studio 中為應用程式佈建 Azure SignalR Service:

  1. 在 Visual Studio 中為應用程式建立一個 Azure Apps 發佈設定檔。
  2. Azure SignalR Service 相依性新增至該設定檔中。 如果 Azure 訂用帳戶沒有預先存在的 Azure SignalR Service 實例來指派給應用程式,請選取 [建立新的 Azure SignalR 服務實例] 以佈建新的服務實例。
  3. 將應用程式發佈至 Azure。

在 Visual Studio 中佈建 Azure SignalR Service 會自動啟用 黏性工作階段,並將 SignalR 連接字串新增至應用程式服務的組態中。

Azure 容器應用程式的可擴縮性

除了使用 Azure SignalR Service 之外,在 Azure 容器應用程式上擴縮伺服器端 Blazor 應用程式還需要注意特定的事項。 由於處理要求路由傳送的方式,因此必須設定 ASP.NET Core 資料保護服務,以將金鑰保存在一個所有容器實例都可以存取的集中位置。 金鑰可以儲存在 Azure Blob 儲存體中,並使用 Azure Key Vault 進行保護。 資料保護服務會使用金鑰來還原序列化 Razor 元件。

注意

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

  1. 若要將資料保護服務設定為使用 Azure Blob 儲存體和 Azure Key Vault,請參考下列 NuGet 套件:

    注意

    如需將套件新增至 .NET 應用程式的指引,請參閱在套件取用工作流程 (NuGet 文件)安裝及管理套件底下的文章。 在 NuGet.org 確認正確的套件版本。

  2. 使用下列醒目提示的程式碼更新 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();
    

    上面的變更可讓應用程式使用集中式、可調整的架構來管理資料保護。 DefaultAzureCredential 會在程式碼部署到 Azure 後探索到容器應用程式受控識別,並用它來連接到 Blob 儲存體和應用程式的金鑰保存庫。

  3. 若要建立容器應用程式受控識別,並授與它對 Blob 儲存體和金鑰保存庫的存取權,請完成下列步驟:

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

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

沒有 Azure SignalR Service 的 Azure App Service

在 Azure App Service 上裝載使用互動式伺服器端轉譯的 Blazor Web 應用程式,需要設定應用程式要求路由 (ARR) 親和性和 WebSocket。 App Service 也應該適當地分散到全域,以減少 UI 延遲。 在 Azure App Service 上裝載時,不需要使用 Azure SignalR 服務。

在 Azure App Service 上裝載 Blazor Server 應用程式需要設定應用程式要求路由 (ARR) 親和性和 WebSocket。 App Service 也應該適當地分散到全域,以減少 UI 延遲。 在 Azure App Service 上裝載時,不需要使用 Azure SignalR 服務。

使用下列指引來設定應用程式:

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

@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)) 會釋放記憶體嗎?
  • 請考慮您的應用程式是否在大型物件堆積上配置物件。
  • 在應用程式準備好接受要求和處理後,您有測試記憶體增長嗎? 一般而言,當程式代碼第一次執行時會填入快取,將一定量的記憶體新增至應用程式的使用量。