Padrão de fila de prioridade

Barramento de Serviço do Azure

Priorize as solicitações enviadas a serviços para que as solicitações com uma prioridade mais alta sejam recebidas e processadas mais rapidamente do que aquelas com uma prioridade mais baixa. Esse padrão é útil em aplicativos que oferecem garantias de nível de serviço diferentes para clientes individuais.

Contexto e problema

Os aplicativos podem delegar tarefas específicas para outros serviços, por exemplo, para executar processamento em segundo plano ou para se integrar com outros aplicativos ou serviços. Na nuvem, uma fila de mensagens é normalmente usada para delegar as tarefas para processamento em segundo plano. Em muitos casos, a ordem em que os pedidos são recebidos por um serviço não é importante. No entanto, em alguns casos, é necessário priorizar solicitações específicas. Essas solicitações devem ser processadas antes que as solicitações de prioridade mais baixa enviadas anteriormente pelo aplicativo.

Solução

Uma fila é geralmente uma estrutura PEPS (primeiro a entrar, primeiro a sair), e os consumidores normalmente recebem mensagens na mesma ordem em que elas foram postadas na fila. No entanto, algumas filas de mensagens dão suporte a mensagens de prioridade. O aplicativo que está postando uma mensagem pode atribuir uma prioridade. As mensagens na fila são reordenadas automaticamente para que aquelas que têm prioridade mais alta sejam recebidas antes daquelas que têm prioridade mais baixa. Este diagrama ilustra o processo:

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

Observação

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

Em sistemas que não dão suporte a filas de mensagens baseadas em prioridade, uma solução alternativa é manter uma fila separada para cada prioridade. O aplicativo é responsável por postar mensagens na fila apropriada. Cada fila pode ter um pool separado de consumidores. Filas de prioridade mais alta podem ter um pool maior de consumidores em execução no hardware mais rápido 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 verifica se há mensagens em filas de alta prioridade primeiro e só depois começa a buscar mensagens das filas de prioridade mais baixa. Há algumas diferenças semânticas entre uma solução que usa um único pool de processos do consumidor (com uma única fila que dá suporte a mensagens com prioridades diferentes ou com várias filas que cada uma lida com 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 sempre são recebidas e processadas antes das mensagens de prioridade mais baixa. Em teoria, as mensagens de baixa prioridade poderiam ser continuamente substituídas e nunca seriam 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 que estão disponíveis para eles).

Usar um mecanismo de enfileiramento de prioridade pode fornecer as seguintes vantagens:

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

  • Ele 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. Mensagens de alta prioridade ainda serão processadas primeiro (embora possivelmente de modo mais lento), e mensagens de prioridade mais baixa podem ser atrasadas por mais tempo. Se você implementar a abordagem de filas de mensagens múltiplas 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 de algumas filas de prioridade muito baixa, parando todos os consumidores que escutam mensagens nessas filas.

  • A abordagem de várias filas de mensagens pode ajudar a maximizar o desempenho e a escalabilidade do aplicativo por meio do particionamento das mensagens com base nos requisitos de processamento. Por exemplo, você pode priorizar tarefas críticas para que sejam manipuladas por receptores que executam imediatamente, e tarefas em segundo plano menos importantes podem ser tratadas por receptores agendados para execução em períodos 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 poderia ser definida como uma mensagem que deveria 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 dos itens de prioridade mais baixa. Se as mensagens são processadas por um único pool de consumidores, você precisa fornecer um mecanismo que pode impedir e suspender uma tarefa que lida com uma mensagem de baixa prioridade se uma mensagem de prioridade mais alta entrar na fila.

  • Na abordagem de várias filas, ao usar um único pool de processos do consumidor que escuta todas as filas em vez de um pool de consumidores exclusivo para cada fila, o consumidor deve aplicar um algoritmo que garante que ele sempre atenda as mensagens das filas de prioridade mais alta antes de mensagens de filas de prioridade mais baixa.

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

  • Se você precisa garantir que as mensagens de baixa prioridade serão processadas, implemente a abordagem de várias filas de mensagens com vários pools de consumidores. Como alternativa, em uma fila que dá suporte à priorização de mensagem, é possível aumentar dinamicamente a prioridade de uma mensagem na fila à medida que ela fica antiga. No entanto, essa abordagem depende da fila de mensagens fornecer esse recurso.

  • A estratégia de utilizar filas separadas com base na prioridade das mensagens é recomendada para sistemas que possuem algumas prioridades bem definidas.

  • O sistema pode determinar logicamente as prioridades das mensagens. Por exemplo, em vez de ter mensagens explícitas de alta e baixa prioridade, você poderia 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 mensagem em uma fila. 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 em busca de mensagens. Esse custo aumenta ao verificar várias filas.

  • É possível 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, confira Diretrizes de dimensionamento automático.

Quando usar esse padrão

Esse padrão é útil em cenários em que:

  • O sistema deve lidar com várias tarefas que têm prioridades diferentes.

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

Design de 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 apoia os objetivos do pilar
As decisões de design 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 itens com base na prioridade dos 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 com eficiência às demandas por meio de otimizações em dimensionamento, dados e código. Separar itens com base na prioridade dos negócios permite que você concentre os esforços de desempenho no trabalho mais sensível ao tempo.

- PE:09 Fluxos críticos

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

Exemplo

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

Uma solução do Azure pode implementar um tópico de Barramento de Serviço no qual um aplicativo pode postar mensagens, da mesma forma que as postaria em uma fila. As mensagens podem conter metadados na forma de propriedades personalizadas definidas pelo aplicativo. Você pode associar assinaturas de Barramento de Serviço 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 prioritária usando tópicos e assinaturas de Barramento de Serviço:

Diagrama que mostra como implementar uma fila de prioridade usando tópicos e assinaturas do Barramento de Serviço.

No diagrama anterior, o aplicativo cria diversas mensagens e atribui uma propriedade personalizada chamada Priority em cada mensagem. Priority tem um valor de High ou Low. O aplicativo posta essas mensagens em um tópico. O tópico tem duas assinaturas associadas que filtram mensagens com base na propriedade Priority. Uma assinatura aceita mensagens com a propriedade Priority definida como High. A outra aceita mensagens com a propriedade Priority definida como Low. Um pool de consumidores lê as mensagens de cada assinatura. A assinatura de alta prioridade tem um pool maior, e esses consumidores podem estar em execução em computadores mais potentes com mais recursos disponíveis do que os computadores do pool de baixa prioridade.

Observe que não há nada de especial sobre a designação de mensagens de prioridade alta e baixa neste exemplo. São simplesmente rótulos especificados como propriedades em cada mensagem. São usados para direcionar mensagens para uma assinatura específica. Se forem necessárias prioridades adicionais, será relativamente fácil criar mais assinaturas e pools de processos do consumidor para lidar com essas prioridades.

A solução PriorityQueue no GitHub é baseada nessa abordagem. Essa solução contém projetos do Azure Function chamados PriorityQueueConsumerHigh e PriorityQueueConsumerLow. Esses projetos da Função do Azure integram-se com o Barramento de Serviço por meio de gatilhos e associações. Eles se conectam a diferentes assinaturas definidas em ServiceBusTrigger e 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 Enforce Scale Out Limit do portal do Azure, definindo um limite máximo de expansão para cada função. Geralmente você precisa ter mais instâncias da função PriorityQueueConsumerHigh do que da função PriorityQueueConsumerLow. Essa configuração garante que as mensagens de alta prioridade sejam lidas na fila mais rapidamente do que as mensagens de baixa prioridade.

Outro projeto, PriorityQueueSender, contém uma função do Azure acionada por tempo configurada para ser executada a cada 30 segundos. Esta função integra-se ao Barramento de Serviço por meio de uma ligação de saída e envia lotes de mensagens de baixa e alta prioridade para um objeto IAsyncCollector. Quando a função publica mensagens no tópico associado às assinaturas usadas pelas funções PriorityQueueConsumerHigh e PriorityQueueConsumerLow, ela especifica a prioridade usando a propriedade personalizada Priority, 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óximas etapas

Os recursos a seguir podem ser úteis ao implementar esse padrão:

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

  • Prévia de mensagens assíncronas. Um serviço do consumidor que processa uma solicitação talvez precise enviar uma resposta para a instância do aplicativo que postou a solicitação. Este artigo fornece informações sobre as estratégias que você pode usar para implementar mensagens de solicitação/resposta.

  • Diretrizes de dimensionamento automático. Às vezes, você pode dimensionar o tamanho do conjunto de processos consumidores que manipulam uma fila com base no comprimento da fila. Essa estratégia pode ajudar a melhorar o desempenho, especialmente para pools que lidam com mensagens de alta prioridade.

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

  • Padrão de consumidores concorrentes. Para aumentar o rendimento das filas, você pode implementar vários consumidores que escutam na mesma fila e processam tarefas em paralelo. Esses consumidores competirão por 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. Você pode implementar a limitação usando 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.