Patrón de cola de prioridad

Azure Service Bus

Clasifica por orden de prioridad las solicitudes enviadas a los servicios para que aquellas con una prioridad más alta se reciban y procesen más rápidamente que las que tienen una prioridad más baja. Este patrón es útil en aplicaciones que ofrecen garantías de nivel de servicio diferentes a los clientes individuales.

Contexto y problema

Las aplicaciones pueden delegar tareas específicas a otros servicios, por ejemplo, para realizar el procesamiento en segundo plano o para integrarse con otras aplicaciones o servicios. En la nube, una cola de mensajes se utiliza normalmente para delegar tareas al procesamiento en segundo plano. En muchos casos, el orden en que se reciben las solicitudes en un servicio no es importante. Sin embargo, en algunos casos, es necesario dar prioridad a solicitudes específicas. Estas solicitudes deben procesarse antes que las solicitudes de menor prioridad enviadas anteriormente por la aplicación.

Solución

Una cola suele tener una estructura basada en el criterio primero en entrar, primero en salir, y los consumidores normalmente reciben mensajes en el mismo orden en que se publican en la cola. Sin embargo, algunas colas de mensajes admiten mensajería de prioridad. La aplicación que publica un mensaje puede asignar una prioridad. Los mensajes de la cola se reordenan automáticamente para que los que tengan una prioridad más alta se reciban antes que aquellos que tengan una prioridad más baja. En este diagrama se muestra el proceso:

Diagrama que ilustra un mecanismo de cola que admite la asignación de prioridad a los mensajes.

Nota

La mayoría de las implementaciones de cola de mensajes admiten varios consumidores. (Consulte el Patrón de consumidores de la competencia). El número de procesos del consumidor se puede escalar y reducir verticalmente en función de la demanda.

En los sistemas que no admiten colas de mensajes basadas en prioridad, una solución alternativa consiste en mantener una cola independiente para cada prioridad. La aplicación es responsable de enviar mensajes a la cola adecuada. Cada cola puede tener un grupo independiente de consumidores. Las colas de mayor prioridad pueden tener un grupo más grande de consumidores en ejecución en hardware más rápido que las colas de menor prioridad. En este diagrama se muestra el uso de colas de mensajes independientes para cada prioridad:

Diagrama que muestra el uso de colas de mensajes independientes para cada prioridad.

Una variación de esta estrategia es implementar un único grupo de consumidores que primero compruebe los mensajes en las colas de alta prioridad y, solo después de eso, inicie la captura de los mensajes de colas de prioridad más baja. Hay algunas diferencias semánticas entre una solución que utiliza un único grupo de procesos de consumidor (ya sea con una sola cola que admite mensajes que tienen prioridades diferentes o con varias colas donde cada una controla los mensajes de una única prioridad) y una solución que usa varias colas con un grupo independiente para cada cola.

En el enfoque de grupo único, los mensajes con prioridad más alta siempre se reciben y procesan antes que los mensajes de prioridad más baja. En teoría, los mensajes de prioridad baja pueden reemplazarse continuamente y puede que nunca se procesen. En el enfoque de varios grupos, los mensajes de prioridad más baja siempre se procesan, pero no tan rápido como los mensajes de mayor prioridad (en función del tamaño relativo de los grupos y los recursos que tienen disponibles).

El uso de un mecanismo de puesta en cola según prioridad puede ofrecer las siguientes ventajas:

  • Permite a las aplicaciones satisfacer los requisitos empresariales que requieren la asignación de prioridades de disponibilidad o rendimiento, por ejemplo, ofrecer distintos niveles de servicio a distintos grupos de clientes.

  • Puede ayudar a minimizar los costos operativos. Si utiliza el enfoque de cola única, se puede reducir el número de consumidores si es necesario. Los mensajes con prioridad alta se procesan aun así en primer lugar (aunque posiblemente más lentos), y los mensajes de menor prioridad podrían tardar más. Si implementa el enfoque de varias colas de mensajes con grupos de consumidores independientes para cada cola, puede reducir el grupo de consumidores en colas de prioridad baja. Puede incluso suspender el proceso en algunas colas de muy baja prioridad deteniendo todos los consumidores que escuchan mensajes en esas colas.

  • El enfoque de varias colas de mensajes puede ayudar a maximizar el rendimiento y la escalabilidad de la aplicación mediante la partición de mensajes en función de los requisitos de procesamiento. Por ejemplo, puede priorizar las tareas críticas para que se controlen mediante receptores que se ejecutan inmediatamente, y los receptores que están menos ocupados pueden controlar las tareas en segundo plano menos importantes que están programadas para ejecutarse en horas de menor carga de trabajo.

Consideraciones

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

  • Defina las prioridades en el contexto de la solución. Por ejemplo, un mensaje de prioridad alta podría definirse como un mensaje que se debe procesar en un plazo de 10 segundos. Identifique los requisitos para administrar elementos de prioridad alta y los recursos que se tienen que asignar para cumplir los criterios.

  • Decida si se deben procesar todos los elementos de prioridad alta antes que los elementos de menor prioridad. Si los mensajes los procesa un único grupo de consumidores, tendrá que proporcionar un mecanismo que pueda dar prioridad y suspender una tarea que administra un mensaje de prioridad baja si un mensaje de mayor prioridad entra en la cola.

  • En el enfoque de varias colas, al usar un único grupo de procesos de consumidores que escuchan en todas las colas en lugar de un grupo de consumidores dedicado para cada cola, el consumidor debe aplicar un algoritmo que asegure que siempre pueda administrar los mensajes de colas de prioridad más alta antes que los mensajes de las colas de prioridad más baja.

  • Supervise la velocidad de procesamiento en colas de alta y baja prioridad para garantizar que los mensajes de esas colas se procesan a la velocidad esperada.

  • Si necesita garantizar que se procesarán los mensajes con prioridad baja, implemente el enfoque de varias colas de mensajes con varios grupos de consumidores. Como alternativa, en una cola que admite la concesión de prioridad a los mensajes, puede aumentar de forma dinámica la prioridad de un mensaje en cola, conforme avanza el tiempo. Sin embargo, este enfoque depende de la cola de mensajes que ofrece esta característica.

  • Se recomienda la estrategia de usar colas independientes basadas en la prioridad del mensaje para los sistemas que tienen algunas prioridades bien definidas.

  • El sistema puede determinar lógicamente las prioridades de los mensajes. Por ejemplo, en lugar de tener mensajes explícitos de prioridad alta y baja, podría designar mensajes como "cliente que paga" o "cliente que no paga". A continuación, el sistema podría asignar más recursos para procesar mensajes de clientes de pago.

  • Puede haber un costo financiero y de procesamiento asociado con la comprobación de una cola de un mensaje. Por ejemplo, algunos sistemas de mensajería comerciales cargan una tarifa reducida cada vez que se envía o recupera un mensaje y cada vez que se consultan mensajes en una cola. Este costo aumenta si comprueba varias colas.

  • Puede ajustar dinámicamente el tamaño de un grupo de consumidores en función de la longitud de la cola que atiende al grupo. Para más información, consulte Guía de escalado automático.

Cuándo usar este patrón

Este patrón es útil en escenarios donde:

  • El sistema debe controlar varias tareas que tengan diferentes prioridades.

  • Los distintos usuarios o inquilinos deben atender las solicitudes con diferentes prioridades.

Diseño de cargas de trabajo

Un arquitecto debe evaluar cómo se puede usar el patrón Priority Queue en el diseño de su carga de trabajo para abordar los objetivos y principios descritos en los pilares del Marco de buena arquitectura 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. La separación de elementos en función de la prioridad empresarial le permite centrar los esfuerzos de confiabilidad en el trabajo más crítico.

- RE:02 Flujos críticos
- RE:07 Trabajos en segundo plano
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 separación de elementos en función de la prioridad empresarial le permite centrar los esfuerzos de rendimiento en el trabajo más sujeto al tiempo.

- PE:09 Flujos críticos

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 no proporciona un mecanismo de puesta en cola que admita de forma nativa la asignación de prioridad a los mensajes mediante su ordenación. Sin embargo, proporciona temas y suscripciones de Azure Service Bus, suscripciones de Service Bus que admiten un mecanismo de puesta en cola que ofrece filtrado de mensajes y un amplio conjunto de funcionalidades flexibles que hacen que Azure sea ideal en las implementaciones de colas con la máxima prioridad.

Una solución de Azure puede implementar un tema de Service Bus en el que una aplicación puede publicar mensajes, tal como los publicaría en una cola. Los mensajes pueden contener metadatos en forma de propiedades personalizadas definidas por la aplicación. Puede asociar las suscripciones de Service Bus con el tema, y las suscripciones pueden filtrar los mensajes en función de sus propiedades. Cuando una aplicación envía un mensaje a un tema, el mensaje se dirige a la suscripción correspondiente donde lo puede leer un consumidor. Los procesos de consumidor pueden recuperar mensajes de una suscripción mediante la misma semántica que usarían con una cola de mensajes. (Una suscripción es una cola lógica). En este diagrama se muestra cómo implementar una cola de prioridad mediante temas y suscripciones de Service Bus:

Diagrama que muestra cómo implementar una cola de prioridad mediante temas y suscripciones de Service Bus.

En la diagrama anterior, la aplicación crea varios mensajes y asigna una propiedad personalizada denominada Priority en cada mensaje. Priority tiene un valor de High o Low. La aplicación envía estos mensajes a un tema. El tema tiene dos suscripciones asociadas que filtran los mensajes basándose en la propiedad Priority. Una suscripción acepta mensajes con la propiedad Priority establecida en High. La otra acepta mensajes con la propiedad Priority establecida en Low. Un grupo de consumidores lee los mensajes de cada suscripción. La suscripción de alta prioridad tiene un grupo más grande, y estos consumidores podrían estar ejecutándose en equipos más eficaces que tienen más recursos disponibles que los ordenadores del grupo de prioridad baja.

No hay nada especial acerca de la designación de mensajes con prioridad alta y baja en este ejemplo. Son simplemente etiquetas que se especifican como propiedades en cada mensaje. Se usan para dirigir los mensajes a una suscripción concreta. Si se necesitan prioridades adicionales, es relativamente fácil crear más suscripciones y grupos de procesos de consumidor para controlar esas prioridades.

La solución PriorityQueue en GitHub se basa en este enfoque. Esta solución contiene los proyectos de Azure Functions denominados PriorityQueueConsumerHigh y PriorityQueueConsumerLow. Estos proyectos de Azure Functions se integran con Service Bus mediante desencadenadores y enlaces. Se conectan a distintas suscripciones definidas en ServiceBusTrigger y reaccionan a los mensajes entrantes.

public static class PriorityQueueConsumerHighFn
{
    [FunctionName("HighPriorityQueueConsumerFunction")]
    public static void Run(
      [ServiceBusTrigger("messages", "highPriority", Connection = "ServiceBusConnection")]string highPriorityMessage,
      ILogger log)
    {
        log.LogInformation($"C# ServiceBus topic trigger function processed message: {highPriorityMessage}");
    }
}

Como administrador, puede configurar cuántas instancias de las funciones de Azure App Service puede escalar horizontalmente. Para ello, configure la opción Aplicar límite de escalabilidad horizontal de Azure Portal, estableciendo un límite máximo de escalado horizontal para cada función. Normalmente, necesitará más instancias de la función PriorityQueueConsumerHigh que de la función PriorityQueueConsumerLow. Esta configuración garantiza que los mensajes de prioridad alta se leen de la cola más rápidamente que los mensajes de prioridad baja.

Otro proyecto, PriorityQueueSender, contiene una función de Azure desencadenada por el tiempo configurada para ejecutarse cada 30 segundos. Esta función se integra con Service Bus a través de un enlace de salida y envía lotes de mensajes de prioridad baja y alta a un objeto IAsyncCollector. Cuando la función envía mensajes al tema que está asociado con las suscripciones utilizadas por las funciones PriorityQueueConsumerHigh y PriorityQueueConsumerLow, especifica la prioridad con el uso de la propiedad personalizada Priority, como se muestra a continuación:

public static class PriorityQueueSenderFn
{
    [FunctionName("PriorityQueueSenderFunction")]
    public static async Task Run(
        [TimerTrigger("0,30 * * * * *")] TimerInfo myTimer,
        [ServiceBus("messages", Connection = "ServiceBusConnection")] IAsyncCollector<ServiceBusMessage> collector)
    {
        for (int i = 0; i < 10; i++)
        {
            var messageId = Guid.NewGuid().ToString();
            var lpMessage = new ServiceBusMessage() { MessageId = messageId };
            lpMessage.ApplicationProperties["Priority"] = Priority.Low;
            lpMessage.Body = BinaryData.FromString($"Low priority message with Id: {messageId}");
            await collector.AddAsync(lpMessage);

            messageId = Guid.NewGuid().ToString();
            var hpMessage = new ServiceBusMessage() { MessageId = messageId };
            hpMessage.ApplicationProperties["Priority"] = Priority.High;
            hpMessage.Body = BinaryData.FromString($"High priority message with Id: {messageId}");
            await collector.AddAsync(hpMessage);
        }
    }
}

Pasos siguientes

Los siguientes recursos pueden resultar útiles al implementar este patrón:

  • Una muestra que enseña este patrón en GitHub.

  • Manual de mensajería asincrónica. Un servicio de consumidor que procesa una solicitud puede necesitar enviar una respuesta a la instancia de la aplicación que publicó la solicitud. Este artículo proporciona información sobre las estrategias que puede usar para implementar la mensajería de solicitud/respuesta.

  • Guía de escalado automático. A veces puede escalar el tamaño del grupo de procesos de consumidor que están controlando una cola en función de la longitud de la misma. Esta estrategia le puede ayudar a mejorar el rendimiento, especialmente en los grupos que controlan los mensajes con prioridad alta.

Los siguientes patrones pueden resultar útiles al implementar este patrón:

  • Patrón de consumidores de la competencia. Para mejorar el rendimiento de las colas, puede implementar varios consumidores que escuchen en la misma cola y procesen tareas en paralelo. Estos consumidores compiten por los mensajes, pero solo uno debe ser capaz de procesar cada mensaje. Este artículo proporciona más información sobre las ventajas y desventajas de la implementación de este enfoque.

  • Patrón de limitación. Puede implementar la limitación mediante el uso de colas. Puede usar la mensajería de prioridad para asegurarse de que se concede prioridad a las solicitudes que proceden de aplicaciones críticas o de aplicaciones que sean ejecutadas por clientes de alto valor con respecto a las solicitudes de aplicaciones menos importantes.