Consideraciones sobre el diseño de aplicaciones para cargas de trabajo críticas

La arquitectura de referencia crítica de línea base ilustra una carga de trabajo altamente confiable mediante una sencilla aplicación de catálogo en línea. Los usuarios finales pueden examinar un catálogo de artículos, ver los detalles de un artículo y publicar clasificaciones y comentarios de los artículos. Este artículo se centra en los aspectos de confiabilidad y resistencia de una aplicación crítica, como el procesamiento asincrónico de solicitudes y cómo lograr un alto rendimiento en una solución.

Importante

Logotipo de GitHub La guía está respaldada por una implementación de referencia de nivel de producción que muestra el desarrollo de aplicaciones críticas en Azure. Esta implementación se puede usar como base para el desarrollo de soluciones adicionales en el primer paso hacia la producción.

Composición de la aplicación

En el caso de las aplicaciones críticas a gran escala, es esencial optimizar la arquitectura para lograr una escalabilidad y resistencia completas. Este estado se puede lograr mediante la separación de los componentes en unidades funcionales que pueden operar de forma independiente. Aplique esta separación en todos los niveles de la pila de la aplicación, lo que permite que cada parte del sistema se escale de forma independiente y satisfaga los cambios en la demanda.

En la implementación, se muestra un ejemplo de ese enfoque. La aplicación usa puntos de conexión de API sin estado, que desacoplan las solicitudes de escritura de larga duración de forma asincrónica mediante un agente de mensajería. La carga de trabajo se compone de forma que todo el clúster de AKS y otras dependencias del stamp se puedan eliminar y volver a crear en cualquier momento. Los componentes principales son:

  • Interfaz de usuario (UI): la aplicación web de página única a la que acceden los usuarios finales se hospeda en el hospedaje de sitios web estáticos de la cuenta de Azure Storage.
  • API (CatalogService): API REST llamada por la aplicación de interfaz de usuario, pero disponible para otras posibles aplicaciones cliente.
  • Rol de trabajo (BackgroundProcessor): rol de trabajo en segundo plano, que procesa las solicitudes de escritura en la base de datos escuchando nuevos eventos en el bus de mensajes. Este componente no expone ninguna API.
  • API del servicio de estado (HealthService): se usa para notificar el estado de la aplicación comprobando si funcionan los componentes críticos (base de datos, bus de mensajería).

Diagrama del flujo de la aplicación.

La API, el rol de trabajo y las aplicaciones de comprobación de estado se conocen como carga de trabajo y se hospedan como contenedores en un espacio de nombres de AKS dedicado (llamado workload). No hay ninguna comunicación directa entre los pods. Los pods no tienen estado y se pueden escalar de forma independiente.

Diagrama de la composición detallada de la carga de trabajo.

Hay otros componentes auxiliares que se ejecutan en el clúster:

  1. Controlador de entrada: se usa el controlador de entrada Nginx para enrutar las solicitudes entrantes a la carga de trabajo y el equilibrio de carga entre pods. Se expone mediante Azure Load Balancer con una dirección IP pública (pero solo se accede mediante Azure Front Door).
  2. Administrador de certificados: se usa cert-manager de Jetstack para aprovisionar automáticamente certificados SSL/TLS (con Let's Encrypt) para las reglas de entrada.
  3. Controlador de secretos CSI: el proveedor de Azure Key Vault para Secrets Store CSI se usa para leer secretos de forma segura, como las cadenas de conexión de Azure Key Vault.
  4. Agente de supervisión: se ajusta la configuración predeterminada de OMSAgent para reducir la cantidad de datos de supervisión enviados al área de trabajo de Log Analytics.

Conexión de base de datos

Debido a la naturaleza efímera de los stamps de implementación, evite conservar el estado dentro del stamp en la medida de lo posible. El estado se debe conservar en un almacén de datos externo. Para admitir el SLO de confiabilidad, ese almacén de datos debe ser resistente. Se recomienda usar servicios administrados (PaaS) combinados con bibliotecas nativas del SDK que controlan automáticamente los tiempos de espera, las desconexiones y otros estados de error.

En la implementación de referencia, Azure Cosmos DB actúa como almacén de datos principal para la aplicación. Se ha elegido Azure Cosmos DB porque proporciona escrituras en varias regiones. Cada stamp puede escribir en la réplica de Azure Cosmos DB de la misma región y Azure Cosmos DB controla internamente la replicación de datos y la sincronización entre regiones. Se usa Azure Cosmos DB for NoSQL porque admite todas las funcionalidades del motor de base de datos.

Para más información, consulta Plataforma de datos para cargas de trabajo críticas.

Nota:

Las nuevas aplicaciones deben usar Azure Cosmos DB for NoSQL. En el caso de las aplicaciones heredadas que usan otro protocolo NoSQL, evalúe la ruta de migración a Azure Cosmos DB.

Sugerencia

Para las aplicaciones críticas que priorizan la disponibilidad sobre el rendimiento, se recomiendan las escritura en una sola región y las lecturas de varias regiones con un nivel de coherencia fuerte.

En esta arquitectura, es necesario almacenar temporalmente el estado en el stamp para los puntos de control de Event Hubs. Se usa Azure Storage para ese propósito.

Todos los componentes de la carga de trabajo usan el SDK para .NET Core de Azure Cosmos DB para comunicarse con la base de datos. El SDK incluye una lógica sólida para mantener las conexiones de base de datos y controlar los errores. Estas son algunas de las principales opciones de configuración:

  • Se usa el modo de conectividad directa. Esta es la configuración predeterminada del SDK para .NET v3 porque ofrece un mejor rendimiento. Hay menos saltos de red en comparación con el modo de puerta de enlace, que usa HTTP.
  • La respuesta de devolución de contenido al escribir está deshabilitada para evitar que el cliente de Azure Cosmos DB devuelva el documento de las operaciones Create, Upsert, Patch y Replace para reducir el tráfico de red. Además, no es necesario para su posterior procesamiento en el cliente.
  • Se usa la serialización personalizada para establecer la directiva de nomenclatura de propiedades JSON en JsonNamingPolicy.CamelCase para traducir las propiedades de estilo .NET al estilo JSON estándar y viceversa. La condición de omisión predeterminada omite las propiedades con valores NULL durante la serialización (JsonIgnoreCondition.WhenWritingNull).
  • La región de la aplicación se establece en la región del stamp, lo que permite al SDK encontrar el punto de conexión más cercano (preferiblemente dentro de la misma región).
//
// /src/app/AlwaysOn.Shared/Services/CosmosDbService.cs
//
CosmosClientBuilder clientBuilder = new CosmosClientBuilder(sysConfig.CosmosEndpointUri, sysConfig.CosmosApiKey)
    .WithConnectionModeDirect()
    .WithContentResponseOnWrite(false)
    .WithRequestTimeout(TimeSpan.FromSeconds(sysConfig.ComsosRequestTimeoutSeconds))
    .WithThrottlingRetryOptions(TimeSpan.FromSeconds(sysConfig.ComsosRetryWaitSeconds), sysConfig.ComsosMaxRetryCount)
    .WithCustomSerializer(new CosmosNetSerializer(Globals.JsonSerializerOptions));

if (sysConfig.AzureRegion != "unknown")
{
    clientBuilder = clientBuilder.WithApplicationRegion(sysConfig.AzureRegion);
}

_dbClient = clientBuilder.Build();

Mensajería asincrónica

El acoplamiento débil permite diseñar servicios de forma que un servicio no tenga dependencia de otros servicios. El aspecto débil permite que un servicio funcione de forma independiente. El aspecto de acoplamiento permite la comunicación entre servicios mediante interfaces bien definidas. En el contexto de una aplicación crítica, facilita la alta disponibilidad al evitar que los errores del flujo descendente se transmitan en cascada al front-end o los diferentes stamps de implementación.

Características principales:

  • Los servicios no están restringidos para usar la misma plataforma de proceso, lenguaje de programación o sistema operativo.
  • Los servicios se escalan de forma independiente.
  • Los errores del flujo descendente no afectan a las transacciones de cliente.
  • La integridad transaccional es más difícil de mantener, ya que la creación y persistencia de los datos se produce en servicios independientes. Esto también es un desafío en los servicios de mensajería y persistencia, como se describe en esta guía sobre el procesamiento de mensajes idempotentes.
  • El seguimiento integral requiere una orquestación más compleja.

El uso de patrones de diseño conocidos, como el patrón de nivelación de carga basada en colas y el patrón de consumidores competidores, es muy recomendable. Estos patrones ayudan a distribuir la carga del productor a los consumidores y al procesamiento asincrónico por parte de los consumidores. Por ejemplo, el rol trabajo permite que la API acepte la solicitud y vuelva al autor de llamada rápidamente mientras procesa una operación de escritura de base de datos por separado.

Se usa Azure Event Hubs como agente de mensajes entre la API y el rol de trabajo.

Importante

El agente de mensajes no está diseñado para usarse como almacén de datos persistente durante largos períodos de tiempo. El servicio Event Hubs admite la característica de captura, que permite a un centro de eventos escribir automáticamente una copia de los mensajes en una cuenta de Azure Storage vinculada. Esto mantiene el uso en la comprobación, pero también sirve como mecanismo para hacer copias de seguridad de los mensajes.

Detalles de implementación para las operaciones de escritura

Las operaciones de escritura, como la publicación de la clasificación y la publicación de comentarios, se procesan de forma asincrónica. La API envía primero un mensaje con toda la información pertinente, como el tipo de acción y los datos de los comentarios, a la cola de mensajes y devuelve inmediatamente el código HTTP 202 (Accepted) con un encabezado Location adicional del objeto que se va a crear.

A continuación, las instancias de BackgroundProcessor que controlan la comunicación real con la base de datos para las operaciones de escritura procesan los mensajes de la cola. BackgroundProcessor se escala y reduce horizontalmente en función del volumen de mensajes de la cola. El límite de escalado horizontal de las instancias de procesador se define mediante el número máximo de particiones de Event Hubs (que es 32 para los niveles Básico y Estándar, 100 para el nivel Premium y 1024 para el nivel Dedicado).

Diagrama que muestra la naturaleza asincrónica de la característica de publicación de la clasificación en la implementación.

La biblioteca de procesador de Azure EventHub de BackgroundProcessor usa Azure Blob Storage para administrar la propiedad de las particiones, el equilibrio de carga entre las diferentes instancias de trabajo y realizar un seguimiento del progreso mediante puntos de control. La escritura de los puntos de control en el almacenamiento de blobs no se produce después de cada evento porque esto agregaría un retraso prohibitivamente costoso para cada mensaje. En su lugar, la escritura de los puntos de control se produce en un bucle de temporizador (duración configurable con una configuración actual de 10 segundos):

while (!stoppingToken.IsCancellationRequested)
{
    await Task.Delay(TimeSpan.FromSeconds(_sysConfig.BackendCheckpointLoopSeconds), stoppingToken);
    if (!stoppingToken.IsCancellationRequested && !checkpointEvents.IsEmpty)
    {
        string lastPartition = null;
        try
        {
            foreach (var partition in checkpointEvents.Keys)
            {
                lastPartition = partition;
                if (checkpointEvents.TryRemove(partition, out ProcessEventArgs lastProcessEventArgs))
                {
                    if (lastProcessEventArgs.HasEvent)
                    {
                        _logger.LogDebug("Scheduled checkpointing for partition {partition}. Offset={offset}", partition, lastProcessEventArgs.Data.Offset);
                        await lastProcessEventArgs.UpdateCheckpointAsync();
                    }
                }
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Exception during checkpointing loop for partition={lastPartition}", lastPartition);
        }
    }
}

Si la aplicación del procesador encuentra un error o se detiene antes de procesar el mensaje:

  • Otra instancia recogerá el mensaje para su reprocesamiento, ya que no se ha agregado el punto de control correctamente en Azure Storage.
  • Si el rol trabajo anterior consiguió conservar el documento en la base de datos antes de producir un error, se producirá un conflicto (porque se usa el mismo identificador y clave de partición) y el procesador puede omitir el mensaje de forma segura, puesto que ya se ha conservado.
  • Si el rol de trabajo anterior finalizó antes de escribir en la base de datos, la nueva instancia repetirá los pasos y finalizará la persistencia.

Detalles de implementación para las operaciones de lectura

La API procesa las operaciones de lectura directamente y devuelve inmediatamente los datos al usuario.

Diagrama de las lecturas de enumeración de los artículos del catálogo directamente desde la base de datos.

No hay ningún canal de vuelta que se comunique con el cliente si la operación se completa correctamente. La aplicación cliente tiene que sondear proactivamente la API para buscar actualizaciones del artículo especificado en el encabezado HTTP Location.

Escalabilidad

Los componentes individuales de la carga de trabajo se deben escalar horizontalmente de forma independiente porque cada uno tiene patrones de carga diferentes. Los requisitos de escalado dependen de la funcionalidad del servicio. Algunos servicios tienen un impacto directo en el usuario final y se espera que se puedan escalar horizontalmente de forma agresiva para proporcionar una respuesta rápida para una experiencia y un rendimiento positivos del usuario en cualquier momento.

En la implementación, los servicios se empaquetan como contenedores Docker y se implementan mediante gráficos de Helm en cada stamp. Están configurados para que tengan implementadas las solicitudes y límites esperados de Kubernetes y una regla de escalado automático preconfigurada. Los componentes CatalogService y BackgroundProcessor de la carga de trabajo se pueden escalar y reducir horizontalmente de forma individual, ambos servicios no tienen estado.

Los usuarios finales interactúan directamente con CatalogService, por lo que esta parte de la carga de trabajo debe responder bajo cualquier carga. Hay al menos 3 instancias por clúster para su distribución entre las tres zonas de disponibilidad de una región de Azure. El escalador automático horizontal de pods de AKS (HPA) se encarga de agregar automáticamente más pods si es necesario y el escalado automático de Azure Cosmos DB puede aumentar y reducir dinámicamente las RU disponibles para la colección. Juntos, CatalogService y Azure Cosmos DB forman una unidad de escalado dentro de un stamp.

HPA se implementa con un gráfico de Helm con un número máximo y mínimo configurable de réplicas. Los valores se configuran como:

Durante una prueba de carga, se identificó que se espera que cada instancia controle aproximadamente 250 solicitudes por segundo con un patrón de uso estándar.

El servicio BackgroundProcessor tiene requisitos muy diferentes y se considera un rol de trabajo en segundo plano que tiene un impacto limitado en la experiencia del usuario. Por lo tanto, BackgroundProcessor tiene una configuración de escalado automático diferente que CatalogService y se puede escalar entre 2 y 32 instancias (este límite se debe basar en el número de particiones usadas en Event Hubs; no hay ninguna ventaja en tener más roles de trabajo que particiones).

Componente minReplicas maxReplicas
CatalogService 3 20
BackgroundProcessor 2 32

Además de eso, cada componente de la carga de trabajo, incluidas las dependencias como ingress-nginx, tiene configurados presupuestos de interrupciones de pods (PDB) para asegurarse de que esté siempre disponible un número mínimo de instancias cuando se implementen cambios en los clústeres.

#
# /src/app/charts/healthservice/templates/pdb.yaml
# Example pod distribution budget configuration.
#
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: {{ .Chart.Name }}-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: {{ .Chart.Name }}

Nota

El número mínimo y máximo real de pods para cada componente se debe determinar mediante pruebas de carga y puede diferir en cada carga de trabajo.

Instrumentación

La instrumentación es un mecanismo importante para evaluar los cuellos de botella de rendimiento y los problemas de mantenimiento que los componentes de la carga de trabajo puedan introducir en el sistema. Cada componente debe emitir información suficiente mediante métricas y registros de seguimiento para ayudar a cuantificar las decisiones. Estas son algunas consideraciones clave para instrumentar la aplicación.

  • Envíe los registros, las métricas y los datos de telemetría adicionales al sistema de registro del stamp.
  • Use el registro estructurado en lugar de texto sin formato para que se pueda consultar la información.
  • Implemente la correlación de eventos para garantizar la vista de transacciones completa. En la RI, cada respuesta de API contiene el identificador de operación como un encabezado HTTP para la trazabilidad.
  • No confíe solo en el registro de stdout (consola). Sin embargo, estos registros se pueden usar para solucionar problemas inmediatos de un pod con errores.

Esta arquitectura implementa el seguimiento distribuido con Application Insights respaldado por el área de trabajo de Log Analytics para todos los datos de supervisión de la aplicación. Se usa Azure Log Analytics para los registros y las métricas de todos los componentes de la infraestructura y la carga de trabajo. La carga de trabajo implementa el seguimiento completo de las solicitudes procedentes de la API, mediante Event Hubs, a Azure Cosmos DB.

Importante

Los recursos de supervisión del stamp se implementan en un grupo de recursos de supervisión independiente y tienen un ciclo de vida diferente al del propio stamp. Para obtener más información, consulte Supervisión de los datos de los recursos de stamp.

Diagrama de la implementación del stamp, los servicios de supervisión y los servicios globales de forma independiente.

Detalles de implementación de la supervisión de la aplicación

El componente BackgroundProcessor usa el paquete NuGet Microsoft.ApplicationInsights.WorkerService para obtener la instrumentación integrada de la aplicación. Además, se usa Serilog para todo el registro dentro de la aplicación con Azure Application Insights configurado como receptor (junto al receptor de la consola). Solo cuando es necesario realizar un seguimiento de métricas adicionales, se usa directamente una instancia de TelemetryClient para Application Insights.

//
// /src/app/AlwaysOn.BackgroundProcessor/Program.cs
//
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) =>
    {
        Log.Logger = new LoggerConfiguration()
                            .ReadFrom.Configuration(hostContext.Configuration)
                            .Enrich.FromLogContext()
                            .WriteTo.Console(outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}")
                            .WriteTo.ApplicationInsights(hostContext.Configuration[SysConfiguration.ApplicationInsightsConnStringKeyName], TelemetryConverter.Traces)
                            .CreateLogger();
    }

Captura de pantalla de la funcionalidad de seguimiento integral.

Para demostrar la trazabilidad práctica de las solicitudes, cada solicitud de API (correcta o no) devuelve el encabezado del identificador de correlación al autor de llamada. Con este identificador, el equipo de soporte técnico de la aplicación puede buscar en Application Insights y obtener una vista detallada de la transacción completa.

//
// /src/app/AlwaysOn.CatalogService/Startup.cs
//
app.Use(async (context, next) =>
{
    context.Response.OnStarting(o =>
    {
        if (o is HttpContext ctx)
        {
            // ... code omitted for brevity
            context.Response.Headers.Add("X-Server-Location", sysConfig.AzureRegion);
            context.Response.Headers.Add("X-Correlation-ID", Activity.Current?.RootId);
            context.Response.Headers.Add("X-Requested-Api-Version", ctx.GetRequestedApiVersion()?.ToString());
        }
        return Task.CompletedTask;
    }, context);
    await next();
});

Nota:

El SDK de Application Insights tiene habilitado el muestreo adaptable de manera predeterminada. Esto significa que no todas las solicitudes se envían a la nube y se pueden buscar por identificador. Los equipos de las aplicaciones críticas deben poder realizar un seguimiento confiable de cada solicitud, por lo que la implementación de referencia tiene deshabilitado el muestreo adaptable en el entorno de producción.

Detalles de implementación de la supervisión de Kubernetes

Además del uso de la configuración de diagnóstico para enviar registros y métricas de AKS a Log Analytics, AKS también está configurado para usar Container Insights. La habilitación de Container Insights implementa OMSAgentForLinux mediante un DaemonSet de Kubernetes en cada uno de los nodos de los clústeres de AKS. OMSAgentForLinux es capaz de recopilar registros y métricas adicionales desde el clúster de Kubernetes y enviarlos a su área de trabajo de Log Analytics correspondiente. Contiene datos más detallados sobre los pods, las implementaciones, los servicios y el estado general del clúster.

Un registro extenso puede afectar negativamente al costo, a la vez que no proporciona ninguna ventaja. Por este motivo, la recopilación de registros de stdout y la extracción de Prometheus están deshabilitadas para los pods de la carga de trabajo en la configuración de Container Insights, puesto que todos los seguimientos ya se capturan mediante Application Insights, lo que generaría registros duplicados.

#
# /src/config/monitoring/container-azm-ms-agentconfig.yaml
# This is just a snippet showing the relevant part.
#
[log_collection_settings]
    [log_collection_settings.stdout]
        enabled = false

        exclude_namespaces = ["kube-system"]

Consulte el archivo de configuración completo como referencia.

Supervisión del estado

La supervisión y la observabilidad de las aplicaciones se suelen usar para identificar rápidamente problemas con un sistema e informar al modelo de estado sobre el estado actual de la aplicación. El seguimiento de estado, expuesto mediante los puntos de conexión de estado y utilizado por los sondeos de estado, proporciona información con la que se puede actuar inmediatamente, y normalmente se indica al equilibrador de carga principal que quite el componente incorrecto de la rotación.

En la arquitectura, el seguimiento de estado se aplica en estos niveles:

  • Pods de carga de trabajo que se ejecutan en AKS. Estos pods tienen sondeos de estado y de ejecución, por lo que AKS puede administrar su ciclo de vida.
  • El servicio de estado es un componente dedicado en el clúster. Azure Front Door está configurado para sondear los servicios de estado de cada stamp y quitar automáticamente los stamps incorrectos del equilibrio de carga.

Detalles de implementación del servicio de estado

HealthService es un componente de la carga de trabajo que se ejecuta junto con otros componentes (CatalogService y BackgroundProcessor) en el clúster de proceso. Proporciona una API REST a la que llama la comprobación de estado de Azure Front Door para determinar la disponibilidad de un stamp. A diferencia de los sondeos de ejecución básicos, el servicio de estado es un componente más complejo que agrega el estado de las dependencias además del propio.

Diagrama del servicio de salud consultando Azure Cosmos DB, Event Hubs y Storage.

Si el clúster de AKS está inactivo, el servicio de estado no responderá y representará una carga de trabajo en estado incorrecto. Cuando el servicio está en ejecución, realiza comprobaciones periódicas en los componentes críticos de la solución. Todas las comprobaciones se realizan de forma asincrónica y en paralelo. Si se produce un error en alguna de ellas, el stamp completo se considerará no disponible.

Advertencia

Los sondeos de estado de Azure Front Door pueden generar una carga significativa en el servicio de estado, ya que las solicitudes proceden de varias ubicaciones PoP. Para evitar sobrecargar los componentes del flujo descendente, es necesario realizar un almacenamiento en caché adecuado.

El servicio de estado también se usa para las pruebas de ping de dirección URL configuradas explícitamente con el recurso de Application Insights de cada stamp.

Para obtener más información sobre la HealthService implementación, consulta Application Health Service.