Partage via


Héberger et déployer des applications Blazor côté serveur

Remarque

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Avertissement

Cette version d’ASP.NET Core n’est plus prise en charge. Pour plus d’informations, consultez la Stratégie de prise en charge de .NET et .NET Core. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Cet article explique comment héberger et déployer des applications Blazor côté serveur (applications Blazor Web App et Blazor Server) avec ASP.NET Core.

Valeurs de configuration de l’hôte

Les applications Blazor côté serveur peuvent accepter les valeurs de configuration d’hôte générique.

Déploiement

En utilisant un modèle d’hébergement côté serveur, Blazor est exécutée sur le serveur à partir d’une application ASP.NET Core. Les mises à jour de l’interface utilisateur, la gestion des événements et les appels JavaScript sont gérés par le biais d’une connexion SignalR.

Un serveur web capable d’héberger une application ASP.NET Core est nécessaire. Visual Studio inclut un modèle de projet d’application côté serveur. Pour plus d’informations sur les modèles de projet Blazor, consultez ASP.NET structure de projet principale Blazor.

Publiez une application dans la configuration Release et déployez le contenu du dossier bin/Release/{TARGET FRAMEWORK}/publish, où l’espace réservé {TARGET FRAMEWORK} est le framework cible.

Évolutivité

Lorsque vous envisagez la scalabilité d’un serveur unique (scale-up), la mémoire disponible pour une application est probablement la première ressource que l’application épuise à mesure que les demandes des utilisateurs augmentent. La mémoire disponible sur le serveur affecte :

  • Le nombre de circuits actifs qu’un serveur peut prendre en charge.
  • La latence de l’interface utilisateur sur le client.

Pour obtenir des conseils sur la génération d’applications côté Blazor serveur sécurisées et évolutives, consultez les ressources suivantes :

Chaque circuit utilise environ 250 Ko de mémoire pour une application de style Hello World minimale. La taille d’un circuit dépend du code de l’application et des exigences de maintenance d’état associées à chaque composant. Nous vous recommandons de mesurer les demandes de ressources pendant le développement de votre application et de votre infrastructure, mais la base de référence suivante peut être un point de départ dans la planification de votre cible de déploiement : si vous prévoyez que votre application prenne en charge 5 000 utilisateurs simultanés, envisagez de budgéter au moins 1,3 Go de mémoire serveur pour l’application (ou environ 273 Ko par utilisateur).

Configuration SignalR

Les conditions d’hébergement et de mise à l’échelle de SignalR s’appliquent aux applications Blazor qui utilisent SignalR.

Pour plus d’informations sur les applications SignalR intégrées sur Blazor, y compris des conseils de configuration, consultez les conseils BlazorSignalR ASP.NET Core.

Transports

Blazor fonctionne le mieux lors de l’utilisation de WebSockets en tant que transport SignalR en raison d’une latence plus faible, d’une meilleure fiabilité et d’une sécurité améliorée. L’interrogation longue est utilisée par SignalR lorsque WebSockets n’est pas disponible ou lorsque l’application est explicitement configurée pour utiliser l’interrogation longue.

Un avertissement de console s’affiche si l’interrogation longue est utilisée :

Échec de la connexion via WebSockets, avec le transport de secours d’interrogation longue. Cela peut être dû au blocage de la connexion par un VPN ou un proxy.

Échecs de déploiement global et de connexion

Recommandations pour les déploiements globaux vers des centres de données géographiques :

  • Déployez l’application dans les régions où résident la plupart des utilisateurs.
  • Prenez en compte la latence accrue du trafic entre les continents. Pour contrôler l’apparition de l’interface utilisateur de reconnexion, consultez Aide ASP.NET Core BlazorSignalR.
  • Envisagez d’utiliser le Azure SignalR Service.

Azure App Service

L’hébergement sur Azure App Service nécessite une configuration pour WebSockets et l’affinité de session, également appelée Application Request Routing.

Remarque

Une application Blazor sur Azure App Service ne nécessite pas Azure SignalR Service.

Activez les éléments suivants pour l’inscription de l’application dans Azure App Service :

  • WebSockets pour permettre au transport WebSockets de fonctionner. La valeur par défaut est Off.
  • Affinité de session pour acheminer les requêtes d’un utilisateur vers la même instance App Service. Le paramètre par défaut est On.
  1. Dans le portail Azure, accéder à l’application web dans App Services.
  2. Ouvrez Paramètres>Configuration.
  3. Définissez Web sockets sur On.
  4. Vérifiez que l’affinité de session est définie sur On.

Service Azure SignalR

Le Azure SignalR Service fonctionne conjointement avec le hub SignalR de l’application pour effectuer le scale-out d’une application côté serveur vers un grand nombre de connexions simultanées. De plus, la portée générale du service et les centres de données hautes performances contribuent de manière significative à réduire la latence due aux emplacements géographiques.

Le service n’est pas nécessaire pour les applications Blazor hébergées dans Azure App Service ou Azure Container Apps, mais peut être utile dans d’autres environnements d’hébergement :

  • Pour faciliter le scale-out de la connexion.
  • Gérer la distribution globale

Remarque

La reconnexion avec état (WithStatefulReconnect) a été mise en production avec .NET 8, mais n’est actuellement pas prise en charge pour Azure SignalR Service. Pour plus d’informations, consultez l’article Prise en charge de la reconnexion avec état ? (Azure/azure-signalr #1878).

Dans le cas où l’application utilise l’interrogation longue ou renvient à l’interrogation longue au lieu de WebSockets, vous devrez peut-être configurer l’intervalle d’interrogation maximal (MaxPollIntervalInSeconds, par défaut : 5 secondes, limite : 1-300 secondes), qui définit l’intervalle d’interrogation maximal autorisé pour les connexions d’interrogation longue dans le Azure SignalR Service. Si la requête d’interrogation suivante n’arrive pas dans l’intervalle d’interrogation maximal, le service ferme la connexion cliente.

Pour obtenir des conseils sur l’ajout du service en tant que dépendance à un déploiement de production, consultez Publier une application SignalR ASP.NET Core sur Azure App Service.

Pour plus d’informations, consultez l’article suivant :

Azure Container Apps

Pour une exploration plus approfondie de la mise à l’échelle des applications Blazor côté serveur sur le service Azure Container Apps, consultez Mise à l’échelle des applications ASP.NET Core sur Azure. Le tutoriel explique comment créer et intégrer les services nécessaires pour héberger des applications sur Azure Container Apps. Les étapes de base sont également fournies dans cette section.

  1. Configurez le service Azure Container Apps pour l’affinité de session en suivant les instructions fournies dans Affinité de session dans Azure Container Apps (documentation Azure).

  2. Le service de protection des données (DP) ASP.NET Core doit être configuré pour conserver les clés dans un emplacement centralisé auquel toutes les instances de conteneur peuvent accéder. Les clés peuvent être stockées dans Stockage Blob Azure et protégées avec Azure Key Vault. Le service de protection des données utilise les clés pour désérialiser les composants Razor. Pour configurer le service de protection des données afin qu’il utilise Stockage Blob Azure et Azure Key Vault, référencez les packages NuGet suivants :

    Remarque

    Pour obtenir des conseils sur l’ajout de packages à des applications .NET, consultez les articles figurant sous Installer et gérer des packages dans Flux de travail de la consommation des packages (documentation NuGet). Vérifiez les versions du package sur NuGet.org.

  3. Mettez à jour Program.cs avec le code mis en évidence suivant :

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

    Les modifications précédentes permettent à l’application de gérer la protection des données à l’aide d’une architecture centralisée et évolutive. DefaultAzureCredential détecte l’identity managée de l’application conteneur après le déploiement du code sur Azure et l’utilise pour se connecter au stockage blob et au coffre de clés de l’application.

  4. Pour créer l’identity managée de l’application conteneur et lui accorder l’accès au stockage blob et à un coffre de clés, procédez comme suit :

    1. Dans le portail Azure, accédez à la page de vue d’ensemble de l’application conteneur.
    2. Dans la navigation de gauche, sélectionnez Connecteur de services.
    3. Sélectionnez + Créer dans la navigation supérieure.
    4. Dans le menu volant Créer une connexion, entrez les valeurs suivantes :
      • Conteneur : sélectionnez l’application conteneur que vous avez créée pour héberger votre application.
      • Type de service : sélectionnez Stockage Blob.
      • Abonnement : sélectionnez l’abonnement qui possède l’application conteneur.
      • Nom de la connexion : entrez un nom de scalablerazorstorage.
      • Type de client : sélectionnez .NET puis Suivant.
    5. Sélectionnez identity managée affectée par le système, puis Suivant.
    6. Utilisez les paramètres réseau par défaut et sélectionnez Suivant.
    7. Une fois qu’Azure a validé les paramètres, sélectionnez Créer.

    Répétez les paramètres précédents pour le coffre de clés. Sélectionnez le service de coffre de clés et la clé appropriés dans l’onglet De base.

IIS

Lorsque vous utilisez IIS, activez :

Pour plus d’informations, consultez les conseils et les liens croisés des ressources IIS externes dans Publier une application ASP.NET Core sur IIS.

Kubernetes

Créez une définition d’entrée avec les annotations Kubernetes suivantes pour l’affinité de session :

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

Suivez les instructions pour une application ASP.NET Core SignalR avec les modifications suivantes :

  • Remplacez le chemin d’accès location de /hubroute (location /hubroute { ... }) par le chemin racine / (location / { ... }).
  • Supprimez la configuration de la mise en mémoire tampon du proxy (proxy_buffering off;), car le paramètre s’applique uniquement aux événements envoyés par le serveur (SSE), qui ne sont pas pertinents pour les interactions client-serveur de l’applicationBlazor.

Pour plus d’informations et pour obtenir de l’aide sur la configuration, consultez les ressources suivantes :

Linux avec Apache

Pour héberger une application Blazor derrière Apache sur Linux, configurez ProxyPass le trafic HTTP et WebSockets.

Dans l’exemple suivant :

  • Kestrel le serveur s’exécute sur l’ordinateur hôte.
  • L’application écoute le trafic sur le port 5000.
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

Activez les modules suivants :

a2enmod   proxy
a2enmod   proxy_wstunnel

Vérifiez les erreurs WebSockets dans la console du navigateur. Exemples d’erreurs :

  • Firefox ne peut pas établir de connexion au serveur à l’adresse ws://the-domain-name.tld/_blazor?id=XXX
  • Erreur : échec du démarrage du transport « WebSockets » : Erreur : une erreur s’est produite avec le transport.
  • Erreur : échec du démarrage du transport « Interrogation longue » : TypeError : this.transport is undefined
  • Erreur : impossible de se connecter au serveur avec l’un des transports disponibles. Échec des WebSockets
  • Erreur : impossible d’envoyer des données si la connexion n’est pas dans l’état « Connecté ».

Pour plus d’informations et pour obtenir de l’aide sur la configuration, consultez les ressources suivantes :

Mesurer la latence réseau

JS L’interopérabilité peut être utilisée pour mesurer la latence du réseau, comme le montre l’exemple suivant.

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

Pour une expérience d’interface utilisateur raisonnable, nous recommandons une latence soutenue de l’interface utilisateur de 250 ms ou moins.

Gestion de la mémoire

Sur le serveur, un nouveau circuit est créé pour chaque session utilisateur. Chaque session utilisateur correspond au rendu d’un seul document dans le navigateur. Par exemple, plusieurs onglets créent plusieurs sessions.

Blazor maintient une connexion constante au navigateur, appelé circuit, qui a lancé la session. Les connexions peuvent être perdues à tout moment pour plusieurs raisons, par exemple lorsque l’utilisateur perd la connectivité réseau ou ferme brusquement le navigateur. En cas de perte de connexion, Blazor dispose d’un mécanisme de récupération qui place un nombre limité de circuits dans un pool « déconnecté », ce qui donne aux clients un délai limité pour se reconnecter et rétablir la session (par défaut : 3 minutes).

Après ce délai, Blazor libère le circuit et ignore la session. À partir de ce moment, le circuit est éligible pour le garbage collection (GC) et est revendiqué lorsqu’une collecte pour la génération GC du circuit est déclenchée. Un aspect important à comprendre est que les circuits ont une longue durée de vie, ce qui signifie que la plupart des objets enracinés par le circuit atteignent finalement Gen 2. Par conséquent, il se peut que vous ne voyiez pas ces objets libérés tant qu’une collection Gen 2 n’a pas été effectuée.

Mesurer l’utilisation de la mémoire en général

Conditions préalables :

  • L’application doit être publiée dans la configuration Release. Les mesures de configuration de débogage ne sont pas pertinentes, car le code généré n’est pas représentatif du code utilisé pour un déploiement de production.
  • L’application doit s’exécuter sans débogueur attaché, car cela peut également affecter le comportement de l’application et gâcher les résultats. Dans Visual Studio, démarrez l’application sans débogage en sélectionnant>Démarrer sans débogage dans la barre de menus ou Ctrl+F5 à l’aide du clavier.
  • Considérez les différents types de mémoire pour comprendre la quantité de mémoire réellement utilisée par .NET. En règle générale, les développeurs inspectent l’utilisation de la mémoire des applications dans le Gestionnaire des tâches sur le système d’exploitation Windows, qui offre généralement une limite supérieure de la mémoire réelle utilisée. Pour plus d’informations, consultez les articles suivants :

Utilisation de la mémoire appliquée à Blazor

Nous calculons la mémoire utilisée par blazor comme suit :

(Circuits actifs × mémoire par circuit) + (Circuits déconnectés × mémoire par circuit)

La quantité de mémoire utilisée par un circuit et le nombre maximal de circuits actifs potentiels qu’une application peut gérer dépendent en grande partie de la façon dont l’application est écrite. Le nombre maximal de circuits actifs possibles est approximativement décrit par :

Mémoire / disponible maximale Mémoire = par circuit Nombre maximal de circuits actifs potentiels

Pour qu’une fuite de mémoire se produise dans Blazor, les éléments suivants doivent être vrais :

  • La mémoire doit être allouée par l’infrastructure, pas par l’application. Si vous allouez un tableau de 1 Go dans l’application, l’application doit gérer la suppression du tableau.
  • La mémoire ne doit pas être utilisée activement, ce qui signifie que le circuit n’est pas actif et a été supprimé du cache des circuits déconnectés. Si le nombre maximal de circuits actifs est en cours d’exécution, le manque de mémoire est un problème de mise à l’échelle, et non une fuite de mémoire.
  • Un garbage collection (GC) pour la génération GC du circuit a été exécuté, mais le garbage collector n’a pas pu revendiquer le circuit, car un autre objet de l’infrastructure contient une référence forte au circuit.

Dans d’autres cas, il n’y a pas de fuite de mémoire. Si le circuit est actif (connecté ou déconnecté), il est toujours en cours d’utilisation.

Si une collection pour la génération GC du circuit ne s’exécute pas, la mémoire n’est pas libérée, car le garbage collector n’a pas besoin de libérer la mémoire à ce moment-là.

Si une collection pour une génération GC s’exécute et libère le circuit, vous devez valider la mémoire par rapport aux statistiques GC, et non au processus, car .NET peut décider de maintenir la mémoire virtuelle active.

Si la mémoire n’est pas libérée, vous devez trouver un circuit qui n’est ni actif ni déconnecté et qui est enraciné par un autre objet dans l’infrastructure. Dans tous les autres cas, l’impossibilité de libérer de la mémoire est un problème d’application dans le code du développeur.

Réduisez l’utilisation de la mémoire

Adoptez l’une des stratégies suivantes pour réduire l’utilisation de la mémoire d’une application :

  • Limitez la quantité totale de mémoire utilisée par le processus .NET. Pour plus d’informations, consultez Options de configuration d’exécution pour le garbage collection.
  • Réduisez le nombre de circuits déconnectés.
  • Réduisez le temps pendant lequel un circuit est autorisé à être à l’état déconnecté.
  • Déclenchez un garbage collection manuellement pour effectuer une collecte pendant les périodes d’arrêt.
  • Configurez le garbage collection en mode Station de travail, qui déclenche de manière agressive le garbage collection, au lieu du mode Serveur.

Taille du tas pour certains navigateurs d’appareils mobiles

Lors de la création d’une application Blazor qui s’exécute sur le client et cible les navigateurs d’appareils mobiles, en particulier Safari sur iOS, il peut être nécessaire de diminuer la mémoire maximale pour l’application avec la propriété MSBuild EmccMaximumHeapSize. Pour plus d’informations, consultez Héberger et déployer ASP.NET Core Blazor WebAssembly.

Actions et considérations supplémentaires

  • Capturez un vidage de la mémoire du processus lorsque les besoins en mémoire sont élevés et identifiez les objets qui prennent le plus de mémoire et où ces objets sont enracinés (ce qui contient une référence à eux).
  • Vous pouvez examiner les statistiques sur le comportement de la mémoire dans votre application en utilisant dotnet-counters. Pour plus d’informations, consultez Examiner les compteurs de performances (dotnet-counters).
  • Même quand une GC (Garbage Collection) est déclenchée, .NET conserve la mémoire au lieu de la renvoyer immédiatement au système d’exploitation, car il est probable qu’il va réutiliser la mémoire dans un avenir proche. Ceci évite de commiter et de décommiter constamment la mémoire, ce qui est coûteux en ressources. Vous verrez que cela se reflète si vous utilisez dotnet-counters, car vous allez voir que les GC se produisent et que la quantité de mémoire utilisée descend à 0 (zéro), mais vous ne verrez pas le compteur de la plage de travail diminuer, ce qui est le signe que .NET conserve la mémoire pour la réutiliser. Pour plus d’informations sur les paramètres de fichier projet (.csproj) pour contrôler ce comportement, consultez Options de configuration d’exécution pour le garbage collection.
  • La GC du serveur ne déclenche pas de garbage collections tant qu’elle ne détermine pas qu’il est absolument nécessaire de le faire pour éviter de figer votre application et considère que votre application est la seule chose en cours d’exécution sur la machine : elle peut donc utiliser toute la mémoire du système. Si le système a 50 Go, le garbage collector cherche à utiliser la totalité de la mémoire disponible de 50 Go avant de déclencher une collecte Gen 2.
  • Pour plus d’informations sur la configuration de la rétention de circuit déconnecté, consultez ASP.NET Core BlazorSignalR instructions.

Mesure de la mémoire

  • Publiez l’application dans la configuration Release.
  • Exécutez une version publiée de l’application.
  • N’attachez pas un débogueur à l’application en cours d’exécution.
  • Le déclenchement d’un nettoyage de compactage forcé Gen 2 (GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true)) libère-t-il la mémoire ?
  • Déterminez si votre application alloue des objets sur le tas des grands objets.
  • Testez-vous la croissance de la mémoire une fois que l’application reçoit des requêtes et effectue des traitements ? En général, il existe des caches qui sont remplis quand le code s’exécute pour la première fois, ce qui ajoute une quantité constante de mémoire à l’empreinte de l’application.