Padrão de Consumidor Idempotente

Projete os consumidores de mensagens para que processar a mesma mensagem mais do que uma vez tenha os mesmos efeitos que processá-la uma vez. Sistemas de mensagens que garantem pelo menos uma entrega podem entregar a mesma mensagem várias vezes. A resiliência contra duplicados garante que o reprocessamento de uma mensagem não cria registos duplicados, não cobra duas vezes um cliente ou tenha outros efeitos indesejados.

Contexto e problema

As aplicações distribuídas normalmente trocam trabalho através de um broker de mensagens em vez de usar chamadas síncronas diretas. A maioria dos brokers, incluindo Azure Service Bus, Hubs de Eventos do Azure, Apache Kafka e RabbitMQ, fornece entrega pelo menos uma vez. Esta garantia garante que uma mensagem chegue ao consumidor mesmo quando ocorrem falhas, mas também significa que o corretor pode entregar a mesma mensagem mais do que uma vez.

É impraticável garantir uma entrega única em todo um sistema distribuído. Mesmo os corretores que usam semântica de uma vez exata podem garantir apenas operações que controlam diretamente, como entregar mensagens aos consumidores ou escrever dados de volta ao corretor. Não conseguem controlar os efeitos secundários que os consumidores implementam em sistemas externos. A solução duradoura não é eliminar a entrega duplicada, mas sim fazer com que o consumidor a processe corretamente. Ao combinar a entrega pelo menos uma vez com um consumidor que ignora duplicatas, consegue-se um processamento efetivamente único.

Os duplicados podem surgir de várias fontes:

  • O produtor tenta tentar. Um produtor envia uma mensagem, não recebe confirmação devido a uma falha de rede transitória ou timeout, e envia a mensagem novamente. O corretor agora detém duas cópias, mesmo que o envio tenha sido bem-sucedido da primeira vez.

  • Reentrega após a ausência de confirmação de receção. Um consumidor recebe e processa uma mensagem, mas não a reconhece porque o consumidor crasha, o bloqueio expira ou o reconhecimento é perdido. O corretor assume que a mensagem não foi processada e entrega-a novamente.

  • Falhas do consumidor durante o processamento. Um consumidor conclui uma escrita na base de dados, mas crasha antes de reconhecer a mensagem. Outra instância recebe a mensagem e repete a operação de escrita.

Solução

Crie um consumidor idempotente fazendo com que o consumidor mantenha um registo das mensagens que processa com sucesso e ignore quaisquer mensagens já processadas, com base num identificador estável que sobreviva à reentrega. O consumidor verifica o armazenamento persistente de identificadores para determinar se já processou esse identificador, e depois processa a mensagem ou descarta-a como duplicada.

O fluxo de núcleo consiste nos seguintes passos:

  1. Leia a mensagem e extraia a sua chave de deduplicação.
  2. Verifique o armazenamento de deduplicação para essa chave.
  3. Se a chave já existir na loja, trate a mensagem como um duplicado. Confirme a mensagem e pare de a processar, devolvendo opcionalmente o resultado previamente registado.
  4. Se a chave ainda não existir no armazenamento, processa a mensagem e regista a chave numa única operação atómica, e depois confirma a mensagem.

As secções seguintes fornecem orientações para tornar os consumidores idempotentes:

Escolha uma chave de deduplicação estável

A chave de deduplicação deve identificar de forma única e consistente a mensagem lógica em cada reentrega. Use um identificador de mensagem atribuído pelo produtor ou uma chave de idempotência ao nível do negócio que identifique a operação lógica específica, não um contexto de correlação partilhado que várias mensagens possam transportar.

Por exemplo, defina a propriedade Service Bus MessageId para um valor que identifique de forma única a mensagem lógica. Não uses CorrelationId como chave, porque refere-se a grupos de mensagens relacionadas, como um pedido e as suas respostas. Para eventos que seguem a especificação CloudEvents, a combinação dos atributos source e id identifica de forma única um evento e permanece estável em todas as reentregas.

Não se baseie em identificadores de nível de transporte que o broker regenera após a reentrega, nem em valores derivados das tentativas de entrega. Esses valores mudam entre entregas e anulam a deteção de duplicados. Também evite derivar a chave de campos voláteis, como marcas temporais de receção.

Quando mais do que um consumidor independente processa o mesmo canal, como vários assinantes num design de publicação-subscrição, cada consumidor recebe a sua própria cópia de uma mensagem e precisa de acompanhar de forma independente a conclusão do processamento. Se os consumidores partilharem um armazenamento de desduplicação, use uma chave composta pela identidade do consumidor e pela identidade da mensagem para os registos. Uma loja com base apenas na identidade da mensagem permitiria ao primeiro consumidor suprimir o processamento de todas as outras.

Decida onde guardar as chaves processadas

As seguintes opções de armazenamento são comuns para chaves processadas:

  • Uma tabela de deduplicação dedicada. O consumidor mantém uma tabela separada, por vezes chamada caixa de entrada, que contém uma linha por cada chave processada. Esta abordagem mantém as preocupações com a deduplicação separadas dos dados empresariais e funciona bem se muitos tipos de mensagens partilharem o mesmo mecanismo.

  • A própria entidade empresarial. O consumidor armazena a chave no registo que a mensagem cria ou atualiza. Esta abordagem evita uma tabela separada, mas associa a deduplicação ao tipo de dados de negócio.

Compromete a chave processada e os efeitos secundários de forma atómica

Um fluxo de verificação seguida de processamento tem uma janela de falha. Se um consumidor processar uma mensagem e depois registar a chave numa etapa separada, uma falha entre as duas operações faz com que os efeitos secundários sejam aplicados, mas a chave fique por registar, pelo que o consumidor volta a processar a mensagem na próxima reentrega.

Evite esta janela de falha escrevendo o marcador de deduplicação e os efeitos secundários do negócio na mesma transação. Se um consumidor confirmar ambas as operações em conjunto ou não confirmar nenhuma delas, quando a mensagem for novamente entregue, ou encontra o marcador e ignora a mensagem, ou não encontra qualquer marcador porque a transação não foi concluída e reprocessa a mensagem em segurança. Esta variante transacional é o padrão Inbox e é o equivalente no lado do consumidor ao padrão Transactional Outbox pattern no lado do produtor.

Evitar duplicados simultâneos

Num regime de entrega de, pelo menos, uma vez com consumidores concorrentes, duas instâncias podem receber cópias da mesma mensagem ao mesmo tempo. Ambas as instâncias podem passar na verificação de existência antes de qualquer uma confirmar a transação, pelo que a verificação, por si só, não impede o processamento em duplicado.

Garantir a correção no armazenamento de dados em vez de na lógica da aplicação, adotando os seguintes passos:

  • Use uma restrição de unicidade na chave de deduplicação de modo que duas transações possam tentar inserir uma chave, mas apenas uma tenha sucesso. A outra transação falha na restrição e trata a mensagem como um duplicado. Esta abordagem faz da base de dados o único árbitro do conflito.

  • Evita corridas de check-then-set nos caches. Um padrão que verifica uma chave e depois a define em duas operações separadas tem uma janela que permite que tentativas simultâneas reclamem a chave. Use uma escrita condicional atómica, como uma inserção que falha em conflito ou uma operação set-if-absent, para tornar a reivindicação da chave um único passo atómico.

Trate os efeitos secundários que não podem fazer parte da transação

Alguns processos não podem participar na transação da base de dados do consumidor, como chamar uma API de terceiros ou escrever para uma loja externa. Utilize a seguinte abordagem bifásica para estes processos:

  1. Grava a chave com um estado em curso e depois realiza a ação externa.
  2. Atualize o registo para concluído e guarde o resultado.

Em caso de reentrega, um registo concluído indica ao consumidor que não repita a chamada. Um registo em andamento indica que uma tentativa anterior pode estar parcialmente concluída ou está a ser processada por outro consumidor. O consumidor deve conciliar registos obsoletos ou encaminhar os casos não resolvidos para intervenção antes de reconhecer a reentrega.

Problemas e considerações

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

  • Prefiro operações naturalmente idempotentes. Algumas operações são inerentemente idempotentes e não necessitam de contabilidade por deduplicação. Um upsert baseado num identificador de negócio, uma escrita que define um valor absoluto em vez de um incremento, ou um HTTP PUT para um identificador de recurso produzem os mesmos resultados quer sejam executados uma ou várias vezes.

    Por vezes, pode-se tornar uma operação naturalmente idempotente usando transferência de estado transportada por eventos. A mensagem contém o estado absoluto resultante, como o novo estado de uma encomenda, pelo que o consumidor a trata como uma operação de inserção ou atualização em vez de uma alteração relativa.

    Sugestão

    Projete para idempotência natural, se possível, e use técnicas de deduplicação apenas para operações que não possam ser tornadas naturalmente idempotentes.

  • Use um framework de mensagens em vez de configurar a deduplicação. A implementação correta do armazenamento com deduplicação, da confirmação e da limpeza é propensa a erros. Frameworks baseados em mensagens fornecem este padrão como uma funcionalidade incorporada.

    Por exemplo, o NServiceBus elimina mensagens duplicadas recebidas com base nos identificadores das mensagens e disponibiliza políticas configuráveis de retenção e limpeza dos dados de deduplicação. A caixa de saída do consumidor do MassTransit acompanha as mensagens recebidas pelos seus identificadores para fornecer um comportamento exato do consumidor.

  • Gerir o ciclo de vida dos registos de deduplicação. Os registos de deduplicação acumulam-se a menos que os definas para expirar. Retenha cada registo pelo menos enquanto o corretor ainda conseguir retransmitir a mensagem original. O tamanho desta janela depende do número máximo de tentativas de entrega do mediador de mensagens, do tempo limite de bloqueio ou de visibilidade e do tempo de vida da mensagem.

    Defina um tempo de vida nos registos de deduplicação que exceda essa janela, para que uma reentrega tardia ainda encontre o respetivo marcador. Eliminar registos demasiado cedo volta a criar a possibilidade de surgirem duplicados. Considere as mensagens que os operadores reenviam a partir de filas de mensagens não entregues, porque estes reenvios podem ocorrer muito depois de terminar o período normal de nova entrega.

  • Não substitua a lógica idempotente do consumidor por deduplicação por corretores. Algumas plataformas filtram duplicados na camada de transporte. Por exemplo, a deteção de duplicados do Service Bus descarta mensagens que repetem um MessageId dentro de uma janela de tempo configurada, o que suprime novas tentativas duplicadas de envio por parte do produtor.

    Esta funcionalidade opera do lado do envio e dentro de uma janela limitada, pelo que não impede um consumidor de processar a mesma mensagem duas vezes após uma reentrega. Ainda precisas de lógica de consumo idempotente. Use funcionalidades da plataforma para reduzir o volume duplicado, e não como substituto do padrão Idempotent Consumer.

  • Tenha em conta a ordem das mensagens. A deduplicação remove duplicados, mas não garante a ordem. Se o consumidor depende da ordem de processamento, combine este padrão com um mecanismo de ordenação, como sessões de mensagens do Service Bus, ou inclua dados de sequência ou versão para que o consumidor possa rejeitar mensagens obsoletas.

  • Ferramenta de observabilidade. Emita a chave de deduplicação e um identificador de correlação em registos estruturados, e acompanhe uma métrica para duplicados detetados. Um aumento da taxa de duplicados pode indicar uma configuração incorreta do produtor, uma janela de confirmação ou de bloqueio demasiado pequena, ou consumidores com problemas. Utilize rastreio distribuído e correlação para acompanhar uma mensagem entre serviços.

  • Propagar idempotência para chamadas a jusante. Tornar idempotente o consumidor de mensagens não protege os serviços a que recorre. Quando um consumidor invocar serviços subsequentes como parte do processamento, propague a chave de idempotência para que cada camada de serviço possa eliminar duplicações no seu próprio processamento.

Quando utilizar este padrão

Utilize este padrão quando:

  • Consomes mensagens de um corretor que fornece pelo menos uma entrega, que é o padrão para a maioria dos corretores.

  • Reprocessar uma mensagem pode produzir resultados incorretos, como transações financeiras duplicadas, criação de recursos duplicados ou notificações repetidas.

  • Múltiplos consumidores concorrentes processam o mesmo canal, o que torna a entrega duplicada simultânea mais provável.

Este padrão pode não ser adequado quando:

  • As operações que o consumidor realiza já são naturalmente idempotentes, pelo que o reprocessamento é inofensivo e a contabilidade de deduplicação acrescenta custos sem benefício.

  • A carga de trabalho pode tolerar os efeitos do processamento ocasional de duplicados, e o custo de um armazenamento de deduplicação supera o efeito de um duplicado.

Processamento idempotente para além da mensagem

Este padrão aplica idempotência aos consumidores de mensagens, mas o processamento idempotente é um princípio de fiabilidade mais amplo que pode beneficiar qualquer operação que seja executada mais do que uma vez sobre uma tarefa idêntica. Este princípio inclui transformações de extração, transformação e carregamento (ETL) que reprocessam dados reproduzidos, processamento em fluxo que retoma a partir de um ponto de controlo, tarefas agendadas que se sobrepõem ou reiniciam, e pontos finais Webhook ou HTTP que recebem envios duplicados.

A mesma técnica de base aplica-se em cada caso.

  1. Use uma chave estável para identificar a unidade de trabalho.
  2. Regista o que processas.
  3. Ignorar ou absorver execuções duplicadas para que repetir a operação não altere o resultado.

Os mecanismos deste artigo, como chaves estáveis, marcadores atómicos e restrições de unicidade, transferem-se para estes contextos mesmo quando não há intermediário de mensagens envolvido.

Design da carga de trabalho

Avalie como utilizar o padrão Consumidor Idempotente na conceção de uma carga de trabalho para cumprir os objetivos e os princípios definidos nos pilares do Azure Well-Architected Framework. A tabela a seguir fornece orientação sobre como esse padrão suporta as metas de cada pilar.

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 garantem que ela se recupere para um estado totalmente funcional após a ocorrência de uma falha. Este padrão permite que uma carga de trabalho utilize pelo menos uma vez a entrega e tentativas seguras sem corromper os dados, o que transforma a entrega duplicada de um risco de correção numa condição tolerada.

- RE:07 Autopreservação
- Falhas transitórias

Se este padrão introduzir compensações dentro de um pilar, considere-as em relação aos objetivos dos outros pilares.

Exemplo

O exemplo seguinte descreve um consumidor idempotente que processa encomendas do Service Bus e mantém o estado no Azure Cosmos DB para NoSQL.

  1. Um produtor define o Service Bus MessageId como um identificador de encomenda ao nível empresarial.
  2. O consumidor recebe a mensagem no modo PeekLock , o que torna a mensagem disponível para reentrega caso o consumidor não a liquide antes do vencimento do bloqueio.
  3. O contentor do Azure Cosmos DB do consumidor é particionado com base no identificador de encomenda /orderId e define o id do documento com esse mesmo identificador de encomenda, pelo que cada cópia de uma determinada encomenda corresponde à mesma partição lógica e o próprio id da encomenda serve como marcador de desduplicação.

O consumidor segue os seguintes passos para processar cada mensagem:

  1. Lê a mensagem e usa-a MessageId como chave de deduplicação.
  2. Tente criar o documento da encomenda com o id e a chave de partição definidos como o identificador da encomenda.
  3. Se a criação for bem-sucedida, completa a mensagem para que o Service Bus a remova da fila.
  4. Se a criação falhar com um código de estado HTTP 409 (Conflito) porque já existe um documento com esse id, leia o documento existente e compare-o com a mensagem atual.
  5. Se o hash de pedido armazenado ou os campos de negócio imutáveis coincidirem, trate a mensagem como um duplicado, complete-a e evite o processamento adicional.
  6. Se o hash do pedido armazenado ou os campos de negócio imutáveis não coincidirem, envie a mensagem para uma fila de letras mortas e acione um alerta, em vez de descartar silenciosamente a mensagem. O produtor pode ter reutilizado o identificador para conteúdos diferentes, ou os detalhes da mensagem podem ter mudado desde que a encomenda foi processada pela primeira vez.
  7. Se o processamento falhar por um motivo transitório, abandone a mensagem para que o Service Bus a reentregue, ou deixe o bloqueio expirar para que outro consumidor possa recebê-la.

A operação de criação é atómica, por isso serve tanto como verificação de deduplicação como como operação de escrita. Dois consumidores que recebem cópias da mesma mensagem não podem ambos criar a encomenda. Uma tentativa de criação é bem-sucedida, enquanto a outra gera um conflito e descarta com segurança o duplicado.

Quando o processamento tem de escrever mais do que um documento, use um lote transacional que inclua tanto a chave de deduplicação como os documentos empresariais dentro da mesma chave de partição. Como um lote transacional opera dentro de uma única partição lógica, escolhe-se uma chave de partição que todos os documentos de uma mensagem partilham. O lote confirma todos os documentos em conjunto, ou então nenhum deles, pelo que uma falha entre o processamento e a confirmação não pode deixar o marcador de deduplicação e os dados de negócio dessincronizados. Um lote que tenta criar um documento que já existe devolve o estado 409 (Conflito), o que identifica o documento duplicado.

Para tornar este consumidor idempotente também resistente a novas tentativas de envio duplicadas, ative a deteção de duplicados na fila. Numa fila Standard ou Premium, a deteção de duplicados suprime envios repetidos dentro da janela de histórico. O consumidor idempotente continua a tratar quaisquer duplicados que fiquem fora dessa janela ou que resultem da reentrega.

Passo seguinte