Padrão de Consumidores Concorrentes

Funções do Azure
Barramento de Serviço do Azure

Habilite vários consumidores simultâneos para processar as mensagens recebidas no mesmo canal de mensagens. Com vários consumidores simultâneos, um sistema pode processar várias mensagens simultaneamente para otimizar a taxa de transferência, melhorar a escalabilidade e a disponibilidade e equilibrar a carga de trabalho.

Contexto e problema

É esperado que um aplicativo em execução na nuvem lide com um grande número de solicitações. Em vez de processar cada solicitação de forma síncrona, uma técnica comum é o aplicativo passá-las por meio de um sistema de mensagens para outro serviço (um serviço consumidor) que as trata de forma assíncrona. Essa estratégia ajuda a garantir que a lógica de negócios do aplicativo não seja bloqueada enquanto as solicitações estão sendo processadas.

O número de solicitações pode variar significativamente com o passar do tempo por vários motivos. Um aumento repentino de atividade do usuário ou solicitações agregadas provenientes de vários locatários pode causar uma carga de trabalho imprevisível. Nos horário de pico de um sistema, pode ser necessário processar centenas de solicitações por segundo, enquanto em outros momentos o número pode ser muito pequeno. Além disso, a natureza do trabalho executado para lidar com essas solicitações pode ser altamente variável. Ao usar uma única instância do serviço do consumidor, você pode fazer com que essa instância seja inundada com solicitações. Ou o sistema de mensagens pode estar sobrecarregado por um influxo de mensagens provenientes do aplicativo. Para lidar com essa carga de trabalho flutuante, o sistema pode executar várias instâncias do serviço do consumidor. No entanto, esses consumidores devem ser coordenados para garantir que cada mensagem seja entregue somente para um único consumidor. A carga de trabalho também precisa ser balanceada entre os consumidores para impedir que uma instância se torne um gargalo.

Solução

Use uma fila de mensagens para implementar o canal de comunicação entre o aplicativo e as instâncias do serviço do consumidor. O aplicativo posta solicitações na forma de mensagens na fila e as instâncias de serviço do consumidor recebem mensagens da fila e as processa. Essa abordagem permite que o mesmo pool de instâncias de serviço do consumidor lide com mensagens de qualquer instância do aplicativo. A figura ilustra o uso de uma fila de mensagens para distribuir o trabalho para instâncias de um serviço.

Usar uma fila de mensagens para distribuir o trabalho para instâncias de um serviço

Observação

Embora existam vários consumidores dessas mensagens, não é o mesmo que o padrão Publicar/Assinar (pub/sub). Com a abordagem de Consumidores Concorrentes, cada mensagem é passada para um único consumidor para processamento, enquanto com a abordagem Pub/Sub, todos os consumidores recebem cada every mensagem.

Essa solução oferece as seguintes vantagens:

  • Ele fornece um sistema para redistribuir a carga que pode lidar com grandes variações no volume de solicitações enviadas por instâncias de aplicativo. A fila atua como um buffer entre as instâncias do aplicativo e as instâncias de serviço do consumidor. Esse buffer pode ajudar a minimizar o impacto na disponibilidade e capacidade de resposta, tanto para o aplicativo quanto para as instâncias de serviço. Para obter mais informações, confira Padrão de Nivelamento de carga baseado em fila. Lidar com uma mensagem que requer que processamento de longa execução não impede que outras mensagens sejam tratadas simultaneamente por outras instâncias do serviço do consumidor.

  • Isso aumenta a confiabilidade. Se um produtor se comunica diretamente com um consumidor em vez de usar esse padrão, mas não monitora o consumidor, há uma grande probabilidade de que as mensagens poderiam ser perdidas ou não conseguirem ser processadas se o consumidor falhar. Nesse padrão, as mensagens não são enviadas para uma instância de serviço específica. Uma instância de serviço com falha não bloqueará um produtor e as mensagens podem ser processadas por qualquer instância de serviço do trabalho.

  • Ela não requer coordenação complexa entre os consumidores ou entre as instâncias do produtor e do consumidor. A fila de mensagens garante que cada mensagem seja entregue pelo menos uma vez.

  • É escalonável. Quando você aplica dimensionamento automático, o sistema pode aumentar ou diminuir dinamicamente o número de instâncias do serviço ao consumidor conforme o volume de mensagens flutua.

  • Isso poderá melhorar a resiliência se a fila de mensagens fornecer operações de leitura transacionais. Se uma instância de serviço do consumidor lê e processa a mensagem como parte de uma operação transacional e a instância de serviço do consumidor falhar, esse padrão pode garantir que a mensagem seja retornada para a fila para ser coletada e tratada por outra instância e serviço do consumidor. Para mitigar o risco de falha contínua de uma mensagem, recomendamos que você use filas de mensagens mortas.

Problemas e considerações

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

  • Ordenação de mensagem. A ordem na qual as instâncias de serviço de consumidor recebem as mensagens não é garantida e não reflete necessariamente a ordem na qual as mensagens foram criadas. Projete o sistema para garantir que o processamento de mensagens seja idempotente, pois isso ajudará a eliminar qualquer dependência em relação à ordem na qual as mensagens são manipuladas. Para obter mais informações, confira Padrões de idempotência no blog de Jonathon Oliver.

    As Filas do Barramento de Serviço do Microsoft Azure podem implementar ordenação primeiro a entrar, primeiro a sair garantida de mensagens usando sessões de mensagens. Para obter mais informações, consulte Padrões de mensagens usando sessões.

  • Projetar serviços para resiliência. Se o sistema foi projetado para detectar e reiniciar as instâncias de serviço com falha, pode ser necessário implementar o processamento executado pelas instâncias do serviço como operações idempotentes para minimizar os efeitos de uma única mensagem sendo recuperada e processada mais de uma vez.

  • Detecção de mensagens suspeitas. Uma mensagem malformada ou uma tarefa que exige acesso a recursos que não estão disponíveis, pode causar uma falha em uma instância de serviço. O sistema deve impedir que essas mensagens sejam retornadas para a fila, capturando-as em vez disso e armazenando os detalhes dessas mensagens em outro local para que possam ser analisados, se necessário.

  • Lidar com resultados. A instância de serviço que lidar com uma mensagem é totalmente separada da lógica do aplicativo que gera a mensagem e pode não ser capaz de se comunicar diretamente. Se a instância de serviço gerar resultados que devem ser passados para a lógica do aplicativo, essas informações devem ser armazenadas em um local que seja acessível para ambos. Para impedir que a lógica do aplicativo recupere dados incompletos, o sistema deve indicar quando o processamento é concluído.

    Se você estiver usando o Azure, um processo de trabalho pode retornar resultados para a lógica do aplicativo por meio de uma fila de resposta de mensagem dedicada. A lógica do aplicativo deve ser capaz de correlacionar esses resultados com a mensagem original. Esse cenário é descrito em mais detalhes no Primer de mensagens assíncronas.

  • Dimensionando o sistema de mensagens. Em uma solução de grande escala, uma única fila de mensagens poderia ficar sobrecarregada com o número de mensagens e se tornar um gargalo no sistema. Nessa situação, considere particionar o sistema de mensagens para enviar as mensagens de produtores específicos para determinada fila ou usar o balanceamento de carga para distribuir mensagens em várias filas de mensagens.

  • Assegurar a confiabilidade do sistema de mensagens. Um sistema de mensagens confiável é necessário para garantir uma mensagem não seja perdida após o aplicativo enfileirá-la. Este sistema é essencial para garantir que todas as mensagens sejam entregues pelo menos uma vez.

Quando usar esse padrão

Use esse padrão quando:

  • A carga de trabalho de um aplicativo é dividida em tarefas que podem ser executadas de forma assíncrona.
  • As tarefas são independentes e podem ser executados em paralelo.
  • O volume de trabalho é altamente variável, o que exige uma solução escalonável.
  • A solução deve fornecer alta disponibilidade e ser resiliente se o processamento de uma tarefa falhar.

Esse padrão pode não ser útil quando:

  • Não for fácil separar a carga de trabalho do aplicativo em tarefas discretas ou se há um alto grau de dependência entre as tarefas.
  • As tarefas devem ser executadas de forma síncrona e a lógica do aplicativo deve esperar que uma tarefa seja concluída antes de continuar.
  • As tarefas devem ser executadas em uma sequência específica.

Alguns sistemas de mensagens dão suporte a sessões que permitem que um produtor agrupe as mensagens e verifique se elas são manipuladas pelo mesmo consumidor. Esse mecanismo pode ser usado com mensagens priorizadas (se houver suporte para elas) para implementar uma forma de ordenação de mensagens que entrega as mensagens na sequência de um produtor para um único consumidor.

Design de carga de trabalho

Um arquiteto deve avaliar como o padrão Consumidores Concorrentes 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. Esse padrão cria redundância no processamento de filas, tratando os consumidores como réplicas, de modo que uma falha de instância não impede que outros consumidores processem mensagens de fila.

- RE:05 Redundância
- RE:07 Trabalhos em segundo plano
A otimização de custos se concentra em sustentar e melhorar o retorno sobre o investimento da sua carga de trabalho. Esse padrão pode ajudar você a otimizar custos, permitindo o dimensionamento baseado na profundidade da fila, até zero quando a fila está vazia. Também pode otimizar custos, permitindo limitar o número máximo de instâncias de consumidores simultâneas.

- CO:05 Otimização de taxas
- CO:07 Custos de componentes
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. Distribuir a carga por todos os nós consumidores aumenta a utilização e o escalonamento dinâmico com base na profundidade da fila minimiza o superprovisionamento.

- PE:05 Dimensionamento e particionamento
- PE:07 Código e infraestrutura

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 fornece Filas de Barramento de Serviço e acionadores de fila do Azure Function que, quando combinados, são uma implementação direta desse padrão de design de nuvem. O Azure Functions integra-se com o Barramento de Serviço do Azure por meio de gatilhos e associações. A integração com o Barramento de Serviço permite criar funções que consomem mensagens de fila enviadas por editores. Os aplicativos de publicação postarão mensagens em uma fila e os consumidores, implementados como Azure Functions, podem recuperar mensagens dessa fila e tratá-las.

Para resiliência, uma fila do Barramento de Serviço permite que um consumidor use o modo PeekLock ao recuperar uma mensagem da fila; esse modo na verdade não remove a mensagem, mas simplesmente a oculta de outros consumidores. O tempo de execução do Azure Functions recebe uma mensagem no modo PeekLock, se a função for concluída com êxito, ela chamará Concluir na mensagem ou poderá chamar Abandonar se a função falhar e a mensagem ficará visível novamente, permitindo que outro consumidor a recupere. Se a função for executada por um período maior que o tempo limite do PeekLock, o bloqueio será renovado automaticamente enquanto a função estiver em execução.

O Azure Functions pode aumentar/diminuir com base na profundidade da fila, todos agindo como consumidores concorrentes da fila. Se várias instâncias das funções forem criadas, todas elas competirão extraindo e processando as mensagens de forma independente.

Para ver informações detalhadas sobre como usar filas do Barramento de Serviço do Azure, consulte Filas, tópicos e assinaturas do Barramento de Serviço.

Para obter informações sobre o Azure Functions acionado por fila, confira Gatilho do Barramento de Serviço do Azure para Azure Functions.

O código a seguir mostra como você pode criar uma nova mensagem e enviá-la para uma fila do barramento de serviço usando uma instância 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);
   }
  }

O exemplo de código a seguir mostra um consumidor, escrito como C# Azure Function, que lê metadados de mensagem e registra uma mensagem de fila do barramento de serviço. Observe como o atributo ServiceBusTrigger é usado para associá-lo a uma Fila do Barramento de Serviço.

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

Próximas etapas

  • Prévia de mensagens assíncronas. Filas de mensagens são um mecanismo de comunicação assíncrona. Se um serviço do consumidor precisa enviar uma resposta para um aplicativo, pode ser necessário implementar alguma forma de mensagens de resposta. O Primer de mensagens assíncronas fornece informações sobre como implementar mensagens de solicitação/resposta usando filas de mensagens.

  • Diretrizes de dimensionamento automático. Pode ser possível iniciar e parar instâncias de um serviço do consumidor, visto que o tamanho da fila na qual os aplicativos postam as mensagens varia. O dimensionamento automático pode ajudar a manter a taxa de transferência durante horários de pico de processamento.

Os padrões e diretrizes a seguir também podem ser relevantes ao implementar esse padrão:

  • Padrão de consolidação de recursos de computação. Pode ser possível consolidar várias instâncias de um serviço do consumidor em um único processo para reduzir os custos e a sobrecarga de gerenciamento. O Padrão de consolidação de recursos de computação descreve os benefícios e as vantagens e desvantagens de seguir essa abordagem.

  • Padrão de nivelamento de carga baseado em fila. Apresentar uma fila de mensagens pode agregar resiliência ao sistema, permitindo que as instâncias de serviço lidem com volumes variáveis de solicitações de instâncias do aplicativo. A fila de mensagens atua como um buffer que nivela a carga. O Padrão de nivelamento de carga com base em fila descreve esse cenário mais detalhadamente.