托管和部署服务器端 Blazor 应用

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

本文介绍如何使用 ASP.NET Core 托管和部署服务器端 Blazor 应用。

主机配置值

服务器端 Blazor 应用可以接受通用主机配置值

部署

使用服务器端托管模型时,Blazor 从 ASP.NET Core 应用内在服务器上执行。 UI 更新、事件处理和 JavaScript 调用是通过 SignalR 连接进行处理。

能够托管 ASP.NET Core 应用的 Web 服务器是必需的。 Visual Studio 包含服务器端应用项目模板。 有关 Blazor 项目模板的详细信息,请参阅 ASP.NET Core Blazor 项目结构

可伸缩性

考虑单一服务器的可伸缩性(纵向扩展)时,需注意:可供应用使用的内存可能是用户需求增加时应用耗尽的第一个资源。 服务器上的可用内存影响以下因素:

  • 服务器可以支持的主动电路数。
  • 客户端上的 UI 延迟。

有关构建安全且可缩放服务器端 Blazor 应用的指导,请参阅以下资源:

每个电路使用约 250 KB 的内存来实现至少为 Hello World 样式的应用。 电路大小取决于应用代码和与每个组件相关的状态维护要求。 我们建议你在开发应用和基础设施的过程中衡量资源需求,但在计划部署目标时可以将以下基准作为起点:如果希望应用支持 5,000 个并发用户,请考虑为应用预算至少 1.3 GB 服务器内存(或每用户 ~273 KB)。

SignalR 配置

SignalR 的托管和缩放条件适用于使用 SignalR 的 Blazor 应用。

有关 Blazor 应用中 SignalR 的详细信息(包括配置指南)请参阅 ASP.NET Core BlazorSignalR 指南

传输

由于低延迟、更好的可靠性和改进的安全性,使用 WebSockets 作为 SignalR 传输时,Blazor 的效果最佳。 当 WebSocket 不可用时,或在将应用显式配置为使用长轮询时,SignalR 将使用长轮询。 部署到 Azure 应用服务时,请在服务的 Azure 门户设置中将应用配置为使用 WebSocket。 有关为 Azure 应用服务配置应用的详细信息,请参阅 SignalR 发布指南

如果使用长轮询,则会出现控制台警告:

无法使用长轮询回退传输通过 Websocket 连接。 这可能是由于 VPN 或代理阻止连接而导致的。

全球部署和连接失败

针对部署到地理数据中心的全球部署的建议:

Azure SignalR 服务

对于采用交互式服务器端呈现的 Blazor Web 应用,请考虑使用 Azure SignalR 服务。 该服务与应用的 Blazor 中心配合使用,以便扩展到大量并发 SignalR 连接。 此外, 服务的全球覆盖和高性能数据中心可帮助显著减少由于地理位置造成的延迟。 如果托管环境已经处理了这些问题,则不需要使用 Azure SignalR 服务。

请考虑使用 Azure SignalR 服务,该服务可与应用的 Blazor 中心配合使用,以扩展到大量并发 SignalR 连接。 此外, 服务的全球覆盖和高性能数据中心可帮助显著减少由于地理位置造成的延迟。 如果托管环境已经处理了这些问题,则不需要使用 Azure SignalR 服务。

重要

禁用 WebSocket 后,Azure 应用服务使用 HTTP 长轮询模拟实时连接。 HTTP 长轮询明显比在启用 WebSocket 的情况下运行慢,WebSocket 不使用轮询来模拟客户机-服务器连接。 如果必须使用长轮询,可能需要配置最大轮询间隔 (MaxPollIntervalInSeconds),该间隔定义 Azure SignalR 服务中允许用于长轮询连接的最大轮询间隔(服务可能需要从 WebSocket 回退到长轮询)。 如果下一轮询请求未在 MaxPollIntervalInSeconds 秒内传入,Azure SignalR 服务会清理客户端连接。 请注意,当缓存的等待写入的缓冲区大小超过 1 MB 时,Azure SignalR 服务也会清理连接,以确保服务性能正常。 MaxPollIntervalInSeconds 的默认值为 5 秒。 此设置的范围为 1-300 秒。

我们建议对部署到 Azure 应用服务的服务器端 Blazor 应用使用 WebSocket。 默认情况下,Azure SignalR 服务使用 WebSockets。 如果应用不使用 Azure SignalR 服务,请参阅将 ASP.NET Core SignalR 应用发布到 Azure 应用服务

有关详细信息,请参阅:

Configuration

若要为 Azure SignalR 服务配置应用,应用必须支持粘滞会话;在此情况下,客户端在预呈现时被重定向回同一服务器。 将 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 服务,则为应用自动采用此方式。

注意

未为 Azure SignalR 服务启用粘滞会话的应用引发以下错误:

blazor.server.js:1 Uncaught (in promise) 错误:由于基础连接已关闭,调用被取消。

预配 Azure SignalR 服务

若要在 Visual Studio 中为应用预配 Azure SignalR 服务:

  1. 在 Visual Studio 中创建适用于 应用的 Azure 应用发布配置文件。
  2. Azure SignalR 服务依赖项添加到配置文件。 如果 Azure 订阅没有要分配给应用的预先存在的 Azure SignalR 服务实例,请选择“创建新的 Azure SignalR 服务实例”以预配新的服务实例。
  3. 将应用发布到 Azure。

如果在 Visual Studio 中预配 Azure SignalR 服务,则会自动启用粘滞会话,并将 SignalR 连接字符串添加到应用服务的配置中。

Azure 容器应用的可伸缩性

在 Azure 容器应用上缩放服务器端 Blazor 应用时,除了使用 Azure SignalR 服务外,还需要遵循特定的注意事项。 考虑到处理请求路由的方式,必须将 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 应用服务服务

在 Azure 应用服务上托管使用交互式服务器端呈现的 Blazor Web 应用需要配置应用程序请求路由 (ARR) 相关性和 WebSocket。 应用服务还应相应地进行全局分发,以减少 UI 延迟。 如果托管在 Azure 应用服务上,则不需要使用 Azure SignalR 服务。

在 Azure 应用服务上托管 Blazor Server 应用需要配置应用程序请求路由 (ARR) 相关性和 WebSocket。 应用服务还应相应地进行全局分发,以减少 UI 延迟。 如果托管在 Azure 应用服务上,则不需要使用 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"

Linux 与 Nginx

按照 ASP.NET Core SignalR 应用的指南操作,并做出以下更改:

  • location 路径从 /hubroute (location /hubroute { ... }) 更改为根路径 / (location / { ... })。
  • 删除代理缓冲配置 (proxy_buffering off;) 因为设置仅适用于服务器发送事件 (SSE),这与 Blazor 应用客户端-服务器交互不相关。

有关详细信息和配置指南,请参阅以下资源:

Linux 与 Apache

若要在 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
  • 错误:未能启动传输“WebSocket”:错误:传输出现错误。
  • 错误:未能启动传输“LongPolling”:TypeError:未定义 this.transport
  • 错误:无法使用任何可用传输连接到服务器。 WebSocket 失败
  • 错误:如果连接未处于“已连接”状态,则无法发送数据。

有关详细信息和配置指南,请参阅以下资源:

衡量网络延迟

可以使用 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 体验,我们建议使用 250 毫秒或更低的持续 UI 延迟。

内存管理

在服务器上,将为每个用户会话创建一个新线路。 每个用户会话对应于在浏览器中呈现一个文档的过程。 例如,多个选项卡可以创建多个会话。

Blazor 与启动会话的浏览器保持着持续连接(称为线路)。 连接可能在任何时候因任何原因而丢失,例如当用户失去网络连接或突然关闭浏览器时。 当连接丢失时,Blazor 具有一种恢复机制,将有限数量的线路放在一个“已断开连接”的池中,使客户端在有限的时间内重新连接并重新建立会话(默认值:3 分钟)。

在此时间之后,Blazor 释放线路并放弃会话。 从此以后,该线路就有资格进行垃圾回收 (GC),并在触发了线路的 GC 生成回收时声明。 需要了解的一个重要方面是,线路的生存期很长,这意味着由线路生根的大多数对象最终都会达到第 2 代。 因此,在发生第 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)
  • 即使触发了 GC,.NET 也会保留内存,而不是立即将其返回给 OS,因为它很可能会在不久的将来重用内存。 这样可以避免不断提交和取消提交内存,从而节省开销。 如果使用 dotnet-counters,你会观察到这一点,因为你会看到 GC 发生,使用的内存量减少到 0(零),但你不会看到工作集计数器减少,这是 .NET 保留内存以重用它的迹象。 有关用于控制此行为的项目文件 (.csproj) 设置的详细信息,请参阅垃圾回收的运行时配置选项
  • 服务器 GC 不会触发垃圾回收,除非它认为绝对有必要这样做,以避免应用冻结,并认为你的应用是计算机上唯一运行的程序,因此它可以使用系统中的所有内存。 如果系统具有 50 GB,垃圾回收器在触发第 2 代回收之前会尝试使用全部 50 GB 的可用内存。
  • 有关断开连接的线路保留配置的信息,请参阅 ASP.NET Core BlazorSignalR 指南

测量内存

  • 在发布配置中发布应用。
  • 运行应用的已发布版本。
  • 不要将调试程序附加到正在运行的应用。
  • 触发第 2 代强制压缩回收 (GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true)) 是否会释放内存?
  • 请考虑你的应用是否在大型对象堆上分配对象。
  • 你是在应用通过请求和处理预热后测试其内存增长情况吗? 通常情况下,代码首次执行时会填充缓存,这些缓存会不断增加应用的内存占用。