Editar

Padrão de Fila de Prioridade

Azure Service Bus

Defina prioridades dos pedidos enviados para os serviços, de modo a que os pedidos com uma prioridade mais alta sejam recebidos e processados mais rapidamente do que aqueles com uma prioridade mais baixa. Este padrão é útil em aplicações que oferecem diferentes garantias de nível de serviço para clientes individuais.

Contexto e problema

As aplicações podem delegar tarefas específicas para outros serviços, por exemplo, realizar o processamento em segundo plano ou integrar com outros serviços ou aplicações. Na cloud, uma fila de mensagens é normalmente utilizada para delegar tarefas para processamento em segundo plano. Em muitos casos, a ordem pela qual as solicitações são recebidas por um serviço não é importante. No entanto, em alguns casos, é necessário atribuir prioridades a pedidos específicos. Estes pedidos devem ser processados mais cedo do que os pedidos de prioridade inferior que foram enviados anteriormente pela aplicação.

Solução

Uma fila geralmente é uma estrutura FIFO (first-in, first-out), e os consumidores normalmente recebem mensagens na mesma ordem em que são postadas na fila. No entanto, algumas filas de mensagens suportam mensagens de prioridade. O aplicativo que está postando uma mensagem pode atribuir uma prioridade. As mensagens na fila são automaticamente reordenadas para que aquelas que têm uma prioridade mais alta sejam recebidas antes daquelas que têm uma prioridade menor. Este diagrama ilustra o processo:

Diagrama que ilustra um mecanismo de enfileiramento que oferece suporte à priorização de mensagens.

Nota

A maioria das implementações de fila de mensagens oferece suporte a vários consumidores. (Veja o Padrão de consumidores concorrentes.) O número de processos de consumo pode ser ampliado e reduzido com base na demanda.

Nos sistemas que não suportam filas de mensagens baseadas na prioridade, uma solução alternativa consiste em manter uma fila separada para cada prioridade. A aplicação é responsável por publicar mensagens na fila adequada. Cada fila pode ter um conjunto separado de consumidores. As filas de prioridade mais alta podem ter um pool maior de consumidores que são executados em hardware mais rápido do que as filas de prioridade mais baixa. Este diagrama ilustra o uso de filas de mensagens separadas para cada prioridade:

Diagrama que ilustra o uso de filas de mensagens separadas para cada prioridade.

Uma variação dessa estratégia é implementar um único pool de consumidores que verificam mensagens em filas de alta prioridade primeiro e só depois começam a buscar mensagens de filas de prioridade mais baixa. Há algumas diferenças semânticas entre uma solução que usa um único pool de processos de consumidor (com uma única fila que oferece suporte a mensagens que têm prioridades diferentes ou com várias filas que manipulam mensagens de uma única prioridade) e uma solução que usa várias filas com um pool separado para cada fila.

Na abordagem de pool único, as mensagens de prioridade mais alta são sempre recebidas e processadas antes das mensagens de prioridade mais baixa. Em teoria, as mensagens de baixa prioridade poderiam ser continuamente substituídas e poderiam nunca ser processadas. Na abordagem de vários pools, as mensagens de prioridade mais baixa são sempre processadas, mas não tão rapidamente quanto as mensagens de prioridade mais alta (dependendo do tamanho relativo dos pools e dos recursos disponíveis para eles).

O uso de um mecanismo de fila de prioridades pode oferecer as seguintes vantagens:

  • Ele permite que os aplicativos atendam aos requisitos de negócios que exigem a priorização de disponibilidade ou desempenho, como oferecer diferentes níveis de serviço para diferentes grupos de clientes.

  • Pode ajudar a minimizar os custos operacionais. Se você usar a abordagem de fila única, poderá reduzir o número de consumidores, se necessário. As mensagens de alta prioridade ainda são processadas primeiro (embora possivelmente mais lentamente) e as mensagens de prioridade mais baixa podem ser atrasadas por mais tempo. Se você implementar a abordagem de fila de várias mensagens com pools separados de consumidores para cada fila, poderá reduzir o pool de consumidores para filas de prioridade mais baixa. Você pode até mesmo suspender o processamento para algumas filas de prioridade muito baixa interrompendo todos os consumidores que ouvem mensagens nessas filas.

  • A abordagem de várias filas de mensagens pode ajudar a maximizar o desempenho e a escalabilidade da aplicação ao particionar mensagens com base nos requisitos de processamento. Por exemplo, você pode priorizar tarefas críticas para que elas sejam manipuladas por recetores que são executados imediatamente, e tarefas em segundo plano menos importantes podem ser tratadas por recetores que estão programados para serem executados em horários menos ocupados.

Considerações

Considere os seguintes pontos ao decidir como implementar esse padrão:

  • Defina as prioridades no contexto da solução. Por exemplo, uma mensagem de alta prioridade pode ser definida como uma mensagem que deve ser processada dentro de 10 segundos. Identifique os requisitos para lidar com itens de alta prioridade e os recursos que precisam ser alocados para atender aos seus critérios.

  • Decida se todos os itens de alta prioridade devem ser processados antes de qualquer item de prioridade inferior. Se as mensagens forem processadas por um único grupo de consumidores, você precisará fornecer um mecanismo que possa antecipar e suspender uma tarefa que está lidando com uma mensagem de baixa prioridade se uma mensagem de prioridade mais alta entrar na fila.

  • Na abordagem de fila múltipla, quando você usa um único pool de processos de consumidor que escutam em todas as filas em vez de um pool de consumidores dedicado para cada fila, o consumidor deve aplicar um algoritmo que garanta que ele sempre atenda mensagens de filas de prioridade mais alta antes de mensagens de filas de prioridade mais baixa.

  • Monitore a velocidade de processamento em filas de alta e baixa prioridade para garantir que as mensagens nessas filas sejam processadas nas taxas esperadas.

  • Se você precisar garantir que as mensagens de baixa prioridade serão processadas, implemente a abordagem de fila de mensagens múltiplas com vários pools de consumidores. Como alternativa, em uma fila que ofereça suporte à priorização de mensagens, você pode aumentar dinamicamente a prioridade de uma mensagem enfileirada à medida que ela envelhece. No entanto, esta abordagem depende da fila de mensagens que fornece esta funcionalidade.

  • A estratégia de usar filas separadas com base na prioridade da mensagem é recomendada para sistemas que têm algumas prioridades bem definidas.

  • O sistema pode determinar logicamente as prioridades da mensagem. Por exemplo, em vez de ter mensagens explícitas de alta e baixa prioridade, você pode designar mensagens como "cliente pagante" ou "cliente não pagante". Seu sistema poderia então alocar mais recursos para processar mensagens de clientes pagantes.

  • Pode haver um custo financeiro e de processamento associado à verificação de uma fila para uma mensagem. Por exemplo, alguns sistemas de mensagens comerciais cobram uma pequena taxa cada vez que uma mensagem é postada ou recuperada, e cada vez que uma fila é consultada para mensagens. Esse custo aumenta quando você verifica várias filas.

  • Você pode ajustar dinamicamente o tamanho de um pool de consumidores com base no comprimento da fila que o pool está atendendo. Para obter mais informações, consulte Diretrizes de dimensionamento automático.

Quando utilizar este padrão

Este padrão é prático em cenários onde:

  • O sistema tem de processar várias tarefas com diferentes prioridades.

  • Diferentes usuários ou locatários devem ser atendidos com prioridades diferentes.

Design da carga de trabalho

Um arquiteto deve avaliar como o padrão de Fila de Prioridade pode ser usado no design de sua carga de trabalho para abordar as metas e os princípios abordados nos pilares do Azure Well-Architected Framework. Por exemplo:

Pilar Como esse padrão suporta os objetivos do pilar
As decisões de projeto de confiabilidade ajudam sua carga de trabalho a se tornar resiliente ao mau funcionamento e a garantir que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. Separar os itens com base na prioridade de negócios permite que você concentre os esforços de confiabilidade no trabalho mais crítico.

- RE:02 Fluxos críticos
- RE:07 Trabalhos em segundo plano
A Eficiência de Desempenho ajuda sua carga de trabalho a atender às demandas de forma eficiente por meio de otimizações em escala, dados e código. Separar os itens com base na prioridade do negócio permite que você concentre os esforços de desempenho no trabalho mais sensível ao tempo.

- PE:09 Fluxos críticos

Como em qualquer decisão de design, considere quaisquer compensações em relação aos objetivos dos outros pilares que possam ser introduzidos com esse padrão.

Exemplo

O Azure não fornece um mecanismo de enfileiramento que ofereça suporte nativo à priorização automática de mensagens por meio da classificação. No entanto, ele fornece tópicos do Barramento de Serviço do Azure, assinaturas do Barramento de Serviço que dão suporte a um mecanismo de enfileiramento que fornece filtragem de mensagens e uma variedade de recursos flexíveis que tornam o Azure ideal para a maioria das implementações de fila de prioridade.

Uma solução do Azure pode implementar um tópico do Service Bus no qual um aplicativo pode postar mensagens, assim como as publicaria em uma fila. As mensagens podem conter metadados sob a forma de propriedades personalizadas definidas pela aplicação. Você pode associar assinaturas do Service Bus ao tópico e as assinaturas podem filtrar mensagens com base em suas propriedades. Quando um aplicativo envia uma mensagem para um tópico, a mensagem é direcionada para a assinatura apropriada, onde um consumidor pode lê-la. Os processos do consumidor podem recuperar mensagens de uma assinatura usando a mesma semântica que usariam com uma fila de mensagens. (Uma assinatura é uma fila lógica.) Este diagrama mostra como implementar uma fila de prioridade usando tópicos e assinaturas do Service Bus:

Diagrama que mostra como implementar uma fila de prioridade usando tópicos e assinaturas do Service Bus.

No diagrama anterior, o aplicativo cria várias mensagens e atribui uma propriedade personalizada chamada Priority em cada mensagem. Priority tem um valor de High ou Low. A aplicação publica estas mensagens num tópico. O tópico tem duas assinaturas associadas que filtram Priority mensagens com base na propriedade. Uma assinatura aceita mensagens com a Priority propriedade definida como High. O outro aceita mensagens com a Priority propriedade definida como Low. Um conjunto de consumidores lê mensagens de cada subscrição. A assinatura de alta prioridade tem um pool maior, e esses consumidores podem estar executando em computadores mais potentes que têm mais recursos disponíveis do que os computadores para o pool de baixa prioridade.

Não há nada de especial sobre a designação de mensagens de alta e baixa prioridade neste exemplo. Eles são simplesmente rótulos que são especificados como propriedades em cada mensagem. Eles são usados para direcionar mensagens para uma assinatura específica. Se forem necessárias prioridades adicionais, é relativamente fácil criar mais subscrições e grupos de processos de consumo para lidar com essas prioridades.

A solução PriorityQueue no GitHub é baseada nessa abordagem. Esta solução contém projetos do Azure Function nomeados PriorityQueueConsumerHigh e PriorityQueueConsumerLow. Esses projetos do Azure Function se integram ao Service Bus por meio de gatilhos e associações. Eles se conectam a diferentes assinaturas definidas e ServiceBusTrigger reagem às mensagens recebidas.

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, você pode configurar quantas instâncias as funções no Serviço de Aplicativo do Azure podem ser dimensionadas. Você pode fazer isso configurando a opção Impor limite de expansão no portal do Azure, definindo um limite máximo de expansão para cada função. Normalmente, você precisa ter mais instâncias da PriorityQueueConsumerHigh função do que a PriorityQueueConsumerLow função. Essa configuração garante que as mensagens de alta prioridade sejam lidas da fila mais rapidamente do que as mensagens de baixa prioridade.

Outro projeto, PriorityQueueSender, contém uma função do Azure acionada por tempo que é configurada para ser executada a cada 30 segundos. Esta função integra-se com o Service Bus através de uma ligação de saída e envia lotes de mensagens de baixa e alta prioridade para um IAsyncCollector objeto. Quando a função posta mensagens no tópico associado às assinaturas usadas pelas PriorityQueueConsumerHigh funções e PriorityQueueConsumerLow , ela especifica a prioridade usando a Priority propriedade personalizada, conforme mostrado aqui:

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

Próximos passos

Os seguintes recursos podem ser úteis para você ao implementar esse padrão:

  • Um exemplo que demonstra esse padrão, no GitHub.

  • Cartilha de mensagens assíncrona. Um serviço de consumidor que processa um pedido pode precisar de enviar uma resposta para a instância da aplicação que publicou o pedido. Este artigo fornece informações sobre as estratégias que você pode usar para implementar mensagens de solicitação/resposta.

  • Orientação de dimensionamento automático. Às vezes, você pode dimensionar o tamanho do pool de processos de consumidor que estão lidando com uma fila com base no comprimento da fila. Essa estratégia pode ajudá-lo a melhorar o desempenho, especialmente para pools que lidam com mensagens de alta prioridade.

Os padrões a seguir podem ser úteis para você ao implementar esse padrão:

  • Padrão de Consumidores Concorrentes. Para aumentar a taxa de transferência das filas, você pode implementar vários consumidores que escutam na mesma fila e processam tarefas em paralelo. Estes consumidores competem pelas mensagens, mas apenas um deve ser capaz de processar cada mensagem. Este artigo fornece mais informações sobre os benefícios e desvantagens da implementação dessa abordagem.

  • Padrão de Limitação. Pode implementar a limitação com filas. Você pode usar mensagens prioritárias para garantir que as solicitações de aplicativos críticos, ou aplicativos executados por clientes de alto valor, tenham prioridade sobre as solicitações de aplicativos menos importantes.