Hospedar e implantar aplicativos Blazor do lado do servidor

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Esse artigo explica como hospedar e implantar aplicativos Blazor (Blazor Web Apps e Blazor Servers) do lado do servidor usando o ASP.NET Core.

Valores de configuração do host

Os aplicativos Blazor do lado do servidor podem aceitar valores de configuração de Host Genérico.

Implantação

Ao usar um modelo de hospedagem no lado do servidor, Blazor é executado no servidor a partir de um aplicativo do ASP.NET Core. As atualizações da interface do usuário, a manipulação de eventos e as chamadas de JavaScript são realizadas por uma conexão SignalR.

É necessário um servidor Web capaz de hospedar um aplicativo ASP.NET Core. O Visual Studio inclui um modelo de projeto de aplicativo do lado do servidor. Para obter mais informações sobre modelos de projeto Blazor, consulte Estrutura do projeto Blazor do ASP.NET Core.

Publique um aplicativo na configuração de versão e implante o conteúdo da pasta bin/Release/{TARGET FRAMEWORK}/publish, em que o espaço reservado {TARGET FRAMEWORK} é a estrutura de destino.

Escalabilidade

Ao considerar a escalabilidade de um único servidor (escalar verticalmente), a memória disponível para um aplicativo provavelmente é o primeiro recurso que o aplicativo esgota à medida que as demandas do usuário aumentam. A memória disponível no servidor afeta:

  • Número de circuitos ativos aos quais um servidor pode dar suporte.
  • Latência de interface do usuário no cliente.

Para obter diretrizes sobre como compilar aplicativos Blazor seguros e escalonáveis do lado do servidor, consulte os recursos a seguir:

Cada circuito usa aproximadamente 250 KB de memória para um aplicativo mínimo de estilo Olá, Mundo. O tamanho de um circuito depende do código do aplicativo e dos requisitos de manutenção de estado associados a cada componente. Recomendamos que você meça as demandas de recursos durante o desenvolvimento para seu aplicativo e infraestrutura, mas a linha de base a seguir pode ser um ponto de partida no planejamento de seu destino de implantação: se você espera que seu aplicativo dê suporte a 5.000 usuários simultâneos, considere o orçamento de pelo menos 1,3 GB de memória do servidor para o aplicativo (ou ~273 KB por usuário).

Configuração de SignalR

As condições de hospedagem e dimensionamento do SignalR se aplicam aos aplicativos Blazor que usam SignalR.

Para obter mais informações sobre aplicativos SignalR em Blazor, incluindo diretrizes de configuração, consulte ASP.NET Core BlazorSignalR diretrizes.

Transportes

Blazor funciona melhor ao usar WebSockets como o transporte do SignalR devido à menor latência, melhor confiabilidade e maior segurança. A Sondagem Longa é usada por SignalR quando WebSockets não está disponível ou quando o aplicativo está explicitamente configurado para usar a Sondagem Longa.

Um aviso do console será exibido se a Sondagem Longa for utilizada:

Falha ao se conectar por meio de WebSockets usando o transporte de fallback de Sondagem Longa. Isso pode ocorrer porque uma VPN ou proxy está bloqueando a conexão.

Falhas globais de implantação e conexão

Recomendações para implantações globais nos data centers geográficos:

  • Implante o aplicativo nas regiões em que a maioria dos usuários reside.
  • Leve em consideração o aumento da latência para o tráfego em todos os continentes. Para controlar a aparência da UI de reconexão, veja a orientação do ASP.NET Core BlazorSignalR.
  • Considere usar o Serviço SignalR do Azure.

Serviço de Aplicativo do Azure

Para hospedar um Serviço de Aplicativo do Azure, é necessário configurar WebSockets e afinidade de sessão, também chamada de afinidade ARR (Application Request Routing).

Observação

Um aplicativo Blazor no Serviço de Aplicativo do Azure não requer o Serviço SignalR do Azure.

Para o registro do aplicativo no Serviço de Aplicativo do Azure, habilite:

  • WebSockets para permitir que o transporte de WebSockets funcione. A configuração padrão é Off.
  • Afinidade de sessão para encaminhar solicitações de um usuário de volta para a mesma instância do Serviço de Aplicativo. A configuração padrão é On.
  1. No portal do Azure, navegue até o aplicativo Web emServiços de Aplicativos.
  2. Abra Configurações>Configuração.
  3. Defina os Soquetes da Web como Ativados.
  4. Verifique se a Afinidade da sessão está definida como Ativada.

Serviço SignalR do Azure

O Serviço SignalR do Azure funciona em conjunto com o hub SignalR do aplicativo para escalar verticalmente um aplicativo do lado do servidor para um grande número de conexões simultâneas. Além disso, o alcance global do serviço e os data centers de alto desempenho ajudam significativamente a reduzir a latência devido à geografia.

O serviço não é necessário para aplicativos Blazor hospedados no Serviço de Aplicativo do Azure ou nos Aplicativos de Contêiner do Azure, mas pode ser útil em outros ambientes de hospedagem:

  • Para facilitar a escala horinzontal da conexão.
  • Habilitar distribuição global

Observação

A reconexão com estado (WithStatefulReconnect) foi lançada com o .NET 8, mas atualmente não tem suporte para o Serviço Azure SignalR. Para obter mais informações, veja Suporte para reconexão com estado? (Azure/azure-signalr #1878).

Caso o aplicativo use a Sondagem longa ou retorne para Songadem Londa em vez de WebSockets, talvez seja necessário configurar o intervalo máximo de sondagem (MaxPollIntervalInSeconds, padrão: 5 segundos, limite: 1-300 segundos), que define o intervalo máximo de sondagem permitido para conexões de Sondagem longa no Serviço SignalR do Azure. Se a próxima solicitação de sondagem não chegar dentro do intervalo máximo de sondagem, o serviço fechará a conexão do cliente.

Para obter diretrizes sobre como adicionar o serviço como uma dependência a uma implantação de produção, consulte Publicar um aplicativo SignalR do ASP.NET Core no Serviço de Aplicativo do Azure.

Para saber mais, veja:

Aplicativos de Contêiner do Azure

Para ver uma exploração mais detalhada da escala de aplicativos Blazor do lado do servidor no serviço Aplicativos de Contêiner do Azure, confira Como escalar aplicativos ASP.NET Core no Azure. O tutorial explica como criar e integrar os serviços necessários para hospedar aplicativos nos Aplicativos de Contêiner do Azure. As etapas básicas também são fornecidas nesta seção.

  1. Configure o serviço Aplicativos de Contêiner do Azure para a afinidade de sessão seguindo as diretrizes descritas em Afinidade de sessão nos Aplicativos de Contêiner do Azure (documentação do Azure).

  2. O serviço de DP (Proteção de Dados) do ASP.NET Core precisa ser configurado para manter as chaves em uma localização centralizada que todas as instâncias de contêiner podem acessar. As chaves podem ser armazenadas no Armazenamento de Blobs do Azure e protegidas com o Azure Key Vault. O serviço de DP usa as chaves para desserializar componentes Razor. Para configurar o serviço de DP para usar o Armazenamento de Blobs do Azure e o Azure Key Vault, faça referência aos seguintes pacotes NuGet:

    Observação

    Para obter diretrizes sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes no Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas de pacote em NuGet.org.

  3. Atualize Program.cs com o seguinte código realçado:

    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();
    

    As alterações anteriores permitem que o aplicativo gerencie o serviço de DP usando uma arquitetura centralizada e escalonável. DefaultAzureCredential descobre a identity gerenciada do aplicativo de contêiner depois que o código é implantado no Azure e o usa para se conectar ao armazenamento de blobs e ao cofre de chaves do aplicativo.

  4. Para criar a identity gerenciada do aplicativo de contêiner e conceder a ela acesso ao armazenamento de blobs e a um cofre de chaves, conclua as seguintes etapas:

    1. No Portal do Azure, navegue até a página de visão geral do aplicativo de contêiner.
    2. À esquerda, selecione Conector de serviço.
    3. Selecione + Criar na navegação superior.
    4. No submenu Criar conexão, insira os seguintes valores:
      • Contêiner: selecione o aplicativo de contêiner que você criou para hospedar seu aplicativo.
      • Tipo de serviço: selecione Armazenamento de Blobs.
      • Assinatura: selecione a assinatura que possui o aplicativo de contêiner.
      • Nome da conexão: insira um nome de scalablerazorstorage.
      • Tipo de cliente: selecione .NET e, em seguida, selecione Avançar.
    5. Selecione identity gerenciada atribuída pelo sistema e clique em Avançar.
    6. Use as configurações de rede padrão e selecione Avançar.
    7. Depois que o Azure validar as configurações, selecione Criar.

    Repita as configurações anteriores para o cofre de chaves. Selecione o serviço e a chave do cofre de chaves apropriados na guia Básico.

IIS

Ao usar o IIS, habilite:

Para obter mais informações, consulte as diretrizes e links cruzados de recursos externos do IIS em Publicar um aplicativo do ASP.NET Core no IIS.

Kubernetes

Crie uma definição de entrada com as seguintes Anotações do Kubernetes para afinidade de sessão:

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 com o Nginx

Siga as diretrizes para um aplicativo SignalR do ASP.NET Core com as seguintes alterações:

  • Altere o location caminho de /hubroute (location /hubroute { ... }) para o caminho raiz / (location / { ... }).
  • Remova a configuração para buffer de proxy (proxy_buffering off;), pois a configuração só se aplica a SSE (Eventos Enviados pelo Servidor), que não são relevantes para as interações cliente-servidor do aplicativo do Blazor.

Para obter mais informações e diretrizes de configuração, confira os seguintes recursos:

Linux com o Apache

Para hospedar um aplicativo Blazor por trás do Apache no Linux, configure ProxyPass para tráfego HTTP e WebSockets.

No exemplo a seguir:

  • O servidor Kestrel está em execução no computador host.
  • O aplicativo escuta o tráfego na porta 5000.
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

Habilite os seguintes módulos:

a2enmod   proxy
a2enmod   proxy_wstunnel

Verifique se há erros de WebSockets no console do navegador. Erros de exemplo:

  • O Firefox não pode estabelecer uma conexão com o servidor em ws://the-domain-name.tld/_blazor?id=XXX
  • Erro: falha ao iniciar o transporte 'WebSockets': Error: ocorreu um erro com o transporte.
  • Erro: falha ao iniciar o transporte 'LongPolling': TypeError: this.transport é indefinido
  • Erro: não é possível se conectar ao servidor com qualquer um dos transportes disponíveis. Falha no WebSockets
  • Erro: não é possível enviar dados se a conexão não estiver no Estado 'Conectado'.

Para obter mais informações e diretrizes de configuração, confira os seguintes recursos:

Medir latência de rede

JSO interoperabilidade pode ser usado para medir a latência de rede, como demonstra o exemplo a seguir.

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();
        }
    }
}

Para uma experiência de interface do usuário razoável, recomendamos uma latência de interface do usuário sustentada de 250 ms ou menos.

Gerenciamento de memória

No servidor, um novo circuito é criado para cada sessão de usuário. Cada sessão de usuário corresponde à renderização de um único documento no navegador. Por exemplo, várias guias criam várias sessões.

Blazor mantém uma conexão constante com o navegador, chamado de circuito, que iniciou a sessão. As conexões podem ser perdidas a qualquer momento por vários motivos, como quando o usuário perde a conectividade de rede ou fecha abruptamente o navegador. Quando uma conexão é perdida, Blazor tem um mecanismo de recuperação que coloca um número limitado de circuitos em um pool "desconectado", dando aos clientes uma quantidade limitada de tempo para reconectar e restabelecer a sessão (padrão: 3 minutos).

Depois desse tempo, Blazor libera o circuito e descarta a sessão. Desse ponto em diante, o circuito é qualificado para GC (coleta de lixo) e é reivindicado quando uma coleta para a geração de GC do circuito é disparada. Um aspecto importante a ser compreendido é que os circuitos têm um longo tempo de vida, o que significa que a maioria dos objetos com raiz pelo circuito eventualmente chega à Gen 2. Como resultado, talvez você não veja esses objetos liberados até que ocorra uma coleção Gen 2.

Medir o uso de memória em geral

Pré-requisitos:

  • O aplicativo deve ser publicado na Configuração de versão. As medidas de configuração de depuração não são relevantes, pois o código gerado não representa o código usado para uma implantação de produção.
  • O aplicativo deve ser executado sem um depurador anexado, pois isso também pode afetar o comportamento do aplicativo e estragar os resultados. No Visual Studio, inicie o aplicativo sem depuração selecionando Depurar>Iniciar Sem Depuração na barra de menus ou Ctrl+F5 usando o teclado.
  • Considere os diferentes tipos de memória para entender quanta memória é realmente usada pelo .NET. Em geral, os desenvolvedores inspecionam o uso de memória do aplicativo no Gerenciador de Tarefas no sistema operacional Windows, que normalmente oferece um limite superior da memória real em uso. Para obter mais informações, confira os seguintes artigos:

Uso de memória aplicado a Blazor

Calculamos a memória usada pelo blazor da seguinte maneira:

(Circuitos ativos × memória por circuito) + (Circuitos desconectados × Memória por circuito)

A quantidade de memória que um circuito usa e os circuitos ativos potenciais máximos que um aplicativo pode manter dependem em grande parte de como o aplicativo é gravado. O número máximo de circuitos ativos possíveis é descrito aproximadamente por:

Máximo de memória disponível / Memória pré-circuito = Máximo de circuitos ativos potenciais

Para que ocorra um vazamento de memória no Blazor, o seguinte deve ser verdadeiro:

  • A memória deve ser alocada pela estrutura, não pelo aplicativo. Se você alocar uma matriz de 1 GB no aplicativo, o aplicativo deverá gerenciar o descarte da matriz.
  • A memória não deve ser usada ativamente, o que significa que o circuito não está ativo e foi removido do cache de circuitos desconectados. Se você tiver o máximo de circuitos ativos em execução, ficar sem memória será um problema de escala, não um vazamento de memória.
  • Uma GC (coleta de lixo) para a geração de GC do circuito foi executada, mas o coletor de lixo não conseguiu reivindicar o circuito porque outro objeto na estrutura está mantendo uma forte referência ao circuito.

Em outros casos, não há perda de memória. Se o circuito estiver ativo (conectado ou desconectado), o circuito ainda estará em uso.

Se uma coleção para a geração de GC do circuito não for executada, a memória não será liberada porque o coletor de lixo não precisará liberar a memória nesse momento.

Se uma coleção de uma geração de GC for executada e liberar o circuito, você deverá validar a memória em relação às estatísticas do GC, não ao processo, pois o .NET pode decidir manter a memória virtual ativa.

Se a memória não for liberada, você deverá encontrar um circuito que não esteja ativo ou desconectado e com raiz por outro objeto na estrutura. Em qualquer outro caso, a incapacidade de liberar memória é um problema de aplicativo no código do desenvolvedor.

Reduzir o uso de memória

Adote qualquer uma das seguintes estratégias para reduzir o uso de memória de um aplicativo:

  • Limite a quantidade total de memória usada pelo processo .NET. Para obter mais informações, consulte Opções de configuração de runtime para coleta de lixo.
  • Reduza o número de circuitos desconectados.
  • Reduza o tempo em que um circuito tem permissão para estar no estado desconectado.
  • Dispare uma coleta de lixo manualmente para executar uma coleta durante períodos de tempo de inatividade.
  • Configure a coleta de lixo no modo estação de trabalho, que dispara agressivamente a coleta de lixo, em vez do modo servidor.

Tamanho de heap para alguns navegadores de dispositivos móveis

Ao criar um Blazor aplicativo que é executado no cliente e direcionado a navegadores de dispositivos móveis, especialmente Safari no iOS, pode ser necessário diminuir a memória máxima do aplicativo com a propriedade MSBuild EmccMaximumHeapSize. Para obter mais informações, confira Hospedar e implantar Blazor WebAssembly do ASP.NET Core.

Ações e considerações adicionais

  • Capture um despejo de memória do processo quando as demandas de memória forem altas e identificar se os objetos estão tirando mais memória e onde esses objetos estão com raiz (o que contém uma referência a eles).
  • Você pode examinar as estatísticas sobre como a memória do seu aplicativo está se comportando usando dotnet-counters. Para obter mais informações, consulte Investigar contadores de desempenho (dotnet-counters).
  • Mesmo quando uma coleta de lixo é disparada, o .NET retém a memória em vez de devolvê-la imediatamente ao sistema operacional, pois é provável que ela seja reutilizada no futuro próximo. Isso evita a confirmação e a anulação constante da memória, o que é caro. Você verá isso refletido se usar dotnet-counters, pois verá os GCs acontecerem e a quantidade de memória usada cair para 0 (zero), mas não verá o contador do conjunto de trabalho diminuir, o que é um sinal de que o .NET está mantendo a memória para reutilizá-la. Para obter mais informações sobre as configurações do arquivo de projeto (.csproj) para controlar esse comportamento, consulte Opções de configuração de runtime para coleta de lixo.
  • O GC do servidor não dispara a coleta de lixo até determinar que é absolutamente necessário fazê-lo para evitar o congelamento do aplicativo e considera que o aplicativo é a única coisa em execução no computador, portanto, pode usar toda a memória do sistema. Se o sistema tiver 50 GB, o coletor de lixo buscará usar os 50 GB completos de memória disponível antes de disparar uma coleção Gen 2.
  • Para obter informações sobre a configuração de retenção de circuito desconectada, consulte ASP.NET Core BlazorDiretrizes SignalR.

Medindo a memória

  • Publique o aplicativo na Configuração de versão.
  • Execute uma versão publicada do aplicativo.
  • Não anexar um depurador ao aplicativo em execução.
  • O disparos de uma coleção de compactação forçada Gen 2 (GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true))) libera a memória?
  • Considere se o seu aplicativo está alocando objetos no heap de objetos grandes.
  • Você está testando o crescimento da memória depois que o aplicativo é aquecido com solicitações e processamento? Normalmente, existem caches que são populados quando o código é executado pela primeira vez, adicionando uma quantidade constante de memória ao volume do aplicativo.