Patrón de consumidores de la competencia

Azure Functions
Azure Service Bus

Permite que varios consumidores simultáneos procesen los mensajes recibidos en el mismo canal de mensajería. Con varios consumidores simultáneos, un sistema puede procesar varios mensajes simultáneamente a fin de optimizar el rendimiento, mejorar la escalabilidad y disponibilidad, y equilibrar la carga de trabajo.

Contexto y problema

Cabe esperar que una aplicación que se ejecuta en la nube administre un gran número de solicitudes. En lugar de procesar cada solicitud de forma sincrónica, una técnica común es que la aplicación las pase mediante un sistema de mensajería a otro servicio (un servicio de consumidor) que las administra de manera asincrónica. Esta estrategia ayuda a garantizar que la lógica de negocios de la aplicación no se bloquee mientras se procesan las solicitudes.

El número de solicitudes puede variar significativamente con el tiempo por diversos motivos. Un aumento repentino de la actividad del usuario o solicitudes agregadas procedentes de varios inquilinos pueden provocar una carga de trabajo imprevisible. En horas de máxima actividad, un sistema podría tener la necesidad de procesar muchos cientos de solicitudes por segundo, mientras que en otras ocasiones el número puede ser muy pequeño. Además, la naturaleza del trabajo realizado para administrar estas solicitudes puede ser muy variable. Mediante una única instancia del servicio de consumidor, puede provocar que esa instancia se inunde de solicitudes. O bien, el sistema de mensajería podría sobrecargarse por una afluencia de mensajes que proceden de la aplicación. Para administrar esta carga de trabajo cambiante, el sistema puede ejecutar varias instancias del servicio de consumidor. Sin embargo, estos consumidores debe coordinarse para garantizar que cada mensaje se entrega solo a un único consumidor. La carga de trabajo también debe equilibrarse entre los consumidores para impedir que una instancia se convierte en un cuello de botella.

Solución

Use una cola de mensajes para implementar el canal de comunicación entre la aplicación y las instancias del servicio de consumidor. La aplicación publica las solicitudes en forma de mensajes en la cola, y las instancias de servicio de consumidor reciben los mensajes de la cola y los procesan. Este enfoque permite que el mismo grupo de instancias de servicio de consumidor administren los mensajes desde cualquier instancia de la aplicación. En la ilustración se muestra cómo usar una cola de mensajes para distribuir trabajo a las instancias de un servicio.

Uso de una cola de mensajes para distribuir el trabajo a las instancias de un servicio

Nota:

Aunque hay varios consumidores de estos mensajes, esto no se corresponde con el Patrón de publicación de suscripciones (pub/sub). Con el enfoque de Consumidores de la competencia, cada mensaje se envía a un único consumidor para su procesamiento, mientras que con el enfoque Pub/Sub, todos los consumidores reciben cada mensaje.

Esta solución tiene las siguientes ventajas:

  • Proporciona un sistema de redistribución de la carga que puede administrar grandes variaciones en el volumen de solicitudes enviadas por las instancias de la aplicación. La cola actúa como búfer entre las instancias de la aplicación y las instancias de servicio de consumidor. Este búfer puede ayudar a minimizar el impacto en la disponibilidad y la capacidad de respuesta, tanto para la aplicación como para las instancias de servicio. Para obtener más información, consulte Patrón Queue-Based Load Leveling. Administrar un mensaje que requiere un procesamiento de ejecución prolongada no impide que otras instancias del servicio de consumidor administren simultáneamente otros mensajes.

  • Mejora la confiabilidad. Si un productor se comunica directamente con un consumidor en lugar de usar este patrón, pero no supervisa el consumidor, existe una alta probabilidad de que los mensajes se pierdan o no puedan procesarse si se produce un error en el consumidor. En este patrón, no se envían mensajes a una instancia de servicio específica. Una instancia de servicio que ha dado error no bloqueará a un productor y cualquier instancia de servicio de trabajo podrá procesar los mensajes.

  • No es necesaria una coordinación compleja entre los consumidores o entre el productor y las instancias de consumidor. La cola de mensajes garantiza que cada mensaje se entrega al menos una vez.

  • Es escalable. Cuando se aplica escalado automático, el sistema puede aumentar o disminuir de forma dinámica el número de instancias del servicio de consumidor a medida que el volumen de mensajes varía.

  • Puede mejorar la resistencia si la cola de mensajes proporciona operaciones de lectura transaccionales. Si una instancia de servicio de consumidor lee y procesa el mensaje como parte de una operación transaccional y se produce un error en la instancia de servicio de consumidor, este patrón puede garantizar que el mensaje se devuelva a la cola para que otra instancia de servicio de consumidor pueda recogerlo y administrarlo. Para mitigar el riesgo de que un mensaje falle continuamente, se recomienda usar colas de mensajes fallidos.

Problemas y consideraciones

Tenga en cuenta los puntos siguientes al decidir cómo implementar este patrón:

  • Orden de los mensajes. El orden en que las instancias de servicio de consumidor reciben loa mensajes no está garantizado y no refleja necesariamente el orden en que se crearon los mensajes. Diseñe el sistema para asegurarse de que el procesamiento de mensajes sea idempotente, ya que de esta forma podrá eliminar cualquier dependencia en el orden en el que se administran los mensajes. Para más información, consulte los patrones de idempotencia en el blog de Jonathon Oliver.

    Las colas de Microsoft Azure Service Bus pueden implementar el orden de mensajes garantizado "primero en entrar, primero en salir" mediante sesiones de mensajes. Para más información, consulte Sesiones de uso de patrones de mensajería.

  • Diseño de servicios para proporcionar resistencia. Si el sistema está diseñado para detectar y reiniciar las instancias de servicio con error, podría ser necesario implementar el procesamiento realizado por las instancias de servicio como operaciones idempotente a fin de reducir los efectos de que un único mensaje se recupere y procese más de una vez.

  • Detección de mensajes dudosos. Un mensaje con formato incorrecto, o una tarea que requiere acceso a recursos que no están disponibles, puede hacer que una instancia de servicio produzca un error. El sistema debe impedir que dichos mensajes se devuelvan a la cola y, en su lugar, capturar y almacenar los detalles de estos mensajes en otra parte, de modo que puedan analizarse si es necesario.

  • Administración de resultados. La instancia de servicio que administra un mensaje se separa por completo de la lógica de aplicación que genera el mensaje, y es posible que no se puedan comunicar directamente. Si la instancia de servicio genera resultados que deben pasarse de vuelta a la lógica de aplicación, esta información debe almacenarse en una ubicación que sea accesible para ambas. Para evitar que la lógica de aplicación recupere datos incompletos, el sistema debe indicar cuándo el procesamiento ha finalizado.

    Si usa Azure, un proceso de trabajo puede pasar los resultados de vuelta a la lógica de aplicación mediante una cola de respuesta a mensajes dedicada. La lógica de aplicación debe poder correlacionar estos resultados con el mensaje original. Este escenario se describe con más detalle en Asynchronous Messaging Primer (Manual básico de mensajería asincrónica).

  • Escalado del sistema de mensajería. En una solución a gran escala, una única cola de mensajes podría verse desbordada por el número de mensajes y convertirse en un cuello de botella en el sistema. En esta situación, considere la posibilidad de crear particiones del sistema de mensajería para enviar mensajes de productores específicos a una cola determinada o usar el equilibrio de carga para distribuir los mensajes entre varias colas de mensajes.

  • Garantía de confiabilidad del sistema de mensajería. Es necesario un sistema de mensajería confiable para garantizar que después de que la aplicación pone en cola un mensaje, este no se perderá. Este sistema es esencial para garantizar que todos los mensajes se entregan al menos una vez.

Cuándo usar este patrón

Use este patrón en los siguientes supuestos:

  • La carga de trabajo de una aplicación se divida en tareas que se pueden ejecutar de forma asincrónica.
  • Las tareas sean independientes y se puedan ejecutar en paralelo.
  • El volumen de trabajo sea tan variable que requiera una solución escalable.
  • La solución debe proporcionar alta disponibilidad y ser resistente si se produce un error en el procesamiento de una tarea.

Este modelo podría no ser útil en las situaciones siguientes:

  • No sea fácil dividir la carga de trabajo de la aplicación en tareas discretas, o haya un alto grado de dependencia entre las tareas.
  • Las tareas deban realizarse de forma sincrónica y la lógica de la aplicación deba esperar a que una tarea se complete antes de continuar.
  • Las tareas se deban realizar en una secuencia concreta.

Algunos sistemas de mensajería admiten sesiones que permiten que un productor agrupe los mensajes y garantizan que el mismo consumidor los administre todos. Este mecanismo se puede usar con los mensajes con prioridad (si se admiten) para implementar una forma de ordenación de los mensajes que los entrega en secuencia desde un productor hasta un único consumidor.

Diseño de cargas de trabajo

El arquitecto debe evaluar cómo se puede usar el patrón de consumidores competidores en el diseño de su carga de trabajo para abordar los objetivos y principios tratados en los pilares del Marco de la Well-Architected de Azure. Por ejemplo:

Fundamento Cómo apoya este patrón los objetivos de los pilares
Las decisiones de diseño de la fiabilidad ayudan a que la carga de trabajo sea resistente a los errores y a garantizar que se recupere a un estado de pleno funcionamiento después de que se produzca un error. Este patrón crea redundancia en el procesamiento de colas tratando a los consumidores como réplicas, de modo que un error de instancia no impide que otros consumidores procesen los mensajes de la cola.

- RE:05 Redundancia
- RE:07 Trabajos en segundo plano
La optimización de costos se centra en mantener y mejorar el retorno de la inversión de la carga de trabajo. Este patrón puede ayudarle a optimizar los costos al permitir un escalado basado en la profundidad de la cola, hasta cero cuando la cola está vacía. También puede optimizar los costos al permitirle limitar el número máximo de instancias de consumo simultáneas.

- CO:05 Optimización de velocidad
- CO:07 Costos de componentes
La eficiencia del rendimiento ayuda a que la carga de trabajo satisfaga eficazmente las demandas mediante optimizaciones en el escalado, los datos y el código. La distribución de la carga entre todos los nodos consumidores aumenta la utilización y el escalado dinámico basado en la profundidad de la cola y minimiza el sobreaprovisionamiento.

- PE:05 Escapado y particiones
- PE:07 Código e infraestructura

Al igual que con cualquier decisión de diseño, hay que tener en cuenta las ventajas y desventajas con respecto a los objetivos de los otros pilares que podrían introducirse con este patrón.

Ejemplo

Azure proporciona colas de Service Bus y desencadenadores de colas de Azure Functions que, cuando se combinan, son una implementación directa de este modelo de diseño en la nube. Azure Functions se integra con Azure Service Bus mediante desencadenadores y enlaces. La integración con Service Bus permite compilar funciones que consumen mensajes de cola enviados por publicadores. Las aplicaciones de publicación publicarán los mensajes en una cola, y los consumidores, implementados como Azure Functions, pueden recuperar los mensajes de esta cola y administrarlos.

Para lograr resistencia, una cola de Service Bus permite al consumidor utilizar el modo PeekLock cuando recupera un mensaje de la cola. Este modo no quita el mensaje realmente, sino que simplemente lo oculta de otros consumidores. El runtime de Azure Functions recibe un mensaje en modo PeekLock. Si la función finaliza correctamente, llama a Completar en el mensaje, o puede llamar a Abandonar si se produce un error en la función, y el mensaje volverá a estar visible, lo que permite que otro consumidor lo recupere. Si la ejecución de la función dura más que el tiempo de espera de PeekLock, el bloqueo se renovará automáticamente siempre que la función esté en ejecución.

Azure Functions se puede escalar o reducir horizontalmente en función de la profundidad de la cola, todos actuando como consumidores en competencia de la cola. Si se crean varias instancias de las funciones, todas ellas compiten por la extracción y el procesamiento de los mensajes de manera independiente.

Para más información sobre el uso de colas de Azure Service Bus, consulte Colas, temas y suscripciones de Service Bus.

Para obtener información sobre las instancias de Azure Functions desencadenadas en cola, consulte Desencadenador de Azure Service Bus para Azure Functions.

En el código siguiente se muestra cómo puede crear un nuevo mensaje y enviarlo a una cola de Service Bus mediante una instancia de ServiceBusClient.

private string serviceBusConnectionString = ...;
...

  public async Task SendMessagesAsync(CancellationToken  ct)
  {
   try
   {
    var msgNumber = 0;

    var serviceBusClient = new ServiceBusClient(serviceBusConnectionString);

    // create the sender
    ServiceBusSender sender = serviceBusClient.CreateSender("myqueue");

    while (!ct.IsCancellationRequested)
    {
     // Create a new message to send to the queue
     string messageBody = $"Message {msgNumber}";
     var message = new ServiceBusMessage(messageBody);

     // Write the body of the message to the console
     this._logger.LogInformation($"Sending message: {messageBody}");

     // Send the message to the queue
     await sender.SendMessageAsync(message);

     this._logger.LogInformation("Message successfully sent.");
     msgNumber++;
    }
   }
   catch (Exception exception)
   {
    this._logger.LogException(exception.Message);
   }
  }

En el ejemplo de código siguiente se muestra un consumidor, escrito como una instancia de Azure Functions C#, que lee metadatos de mensaje y registra un mensaje de cola de Service Bus. Observe cómo se utiliza el atributo ServiceBusTrigger para enlazarlo a una cola de Service Bus.

[FunctionName("ProcessQueueMessage")]
public static void Run(
    [ServiceBusTrigger("myqueue", Connection = "ServiceBusConnectionString")]
    string myQueueItem,
    Int32 deliveryCount,
    DateTime enqueuedTimeUtc,
    string messageId,
    ILogger log)
{
    log.LogInformation($"C# ServiceBus queue trigger function consumed message: {myQueueItem}");
    log.LogInformation($"EnqueuedTimeUtc={enqueuedTimeUtc}");
    log.LogInformation($"DeliveryCount={deliveryCount}");
    log.LogInformation($"MessageId={messageId}");
}

Pasos siguientes

  • Manual de mensajería asincrónica. Las colas de mensajes son un mecanismo de comunicaciones asincrónico. Si un servicio de consumidor debe enviar una respuesta a una aplicación, podría ser necesario implementar alguna forma de mensajería de respuesta. En Asynchronous Messaging Primer se proporciona información sobre cómo implementar la mensajería de solicitud/respuesta con colas de mensajes.

  • Guía de escalado automático. Se podrían iniciar y detener instancias de un servicio de consumidor dado que la longitud de la cola en la que las aplicaciones publican los mensajes varía. El escalado automático puede ayudar a mantener el rendimiento durante los períodos de procesamiento de máximo.

Los patrones y las directrices siguientes podrían ser importantes a la hora de implementar este patrón:

  • Patrón Compute Resource Consolidation. Se podrían consolidar varias instancias de un servicio de consumidor en un único proceso para reducir los costes y la sobrecarga de administración. El patrón de consolidación de los recursos de proceso describe las ventajas e inconvenientes de este enfoque.

  • Patrón Queue-Based Load Leveling. La introducción de una cola de mensajes puede agregar resistencia al sistema, al permitir que las instancias de servicio administren volúmenes muy diversos de solicitudes desde instancias de aplicación. La cola de mensajes actúa como búfer, que redistribuye la carga. El patrón de redistribución de carga basada en colas describe este escenario con mayor detalle.