Compartir a través de


Hospedar e implementar aplicaciones Blazordel lado del servidor

Nota:

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión de .NET 9 de este artículo.

En este artículo se explica cómo hospedar e implementar aplicaciones de Blazor del lado servidor (Blazor Web App y aplicaciones de Blazor Server) mediante ASP.NET Core.

Valores de configuración de host

Las aplicaciones del lado del servidorBlazor pueden aceptarvalores de configuración de host genérico.

Implementación

Mediante un modelo de hospedaje del lado del servidor Blazor se ejecuta en el servidor desde una aplicación ASP.NET Core. Las actualizaciones de la interfaz de usuario, el control de eventos y las llamadas de JavaScript se controlan mediante una conexión SignalR.

Se requiere un servidor web que pueda hospedar una aplicación ASP.NET Core. Visual Studio incluye una plantilla de proyecto de aplicación del lado del servidor. Para más información sobre las plantillas de proyecto de Blazor, vea Estructura del proyecto de Blazor de ASP.NET Core.

Publique una aplicación en la configuración de versión e implemente el contenido de la carpeta bin/Release/{TARGET FRAMEWORK}/publish, donde el marcador de posición {TARGET FRAMEWORK} es la plataforma de destino.

Escalabilidad

A la hora de considerar la escalabilidad de un solo servidor (escalado vertical), la memoria disponible para una aplicación es probablemente el primer recurso que la aplicación agota a medida que la demanda de los usuarios aumenta. La memoria disponible en el servidor afecta a lo siguiente:

  • Número de circuitos activos que un servidor puede admitir.
  • Latencia de la interfaz de usuario en el cliente.

Para obtener instrucciones sobre la creación de aplicaciones del lado servidor seguras y escalables de Blazor, consulte los siguientes recursos:

Cada circuito utiliza aproximadamente 250 KB de memoria para una aplicación mínima de estilo Hola mundo. El tamaño de un circuito depende del código de la aplicación y de los requisitos de mantenimiento del estado asociados a cada componente. Se recomienda que mida la demanda de recursos durante el desarrollo de la aplicación y la infraestructura, pero la línea de base siguiente puede ser un punto de partida para planear el destino de implementación: Si espera que la aplicación admita 5000 usuarios simultáneos, considere la posibilidad de presupuestar al menos 1,3 GB de memoria del servidor en la aplicación (o ~273 KB por usuario).

Configuración de SignalR

Las condiciones de hospedaje y escalado de SignalR se aplican a aplicaciones Blazor que usan SignalR.

Para obtener más información sobre las aplicaciones de SignalR enBlazor, incluida la guía de configuración, consulte la Guía deBlazorSignalR ASP.NET Core.

Transportes

Blazor funciona mejor cuando se usa WebSockets como transporte de SignalR debido a su menor latencia, su mayor confiabilidad y su seguridad mejorada. SignalR usa el sondeo largo cuando WebSockets no está disponible o cuando la aplicación está configurada explícitamente para usarlo.

Aparece una advertencia en la consola si se utiliza el sondeo largo:

No se pudo conectar a través de WebSockets mediante el transporte de reserva de sondeo largo. Esto puede deberse a que una VPN o un proxy bloquean la conexión.

Errores de implementación global y conexión

Recomendaciones para implementaciones globales en centros de datos geográficos:

  • Implemente la aplicación en las regiones donde residen la mayoría de los usuarios.
  • Ten en cuenta la mayor latencia para el tráfico entre continentes. Para controlar la apariencia de la interfaz de usuario de reconexión, consulta Guía de ASP.NET Core BlazorSignalR.
  • Considera la posibilidad de usar Azure SignalR Service.

Azure App Service

El hospedaje de Azure App Service requiere la configuración de WebSockets y la afinidad de sesión, también llamada afinidad de enrutamiento de solicitud de aplicaciones (ARR).

Nota:

Una aplicación Blazor en Azure App Service no requiere Azure SignalR Service.

Habilita lo siguiente para el registro de la aplicación en Azure App Service:

  • WebSockets para permitir que el transporte de WebSockets funcione. El valor predeterminado es Desactivado.
  • Afinidad de sesión para enrutar las solicitudes de un usuario a la misma instancia de App Service. El valor predeterminado es Activado.
  1. En Azure Portal, ve a la aplicación web en App Services.
  2. Abre Configuración>Configuración.
  3. Establece Web Sockets en Activado.
  4. Comprueba que la Afinidad de sesión esté establecida en Activado.

Azure SignalR Service

Azure SignalR Service funciona en junto con el centro de aplicaciones SignalR para escalar una aplicación del lado del servidor a un gran número de conexiones simultáneas. Además, los centros de datos de alto rendimiento y alcance global del servicio son de gran ayuda a la hora de reducir la latencia ocasionada por la geografía.

El servicio no es necesario para las aplicaciones Blazor hospedadas en Azure App Service o Azure Container Apps, pero puede ser útil en otros entornos de hospedaje:

  • Para facilitar el escalado horizontal de la conexión.
  • Habilitar la distribución global

Nota:

La reconexión con estado (WithStatefulReconnect) se publicó con .NET 8, pero actualmente no se admite para el servicio de Azure SignalR. Para obtener más información, consulta ¿Compatibilidad con la reconexión con estado? (Azure/azure-signalr #1878).

En caso de que la aplicación use el sondeo largo o se remita al sondeo largo en lugar de WebSockets, es posible que tengas que configurar el intervalo de sondeo máximo (MaxPollIntervalInSeconds, por defecto: 5 segundos, límite 1-300 segundos), que define el intervalo de sondeo máximo permitido para las conexiones de sondeo largo en Azure SignalR Service. Si la siguiente solicitud de sondeo no llega dentro del intervalo de sondeo máximo, el servicio cierra la conexión de cliente.

Para obtener instrucciones sobre cómo agregar el servicio como una dependencia a una implementación de producción, consulta Publicación de una aplicación ASP.NET Core SignalR en Azure App Service.

Para obtener más información, consulta:

Azure Container Apps

Para una exploración más profunda del escalado de aplicaciones del lado servidor Blazor en el servicio Azure Container Apps, consulta Escalado de aplicaciones de ASP.NET Core en Azure. En el tutorial se explica cómo crear e integrar los servicios necesarios para hospedar aplicaciones en Azure Container Apps. Los pasos básicos también se proporcionan en esta sección.

  1. Configura el servicio Azure Container Apps para la afinidad de sesión siguiendo las instrucciones de Afinidad de sesión en Azure Container Apps (documentación de Azure).

  2. Debe configurarse el servicio de protección de datos ASP.NET Core para conservar las claves en una ubicación centralizada a la que puedan acceder todas las Container Instances. Las claves se pueden almacenar en Azure Blob Storage y protegerse con Azure Key Vault. El servicio de protección de datos usa las claves para deserializar componentes Razor. Para configurar el servicio de protección de datos para que use Azure Blob Storage y Azure Key Vault, haz referencia a los siguientes paquetes NuGet:

    Nota:

    Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulta los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (documentación de NuGet). Confirma las versiones correctas del paquete en NuGet.org.

  3. Actualiza Program.cs con el siguiente código resaltado:

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

    Los cambios anteriores permiten a la aplicación administrar el servicio de protección de datos mediante una arquitectura centralizada y escalable. DefaultAzureCredential detecta el objeto identity administrado de la aplicación contenedora después de implementar el código en Azure y lo usa para conectarse a Blob Storage y al almacén de claves de la aplicación.

  4. Para crear identity administrada de la aplicación contenedora y concederle acceso a Blob Storage y a un almacén de claves, completa los pasos siguientes:

    1. En Azure Portal, navega a la página de información general de la aplicación contenedora.
    2. Selecciona Conector de servicio en el panel de navegación de la izquierda.
    3. Selecciona + Crear en el panel de navegación superior.
    4. En el menú de control flotante Crear conexión, escribe los valores siguientes:
      • Contenedor: selecciona la aplicación contenedora que has creado para hospedar la aplicación.
      • Tipo de servicio: selecciona Blob Storage.
      • Suscripción: selecciona la suscripción que sea propietaria de la aplicación contenedora.
      • Nombre de conexión: escribe un nombre de scalablerazorstorage.
      • Tipo de cliente: selecciona .NET y después Siguiente.
    5. Selecciona identity administrada asignada por el sistema y, luego, Siguiente.
    6. Usa la configuración de red predeterminada y selecciona Siguiente.
    7. Una vez que Azure valide la configuración, selecciona Crear.

    Repite la configuración anterior para el almacén de claves. Selecciona el servicio y la clave adecuados del almacén de claves en la pestaña Aspectos básicos.

IIS

Al usar IIS, habilita:

Para obtener más información, consulta las instrucciones y los vínculos cruzados de recursos de IIS externos en Publicar una aplicación de ASP.NET Core en IIS.

Kubernetes

Crea una definición de entrada con las siguientes anotaciones de Kubernetes para afinidad de sesión:

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

Sigue las instrucciones para obtener una aplicación SignalR de ASP.NET Core con los siguientes cambios:

  • Cambia la ruta de acceso location de /hubroute (location /hubroute { ... }) a la ruta de acceso / (location / { ... }).
  • Quita la configuración de almacenamiento en búfer de proxy (proxy_buffering off;) porque la configuración solo se aplica a los eventos enviados por el servidor (SSE), que no son relevantes para las interacciones cliente-servidor de la aplicación Blazor.

Para obtener más información e instrucciones de configuración, consulta los recursos siguientes:

Linux con Apache

Para hospedar una aplicación de Blazor en Apache en Linux, configura ProxyPass para el tráfico HTTP y WebSockets.

En el ejemplo siguiente:

  • El servidor de Kestrel se está ejecutando en el equipo host.
  • La aplicación escucha el tráfico en el puerto 5000.
ProxyPreserveHost   On
ProxyPassMatch      ^/_blazor/(.*) http://localhost:5000/_blazor/$1
ProxyPass           /_blazor ws://localhost:5000/_blazor
ProxyPass           / http://localhost:5000/
ProxyPassReverse    / http://localhost:5000/

Habilita los siguientes módulos:

a2enmod   proxy
a2enmod   proxy_wstunnel

Busca errores de WebSockets en la consola del explorador. Errores de ejemplo:

  • Firefox no puede establecer una conexión con el servidor en ws://the-domain-name.tld/_blazor?id=XXX
  • Error: No se pudo iniciar el transporte 'WebSockets': Error: Error en el transporte.
  • Error: No se pudo iniciar el transporte 'LongPolling': Tipo de error: this.transport no definido
  • Error: No se puede conectar con el servidor con ninguno de los transportes disponibles. Error de WebSockets
  • Error: No se pueden enviar datos si la conexión no está en estado 'Conectado'.

Para obtener más información e instrucciones de configuración, consulta los recursos siguientes:

Medición de la latencia de red

La interoperabilidad de JS se puede usar para medir la latencia de red, como se muestra en el ejemplo siguiente.

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 una experiencia de interfaz de usuario razonable, se recomienda una latencia de interfaz de usuario sostenida de 250 ms o menos.

Administración de memoria

En el servidor, se crea un nuevo circuito para cada sesión de usuario. Cada sesión de usuario corresponde a la representación de un único documento en el explorador. Por ejemplo, varias pestañas crean varias sesiones.

Blazor mantiene una conexión constante con el explorador, denominado circuito, que inició la sesión. Las conexiones pueden perderse en cualquier momento por varias razones, como cuando el usuario pierde la conectividad de red o cierra bruscamente el navegador. Cuando se pierde una conexión, Blazor tiene un mecanismo de recuperación que coloca un número limitado de circuitos en un grupo "desconectado", lo que proporciona a los clientes un tiempo limitado para reconectarse y restablecer la sesión (valor predeterminado: 3 minutos).

Después de ese tiempo, Blazor libera el circuito y descarta la sesión. A partir de ese momento, el circuito es apto para la recolección de elementos no utilizados (GC) y se le reclama cuando se desencadena una recolección para la generación de GC del circuito. Un aspecto importante que hay que comprender es que los circuitos tienen una larga vida útil, lo que significa que la mayoría de los objetos con raíz en el circuito acaban llegando a Gen 2. Como resultado, es posible que no veas esos objetos liberados hasta que se produzca una recolección de Gen 2.

Medición del uso de memoria en general

Requisitos previos:

  • La aplicación debe publicarse en la configuración de versión. Las medidas de configuración de depuración no son pertinentes, ya que el código generado no es representativo del código usado para una implementación de producción.
  • La aplicación debe ejecutarse sin un depurador asociado, ya que esto también podría afectar al comportamiento de la aplicación y estropear los resultados. En Visual Studio, inicia la aplicación sin depurar seleccionando Depurar>Iniciar sin depurar en la barra de menús o Ctrl+F5 con el teclado.
  • Ten en cuenta los diferentes tipos de memoria para comprender la cantidad de memoria que realmente usa .NET. Por lo general, los desarrolladores inspeccionan el uso de memoria de la aplicación en el Administrador de tareas del sistema operativo Windows, que normalmente ofrece un límite superior de la memoria real en uso. Para obtener más información, consulta los artículos siguientes:

Uso de memoria aplicado a Blazor

Calculamos la memoria usada por blazor de la siguiente manera:

(Circuitos activos × memoria por circuito) + (circuitos desconectados × memoria por circuito)

La cantidad de memoria que usa un circuito y el máximo potencial de circuitos activos que una aplicación puede mantener depende en gran medida de cómo se escribe la aplicación. El número máximo de posibles circuitos activos se describe aproximadamente mediante:

Memoria máxima disponible / Memoria por circuito = Máximo potencial de circuitos activos

Para que se produzca una fuga de memoria en Blazor, debe cumplirse lo siguiente:

  • El marco debe asignar la memoria, no la aplicación. Si asignas una matriz de 1 GB en la aplicación, la aplicación debe administrar la eliminación de la matriz.
  • La memoria no debe usarse activamente, lo que significa que el circuito no está activo y se ha expulsado de la memoria caché de circuitos desconectados. Si tienes los circuitos activos máximos en ejecución, quedarte sin memoria es un problema de escala, no una pérdida de memoria.
  • Se ha ejecutado una recolección de elementos no utilizados (GC) para la generación de GC del circuito, pero el recolector de elementos no utilizados no ha podido reclamar el circuito porque otro objeto del marco contiene una referencia fuerte al circuito.

En otros casos, no hay pérdida de memoria. Si el circuito está activo (conectado o desconectado), el circuito todavía está en uso.

Si no se ejecuta una recolección para la generación de GC del circuito, la memoria no se libera porque el recolector de elementos no utilizados no necesita liberar la memoria en ese momento.

Si se ejecuta una recolección de una generación de GC y libera el circuito, debes validar la memoria con las estadísticas de GC, no con el proceso, ya que .NET podría decidir mantener activa la memoria virtual.

Si la memoria no se libera, debes encontrar un circuito que no esté activo o desconectado y que tenga su raíz en otro objeto del marco. En cualquier otro caso, la imposibilidad de liberar memoria es un problema de aplicación en el código del desarrollador.

Reducción del uso de memoria

Adopte cualquiera de las siguientes estrategias para reducir el uso de memoria de una aplicación:

  • Limite la cantidad total de memoria usada por el proceso de .NET. Para obtener más información, consulte Opciones de configuración de ejecución para la recolección de elementos no utilizados.
  • Reduzca el número de circuitos desconectados.
  • Reduzca el tiempo que se permite que un circuito esté en estado desconectado.
  • Desencadene manualmente una recolección de elementos no utilizados para realizar una recolección durante períodos de inactividad.
  • Configure la recolección de elementos no utilizados en modo Estación de trabajo, que desencadena agresivamente la recolección de elementos no utilizados, en lugar de en modo servidor.

Tamaño del montón para algunos exploradores de dispositivos móviles

Al compilar una aplicación Blazor que se ejecuta en el cliente y tiene como destino exploradores de dispositivos móviles, especialmente Safari en iOS, es posible que sea necesario reducir la memoria máxima de la aplicación con la propiedad MSBuild EmccMaximumHeapSize. Para obtener más información, vea Hospedaje e implementación de Blazor WebAssembly en ASP.NET Core.

Acciones y consideraciones adicionales

  • Capture un volcado de memoria del proceso cuando las demandas de memoria sean altas e identifique los objetos que consumen la mayor parte de la memoria y dónde tienen la raíz esos objetos (qué contiene una referencia a ellos).
  • Puedes examinar las estadísticas sobre cómo se comporta la memoria de la aplicación mediante dotnet-counters. Para más información, vea Investigación de los contadores de rendimiento (dotnet-counters).
  • Incluso cuando se desencadena la recolección de elementos no utilizados, .NET se mantiene en la memoria en lugar de devolverse al sistema operativo inmediatamente, ya que es probable que reutilice la memoria en un futuro próximo. Esto evita confirmar y anular la confirmación de la memoria constantemente, lo que es costoso. Lo verá reflejado si usa dotnet-counters porque verá que se produce la recolección de elementos no utilizados y la cantidad de memoria usada baja a 0 (cero), pero no verá la disminución del contador del conjunto de trabajo, que es la señal de .NET se mantiene en la memoria para reutilizarlo. Para obtener más información sobre la configuración del archivo de proyecto (.csproj) para controlar este comportamiento, consulte Opciones de configuración de ejecución para la recolección de elementos no utilizados.
  • La recolección de elementos no utilizados del servidor no desencadena recolecciones de elementos no utilizados hasta que determina que es absolutamente necesario hacerlo para evitar el bloqueo de la aplicación y considera que la aplicación es lo único que se ejecuta en la máquina, para que pueda usar toda la memoria del sistema. Si el sistema tiene 50 GB, el recolector de elementos no utilizados busca usar la memoria disponible completa de 50 GB antes de desencadenar una recolección de Gen 2.
  • Para obtener información sobre la configuración de retención de circuitos desconectados, vea las instrucciones de ASP.NET Core BlazorSignalR.

Medición de la memoria

  • Publique la aplicación en Configuración de versión.
  • Ejecute una versión publicada de la aplicación.
  • No adjunte el depurador a la aplicación en ejecución.
  • ¿El desencadenamiento forzado de Gen 2 compacta la colección (GC.Collect(2, GCCollectionMode.Aggressive | GCCollectionMode.Forced, blocking: true, compacting: true)) libera la memoria?
  • Considere si la aplicación asigna objetos en el montón de objetos grandes.
  • ¿Va a probar el crecimiento de memoria después de que la aplicación se active con las solicitudes y el procesamiento? Normalmente, hay cachés que se rellenan cuando el código se ejecuta por primera vez que agregan una cantidad constante de memoria a la superficie de la aplicación.