Padrão de Consumidores Concorrentes

Permita que vários consumidores em simultâneo processem mensagens recebidas no mesmo canal de mensagens. Um sistema pode, assim, processar várias mensagens em simultâneo para otimizar o débito, melhorar a escalabilidade e disponibilidade e balancear a carga de trabalho.

Contexto e problema

Espera-se que uma aplicação em execução na nuvem processe um grande número de pedidos. Em vez de processar cada pedido de forma síncrona, uma técnica comum passa por colocar a aplicação a transmiti-los através de um sistema de mensagens para outro serviço (um serviço de consumidor) que os processa de forma assíncrona. Esta estratégia ajuda a garantir que a lógica de negócio na aplicação não é bloqueada enquanto os pedidos estão a ser processados.

O número de pedidos pode variar significativamente ao longo do tempo por diversos motivos. Um aumento repentino da atividade do utilizador ou dos pedidos agregados provenientes de vários inquilinos pode produzir uma carga de trabalho imprevisível. Nas horas de pico, um sistema pode ter de processar várias centenas de pedidos por segundo, enquanto noutras alturas o número pode ser muito pequeno. Além disso, a natureza do trabalho realizado para processar estes pedidos pode ser altamente variável. A utilização de uma única instância do serviço de consumidor pode fazer com que esta seja inundada com pedidos ou o sistema de mensagens pode ficar sobrecarregado por uma afluência de mensagens provenientes da aplicação. Para processar esta carga de trabalho flutuante, o sistema pode executar várias instâncias do serviço de consumidor. No entanto, estes consumidores têm de estar coordenados para garantir que cada mensagem é apenas enviada para um único consumidor. A carga de trabalho também tem de ser balanceada entre os consumidores para impedir que uma instância se torne num estrangulamento.

Solução

Utilize uma fila de mensagens para implementar o canal de comunicação entre a aplicação e as instâncias do serviço de consumidor. A aplicação publica pedidos sob a forma de mensagens na fila. Por sua vez, as instâncias do serviço de consumidor recebem as mensagens da fila e processam-nas. Esta abordagem permite que o mesmo conjunto de instâncias do serviço de consumidor processe mensagens de qualquer instância da aplicação. A figura mostra a utilização de uma fila de mensagens para distribuir o trabalho para as instâncias de um serviço.

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

Esta solução possui os benefícios seguintes:

  • Fornece um sistema de carga redistribuída que pode processar grandes variações no volume de pedidos enviados pelas instâncias da aplicação. A fila funciona como uma memória intermédia entre as instâncias da aplicação e as instâncias do serviço de consumidor. Este procedimento pode ajudar a minimizar o impacto sobre a disponibilidade e sobre a capacidade de resposta tanto para a aplicação como para as instâncias do serviço, conforme descrito pelo padrão de Redistribuição de Carga Baseada na Fila. O processamento de uma mensagem que precisa de um processamento de execução longa não impede que as outras mensagens sejam processadas em simultâneo por outras instâncias do serviço de consumidor.

  • Melhora a fiabilidade. Se um produtor comunicar diretamente com um consumidor em vez de utilizar este padrão, mas não monitorizar o consumidor, existirá uma grande probabilidade de as mensagens se perderem ou não serem processadas se o consumidor falhar. Neste padrão, as mensagens não são enviadas para uma instância de serviço específica. Uma instância de serviço que falha não bloqueia um produtor e as mensagens podem ser processadas por qualquer instância de serviço em funcionamento.

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

  • É escalável. O sistema pode aumentar ou reduzir de forma dinâmica o número de instâncias do serviço de consumidor à medida que o volume de mensagens varia.

  • Poderá melhorar a resiliência se a fila de mensagens fornecer operações de leitura transacionais. Se uma instância do serviço de consumidor ler e processar a mensagem como parte de uma operação transacional e se a instância do serviço de consumidor falhar, este padrão poderá garantir que a mensagem será devolvida à fila para que seja recolhida e processada por outra instância do serviço de consumidor.

Problemas e considerações

Na altura de decidir como implementar este padrão, considere os seguintes pontos:

  • Ordenação de mensagens. A ordem pela qual as instâncias do serviço de consumidor recebem as mensagens não é garantida nem reflete necessariamente a ordem pela qual as mensagens foram criadas. Crie o sistema para garantir que o processamento das mensagens é idempotente. Eliminará assim qualquer dependência relativamente à ordem de processamento das mensagens. Para obter mais informações, veja Idempotency Patterns (Padrões de Idempotência) no blogue de Jonathan Oliver.

    As Filas do Microsoft Azure Service Bus podem implementar uma ordenação de mensagens first-in-first-out garantida com sessões de mensagens. Para obter mais informações, veja Messaging Patterns Using Sessions (Padrões de Mensagens com Sessões).

  • Criar serviços para fins de resiliência. Se o sistema tiver sido concebido para detetar e reiniciar instâncias de serviço que falham, poderá ser necessário implementar o processamento realizado pelas instâncias do serviço como operações idempotentes para minimizar os efeitos de uma única mensagem a ser obtida e processada mais do que uma vez.

  • Detetar mensagens não processáveis. Uma mensagem incorretamente formada ou uma tarefa que requer acesso a recursos indisponíveis pode fazer com que uma instância de serviço falhe. O sistema deve evitar que estas mensagens sejam devolvidas à fila. Em vez disso, deve capturar e armazenar os detalhes destas mensagens noutro local para que possam ser analisadas, se necessário.

  • Processar resultados. A instância de serviço a processar uma mensagem está totalmente desacoplada da lógica aplicacional que gera a mensagem e pode não ter a capacidade de comunicar diretamente. Se a instância de serviço gerar resultados que têm de ser transmitidos para a lógica aplicacional, estas informações terão de ser armazenadas numa localização que esteja acessível para ambos. Para evitar que a lógica aplicacional obtenha dados incompletos, o sistema tem de indicar quando é que o processamento é concluído.

    Se estiver a utilizar o Azure, um processo de trabalho poderá transmitir os resultados de volta para a lógica aplicacional com uma fila de respostas de mensagens dedicada. A lógica aplicacional tem de conseguir correlacionar estes resultados com a mensagem original. Este cenário é descrito com maior detalhe no Asynchronous Messaging Primer (Manual Básico de Mensagens Assíncronas).

  • Dimensionar o sistema de mensagens. Numa solução em grande escala, uma fila de mensagens única pode ficar sobrecarregada pelo número de mensagens e tornar-se num estrangulamento no sistema. Nesta situação, considere a criação de partições do sistema de mensagens para enviar mensagens de produtores específicos a uma fila específica ou utilize o balanceamento de carga para distribuir as mensagens por várias filas de mensagens.

  • Garantir a fiabilidade do sistema de mensagens. É necessário um sistema de mensagens fiável para assegurar que, após a aplicação colocar uma mensagem em fila, esta não é perdida. Esta é uma condição é essencial para garantir que todas as mensagens são fornecidas pelo menos uma vez.

Quando utilizar este padrão

Utilize este padrão quando:

  • A carga de trabalho de uma aplicação está dividida em tarefas que podem ser executadas de forma assíncrona.
  • As tarefas são independentes e podem ser executadas em paralelo.
  • O volume de trabalho é altamente variável e requer uma solução dimensionável.
  • A solução tem de fornecer uma elevada disponibilidade e tem de ser resiliente caso ocorra uma falha no processamento de uma tarefa.

Este padrão poderá não ser prático quando:

  • Não é fácil separar a carga de trabalho da aplicação em tarefas discretas ou há um elevado grau de dependência entre as tarefas.
  • As tarefas devem ser executadas de forma síncrona e a lógica aplicacional tem de aguardar pela conclusão de uma tarefa antes de continuar.
  • As tarefas têm de ser executadas numa sequência específica.

Alguns sistemas de mensagens suportam sessões que permitem a um produtor agrupar mensagens e asseguram que estas são processadas pelo mesmo consumidor. Este mecanismo pode ser utilizado com mensagens prioritárias (se suportadas) para implementar uma forma de ordenação de mensagens em sequência de um produtor para um único consumidor.

Exemplo

A Azure fornece filas de autocarros de serviço e a fila Azure Function dispara que, quando combinado, são uma implementação direta deste padrão de design de nuvem. As funções Azure integram-se com o Azure Service Bus através de gatilhos e encadernações. A integração com o Service Bus permite-lhe construir funções que consomem mensagens de fila enviadas pelos editores. A(s) aplicação de publicação publicará mensagens para uma fila, e os consumidores, implementados como Funções Azure, podem recolher mensagens desta fila e manuseá-las.

Para a resiliência, uma fila de Service Bus permite ao consumidor utilizar PeekLock o modo de utilização quando recupera uma mensagem da fila; este modo não remove a mensagem, mas simplesmente esconde-a de outros consumidores. O tempo de funcionamento das funções Azure recebe uma mensagem no modo PeekLock, se a função terminar com sucesso, chama Complete na mensagem, ou pode chamar Abandono se a função falhar, e a mensagem tornar-se-á visível novamente, permitindo que outro consumidor a recupere. Se a função funcionar durante um período superior ao tempo limite do PeekLock, o bloqueio é automaticamente renovado enquanto a função estiver a funcionar.

As Funções Azure podem escalar para fora/em com base na profundidade da fila, todas atuando como consumidores concorrentes da fila. Se forem criados múltiplos casos das funções, todos competem puxando e processando as mensagens de forma independente.

Para obter informações detalhadas sobre a utilização das filas do Azure Service Bus, veja Filas, tópicos e subscrições do Service Bus.

Para obter informações sobre as funções Azure ativadas, consulte o gatilho do ônibus da Azure Service para as funções Azure.

O código que se segue mostra como pode criar uma nova mensagem e enviá-la para uma fila de autocarros de serviço utilizando uma QueueClient instância.

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

O exemplo de código que se segue mostra um consumidor, escrito como uma Função Azure C# que lê metadados de mensagens e regista uma mensagem de fila de autocarros de serviço. Note como o ServiceBusTrigger atributo é usado para ligá-lo a uma fila de autocarros 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}");
}

Os padrões e as orientações que se seguem podem ser relevantes ao implementar este padrão:

  • Asynchronous Messaging Primer (Manual Básico de Mensagens Assíncronas). As Filas de mensagens são um mecanismo de comunicação assíncrono. Se um serviço de consumidor precisar de enviar uma resposta para uma aplicação, poderá ser necessário implementar alguma forma de mensagem de resposta. O Manual Básico de Mensagens Assíncronas fornece informações sobre como implementar mensagens de pedido/resposta com filas de mensagens.

  • Orientação de autoescalagem. Pode ser possível iniciar e parar instâncias de um serviço de consumidor, uma vez que o comprimento da fila na qual as aplicações publicam mensagens varia. O dimensionamento automático pode ajudar a manter o débito durante os períodos de pico de processamento.

  • Padrão de consolidação de recursos computacional. Pode ser possível consolidar várias instâncias de um serviço de consumidor num único processo para reduzir os gastos e os custos de gestão. O padrão de Consolidação de Recursos de Computação descreve os benefícios e os compromissos desta abordagem.

  • Padrão de nivelamento de carga baseado na fila. A introdução de uma fila de mensagens pode adicionar resiliência ao sistema, o que permite que as instâncias de serviço processem diversos e variados volumes de pedidos de instâncias da aplicação. A fila de mensagens funciona como uma memória intermédia que redistribui a carga. O padrão de Redistribuição de Carga Baseada na Fila descreve este cenário mais detalhadamente.