Condividi tramite


Ospitare e distribuire app lato Blazor server

Nota

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avviso

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Questo articolo illustra come ospitare e distribuire app lato Blazor server (Blazor Web Apps e Blazor Server app) usando ASP.NET Core.

Valori di configurazione dell'host

Le app lato Blazor server possono accettare valori di configurazione host generici.

Distribuzione

Usando un modello di hosting lato server, Blazor viene eseguito nel server dall'interno di un'app ASP.NET Core. Gli aggiornamenti dell'interfaccia utente, la gestione degli eventi e le chiamate JavaScript vengono gestiti tramite una SignalR connessione.

È necessario un server Web in grado di ospitare un'app ASP.NET Core. Visual Studio include un modello di progetto app sul lato server. Per altre informazioni sui Blazor modelli di progetto, vedere ASP.NET struttura del progetto CoreBlazor.

Pubblicare un'app nella configurazione della versione e distribuire il contenuto della bin/Release/{TARGET FRAMEWORK}/publish cartella, in cui il {TARGET FRAMEWORK} segnaposto è il framework di destinazione.

Scalabilità

Quando si considera la scalabilità di un singolo server (aumento delle prestazioni), è probabile che la memoria disponibile per un'app sia la prima risorsa esaurita dall'app man mano che l'utente richiede un aumento. La memoria disponibile nel server influisce su:

  • Numero di circuiti attivi che un server può supportare.
  • Latenza dell'interfaccia utente nel client.

Per indicazioni sulla creazione di app lato Blazor server sicure e scalabili, vedere le risorse seguenti:

Ogni circuito usa circa 250 KB di memoria per un'app di tipo Hello World minima. Le dimensioni di un circuito dipendono dal codice dell'app e dai requisiti di manutenzione dello stato associati a ogni componente. Ti consigliamo di misurare le richieste di risorse durante lo sviluppo per l'app e l'infrastruttura, ma la baseline seguente può essere un punto di partenza nella pianificazione della destinazione di distribuzione: se prevedi che l'app supporti 5.000 utenti simultanei, valuta la possibilità di budget di almeno 1,3 GB di memoria server per l'app (o circa 273 KB per utente).

Configurazione SignalR

SignalRLe condizioni di hosting e ridimensionamento si applicano alle Blazor app che usano SignalR.

Per altre informazioni sulle app, incluse le linee guida sulla SignalR configurazione, vedere ASP.NET Linee guida di baseBlazorSignalR.Blazor

Trasporti

Blazor funziona meglio quando si usano WebSocket come SignalR trasporto a causa di una latenza inferiore, maggiore affidabilità e maggiore sicurezza. Il polling lungo viene usato da SignalR quando WebSocket non è disponibile o quando l'app è configurata in modo esplicito per l'uso del polling lungo.

Viene visualizzato un avviso della console se viene utilizzato long polling:

Impossibile connettersi tramite WebSocket, utilizzando il trasporto di fallback di polling lungo. Ciò può essere dovuto a una VPN o a un proxy che blocca la connessione.

Errori di distribuzione e connessione globali

Raccomandazioni per le distribuzioni globali nei data center geografici:

  • Distribuire l'app nelle aree in cui risiede la maggior parte degli utenti.
  • Prendere in considerazione l'aumento della latenza per il traffico in tutti i continenti. Per controllare l'aspetto dell'interfaccia utente di riconnessione, vedere ASP.NET Linee guida di baseBlazorSignalR.
  • Prendere in considerazione l'uso del servizio di AzureSignalR.

Servizio app di Azure

L'hosting nel servizio app Azure richiede la configurazione per WebSocket e l'affinità di sessione, detta anche affinità ARR (Application Request Routing).

Nota

Un'app Blazor nel servizio app Azure non richiede il servizio di AzureSignalR.

Abilitare quanto segue per la registrazione dell'app nel servizio app Azure:

  • WebSocket per consentire il funzionamento del trasporto WebSocket. L'impostazione predefinita è Disattivata.
  • Affinità di sessione per instradare le richieste da un utente alla stessa istanza di servizio app. L'impostazione predefinita è .
  1. Nella portale di Azure passare all'app Web in servizio app s.
  2. Aprire Configurazione delle impostazioni>.
  3. Impostare Web Sockets su .
  4. Verificare che l'affinità di sessione sia impostata su .

Servizio di Azure SignalR

Il servizio di Azure SignalR facoltativo funziona insieme all'hub dell'app SignalR per aumentare le prestazioni di un'app lato server a un numero elevato di connessioni simultanee. Inoltre, la copertura globale del servizio e i data center ad alte prestazioni aiutano significativamente a ridurre la latenza a causa della geografia.

Il servizio non è necessario per Blazor le app ospitate nel servizio app Azure o nelle app di Azure Container, ma può essere utile in altri ambienti di hosting:

  • Per facilitare la scalabilità orizzontale delle connessioni.
  • Gestire la distribuzione globale.

Nota

La riconnessione con stato (WithStatefulReconnect) è stata rilasciata con .NET 8, ma non è attualmente supportata per il servizio di Azure SignalR . Per altre informazioni, vedere Supporto della riconnessione con stato? (Azure/azure-signalr n. 1878)..

Se l'app usa il polling lungo o esegue il fallback a Polling lungo anziché a WebSocket, potrebbe essere necessario configurare l'intervallo di polling massimo (MaxPollIntervalInSecondsimpostazione predefinita: 5 secondi, limite: 1-300 secondi), che definisce l'intervallo di polling massimo consentito per le connessioni di polling lungo nel servizio di Azure SignalR . Se la richiesta di polling successiva non arriva entro l'intervallo massimo di polling, il servizio chiude la connessione client.

Per indicazioni su come aggiungere il servizio come dipendenza a una distribuzione di produzione, vedere Pubblicare un'app ASP.NET Core SignalR per app Azure Servizio.

Per altre informazioni, vedi:

App contenitore di Azure

Per un'esplorazione più approfondita del ridimensionamento delle app lato Blazor server nel servizio App Contenitore di Azure, vedere Ridimensionamento delle app di base ASP.NET in Azure. L'esercitazione illustra come creare e integrare i servizi necessari per ospitare app in App Azure Container. In questa sezione vengono forniti anche i passaggi di base.

  1. Configurare il servizio App Contenitore di Azure per l'affinità di sessione seguendo le indicazioni riportate in Affinità di sessione in App Azure Container (documentazione di Azure).

  2. Il servizio Protezione dati di base (DP) ASP.NET deve essere configurato per rendere persistenti le chiavi in una posizione centralizzata a cui tutte le istanze del contenitore possono accedere. Le chiavi possono essere archiviate in Archiviazione BLOB di Azure e protette con Azure Key Vault. Il servizio Device Provisioning usa le chiavi per deserializzare Razor i componenti. Per configurare il servizio Device Provisioning per l'uso di Archiviazione BLOB di Azure e Azure Key Vault, fare riferimento ai pacchetti NuGet seguenti:

    Nota

    Per indicazioni sull'aggiunta di pacchetti alle app .NET, vedere gli articoli sotto Installare e gestire pacchetti in Flusso di lavoro dell'utilizzo di pacchetti (documentazione di NuGet). Confermare le versioni corrette del pacchetto all'indirizzo NuGet.org.

  3. Eseguire l'aggiornamento Program.cs con il codice evidenziato seguente:

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

    Le modifiche precedenti consentono all'app di gestire il servizio Device Provisioning usando un'architettura centralizzata e scalabile. DefaultAzureCredential individua l'app contenitore gestita identity dopo la distribuzione del codice in Azure e la usa per connettersi all'archiviazione BLOB e all'insieme di credenziali delle chiavi dell'app.

  4. Per creare l'app contenitore gestita identity e concedergli l'accesso all'archiviazione BLOB e a un insieme di credenziali delle chiavi, completare la procedura seguente:

    1. Nel portale di Azure passare alla pagina di panoramica dell'app contenitore.
    2. Selezionare Service Connector (Connettore di servizi) nel riquadro di spostamento sinistro.
    3. Selezionare + Crea nella barra di spostamento superiore.
    4. Nel menu a comparsa Crea connessione immettere i valori seguenti:
      • Contenitore: selezionare l'app contenitore creata per ospitare l'app.
      • Tipo di servizio: selezionare Archiviazione BLOB.
      • Sottoscrizione: selezionare la sottoscrizione proprietaria dell'app contenitore.
      • Nome connessione: immettere un nome .scalablerazorstorage
      • Tipo di client: selezionare .NET e quindi selezionare Avanti.
    5. Selezionare Sistema gestito identity assegnato dal sistema e selezionare Avanti.
    6. Usare le impostazioni di rete predefinite e selezionare Avanti.
    7. Dopo che Azure convalida le impostazioni, selezionare Crea.

    Ripetere le impostazioni precedenti per l'insieme di credenziali delle chiavi. Selezionare il servizio e la chiave dell'insieme di credenziali delle chiavi appropriati nella scheda Informazioni di base .

IIS

Quando si usa IIS, abilitare:

Per altre informazioni, vedere le linee guida e i collegamenti incrociati delle risorse IIS esterne in Pubblicare un'app ASP.NET Core in IIS.

Kubernetes

Creare una definizione di ingresso con le annotazioni Kubernetes seguenti per l'affinità di sessione:

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 con Nginx

Seguire le indicazioni per un'app ASP.NET Core SignalR con le modifiche seguenti:

  • Modificare il location percorso da /hubroute (location /hubroute { ... }) al percorso / radice (location / { ... }).
  • Rimuovere la configurazione per il buffering proxy (proxy_buffering off;) perché l'impostazione si applica solo agli eventi inviati dal server (SSE), che non sono rilevanti per Blazor le interazioni client-server dell'app.

Per altre informazioni e indicazioni sulla configurazione, vedere le risorse seguenti:

Linux con Apache

Per ospitare un'app Blazor dietro Apache in Linux, configurare ProxyPass per il traffico HTTP e WebSocket.

Nell'esempio seguente :

  • Kestrel server è in esecuzione nel computer host.
  • L'app rimane in ascolto del traffico sulla 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/

Abilitare i moduli seguenti:

a2enmod   proxy
a2enmod   proxy_wstunnel

Controllare la presenza di errori webSocket nella console del browser. Errori di esempio:

  • Firefox non riesce a stabilire una connessione al server all'indirizzo ws://the-domain-name.tld/_blazor?id=XXX
  • Errore: Impossibile avviare il trasporto 'WebSockets': Errore: Errore: Errore: errore con il trasporto.
  • Errore: Impossibile avviare il trasporto 'LongPolling': TypeError: this.transport non è definito
  • Errore: impossibile connettersi al server con uno dei trasporti disponibili. WebSocket non riuscito
  • Errore: impossibile inviare dati se la connessione non è nello stato "Connesso".

Per altre informazioni e indicazioni sulla configurazione, vedere le risorse seguenti:

Misurare la latenza di rete

JS l'interoperabilità può essere usata per misurare la latenza di rete, come illustrato nell'esempio seguente.

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

Per un'esperienza di interfaccia utente ragionevole, è consigliabile una latenza dell'interfaccia utente sostenuta di 250 ms o meno.

Gestione della memoria

Nel server viene creato un nuovo circuito per ogni sessione utente. Ogni sessione utente corrisponde al rendering di un singolo documento nel browser. Ad esempio, più schede creano più sessioni.

Blazor mantiene una connessione costante al browser, denominata circuito, che ha avviato la sessione. Le connessioni possono andare perse in qualsiasi momento per diversi motivi, ad esempio quando l'utente perde la connettività di rete o chiude bruscamente il browser. Quando una connessione viene persa, Blazor dispone di un meccanismo di ripristino che inserisce un numero limitato di circuiti in un pool "disconnesso", offrendo ai client una quantità limitata di tempo per riconnettersi e ristabilire la sessione (impostazione predefinita: 3 minuti).

Successivamente, Blazor rilascia il circuito ed elimina la sessione. Da questo punto in poi, il circuito è idoneo per l'operazione di Garbage Collection (GC) e viene richiesto quando viene attivata una raccolta per la generazione GC del circuito. Un aspetto importante da comprendere è che i circuiti hanno una lunga durata, il che significa che la maggior parte degli oggetti radicati dal circuito alla fine raggiunge la generazione 2. Di conseguenza, è possibile che tali oggetti non vengano rilasciati fino a quando non si verifica una raccolta di generazione 2.

Misurare l'utilizzo della memoria in generale

Prerequisiti:

  • L'app deve essere pubblicata nella configurazione della versione . Le misurazioni della configurazione di debug non sono rilevanti, perché il codice generato non è rappresentativo del codice usato per una distribuzione di produzione.
  • L'app deve essere eseguita senza un debugger collegato, perché ciò potrebbe influire anche sul comportamento dell'app e rovinare i risultati. In Visual Studio avviare l'app senza eseguire il debug selezionando Avvia debug>senza eseguire debug dalla barra dei menu o CTRL+F5 usando la tastiera.
  • Prendere in considerazione i diversi tipi di memoria per comprendere la quantità di memoria effettivamente usata da .NET. In genere, gli sviluppatori controllano l'utilizzo della memoria delle app in Gestione attività nel sistema operativo Windows, che in genere offre un limite superiore della memoria effettiva in uso. Per altre informazioni, vedere gli articoli seguenti:

Utilizzo della memoria applicato a Blazor

La memoria usata da blazor viene calcolata come segue:

(Circuiti attivi × memoria per circuito) + (circuiti disconnessi × memoria per circuito)

La quantità di memoria usata da un circuito e il massimo potenziale circuito attivo che un'app può gestire dipende in gran parte dal modo in cui viene scritta l'app. Il numero massimo di possibili circuiti attivi è descritto approssimativamente da:

Numero massimo di circuiti attivi per memoria disponibile / per circuito =

Affinché si verifichi una perdita di memoria in Blazor, è necessario che sia true quanto segue:

  • La memoria deve essere allocata dal framework, non dall'app. Se si alloca una matrice di 1 GB nell'app, l'app deve gestire l'eliminazione della matrice.
  • La memoria non deve essere usata attivamente, il che significa che il circuito non è attivo ed è stato rimosso dalla cache dei circuiti disconnessi. Se sono in esecuzione i circuiti attivi massimi, l'esaurimento della memoria è un problema di scalabilità, non una perdita di memoria.
  • È stata eseguita una Garbage Collection (GC) per la generazione GC del circuito, ma il Garbage Collector non è riuscito a richiedere il circuito perché un altro oggetto nel framework contiene un riferimento sicuro al circuito.

In altri casi, non c'è perdita di memoria. Se il circuito è attivo (connesso o disconnesso), il circuito è ancora in uso.

Se non viene eseguita una raccolta per la generazione GC del circuito, la memoria non viene rilasciata perché il Garbage Collector non deve liberare la memoria in quel momento.

Se una raccolta per una generazione GC viene eseguita e libera il circuito, è necessario convalidare la memoria rispetto alle statistiche GC, non il processo, in quanto .NET potrebbe decidere di mantenere attiva la memoria virtuale.

Se la memoria non viene liberata, è necessario trovare un circuito che non sia attivo o disconnesso e che sia rooted da un altro oggetto nel framework. In qualsiasi altro caso, l'impossibilità di liberare memoria è un problema dell'app nel codice per sviluppatori.

Ridurre l'utilizzo della memoria

Adottare una delle strategie seguenti per ridurre l'utilizzo della memoria di un'app:

  • Limitare la quantità totale di memoria usata dal processo .NET. Per altre informazioni, vedere Opzioni di configurazione del runtime per Garbage Collection.
  • Ridurre il numero di circuiti disconnessi.
  • Ridurre il tempo in cui un circuito può trovarsi nello stato disconnesso.
  • Attivare manualmente un'operazione di Garbage Collection per eseguire una raccolta durante i periodi di inattività.
  • Configurare l'operazione di Garbage Collection in modalità workstation, che attiva in modo aggressivo la Garbage Collection anziché la modalità Server.

Dimensioni dell'heap per alcuni browser per dispositivi mobili

Quando si compila un'app Blazor eseguita nel client e si rivolge ai browser per dispositivi mobili, in particolare Safari in iOS, è possibile che sia necessaria una riduzione della memoria massima per l'app con la proprietà EmccMaximumHeapSize MSBuild. Per altre informazioni, vedere Ospitare e distribuire ASP.NET Core Blazor WebAssembly.

Azioni e considerazioni aggiuntive

  • Acquisire un dump di memoria del processo quando le richieste di memoria sono elevate e identificare gli oggetti che stanno occupando la maggior parte della memoria e dove si trovano tali oggetti sono rooted (ciò che contiene un riferimento a tali oggetti).
  • È possibile esaminare le statistiche sul comportamento della memoria nell'app usando dotnet-counters. Per altre informazioni, vedere Analizzare i contatori delle prestazioni (dotnet-counters).
  • Anche quando viene attivato un GC, .NET mantiene la memoria invece di restituirla immediatamente al sistema operativo, perché è probabile che riutilizzi la memoria nel prossimo futuro. In questo modo si evita il commit e il decommettimento costante della memoria, che è costoso. Si noterà che ciò si riflette se si usa dotnet-counters perché si noterà che i controller di dominio vengono eseguiti e la quantità di memoria usata scende a 0 (zero), ma non si noterà la riduzione del contatore del set di lavoro, ovvero il segno che .NET è in possesso della memoria per riutilizzarla. Per altre informazioni sulle impostazioni del file di progetto (.csproj) per controllare questo comportamento, vedere Opzioni di configurazione del runtime per Garbage Collection.
  • Server GC non attiva Garbage Collection finché non determina che è assolutamente necessario farlo per evitare di bloccare l'app e considera che l'app è l'unica cosa in esecuzione nel computer, in modo che possa usare tutta la memoria nel sistema. Se il sistema ha 50 GB, il Garbage Collector cerca di usare l'intero 50 GB di memoria disponibile prima di attivare una raccolta di generazione 2.
  • Per informazioni sulla configurazione di conservazione dei circuiti disconnessi, vedere ASP.NET Linee guida di baseBlazorSignalR.

Misurazione della memoria

  • Pubblicare l'app nella configurazione della versione.
  • Eseguire una versione pubblicata dell'app.
  • Non collegare un debugger all'app in esecuzione.
  • L'attivazione di una raccolta forzata di generazione 2 compattazione (GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true)) liberare la memoria?
  • Valutare se l'app sta allocando oggetti nell'heap di oggetti di grandi dimensioni.
  • Si sta testando la crescita della memoria dopo che l'app viene riscaldata con richieste ed elaborazione? In genere, sono presenti cache popolate durante l'esecuzione del codice per la prima volta che aggiungono una quantità costante di memoria al footprint dell'app.