Patrón de consumidores de la competencia

Permite que varios consumidores simultáneos procesen los mensajes recibidos en el mismo canal de mensajería. Este patrón permite que un sistema procese 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. Usar una única instancia del servicio de consumidor puede provocar que la instancia se inunde de solicitudes, o el sistema de mensajería podría sobrecargarse por una afluencia de mensajes procedentes 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

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. Esto puede ayudar a reducir el impacto sobre la disponibilidad y la capacidad de respuesta de las instancias de aplicación y las de servicio, como se describe en Queue-based Load Leveling pattern (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. 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.

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 Idempotency Patterns (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á. Esto 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.

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 QueueClient.

private string serviceBusConnectionString = ...;
...

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

    var queueClient = new QueueClient(serviceBusConnectionString, "myqueue");

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

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

     // Send the message to the queue
     await queueClient.SendAsync(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}");
}

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

  • 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.

  • 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.