Padrão Consumidor Idempotente

Projete os consumidores de mensagens para que o processamento da mesma mensagem mais de uma vez tenha os mesmos efeitos que processá-la uma vez. Sistemas de mensagens que garantem a entrega pelo menos uma vez podem entregar a mesma mensagem várias vezes. A resiliência em relação a duplicatas garante que o reprocessamento de uma mensagem não crie registros duplicados, carregue duas vezes um cliente ou tenha outros efeitos indesejados.

Contexto e problema

Aplicações distribuídas geralmente trocam tarefas por meio de um broker de mensagens em vez de usar chamadas síncronas diretas. A maioria dos agentes, incluindo Barramento de Serviço do Azure, Hubs de Eventos do Azure, Apache Kafka e RabbitMQ, fornecem entrega pelo menos uma vez. Essa garantia garante que uma mensagem chegue a um consumidor mesmo quando ocorrerem falhas, mas também significa que o agente pode entregar a mesma mensagem mais de uma vez.

É impraticável garantir a entrega exatamente uma vez em um sistema distribuído. Mesmo os brokers que usam semântica de exatamente uma vez podem garantir apenas as operações que controlam diretamente, como entregar mensagens aos consumidores ou gravar dados de volta no broker. Eles não podem controlar os efeitos colaterais que os consumidores implementam em sistemas externos. A solução durável não é eliminar a entrega duplicada, mas fazer com que o consumidor a processe corretamente. Quando você combina entrega pelo menos uma vez com um consumidor que ignora duplicatas, obtém o processamento efetivamente uma única vez.

As duplicatas podem surgir de várias fontes:

  • Repetições do produtor. Um produtor envia uma mensagem, não recebe uma confirmação devido a uma falha de rede transitória ou tempo limite e envia a mensagem novamente. O broker agora mantém duas cópias, mesmo que o envio tenha sido bem-sucedido na primeira tentativa.

  • Nova entrega após a ausência de confirmação. Um consumidor recebe e processa uma mensagem, mas não envia a confirmação de recebimento porque sofre uma falha, o bloqueio expira ou a confirmação é perdida. O agente pressupõe que a mensagem não foi processada e a entrega novamente.

  • Falhas do consumidor durante o processamento. Um consumidor conclui uma gravação de banco de dados, mas falha antes de reconhecer a mensagem. Outra instância recebe a mensagem e repete a gravação.

Solução

Crie um consumidor idempotente fazendo com que ele mantenha um registro das mensagens processadas com êxito e ignore qualquer mensagem que já tenha sido processada, com base em um identificador estável que permaneça o mesmo em caso de reentrega. O consumidor verifica o repositório de identificador persistente para determinar se ele já processou esse identificador e, em seguida, processa a mensagem ou a descarta como duplicata.

O fluxo principal consiste nas seguintes etapas:

  1. Leia a mensagem e extraia sua chave de deduplicação.
  2. Verifique o armazenamento de eliminação de duplicação para essa chave.
  3. Se a chave já existir no repositório, trate a mensagem como duplicada. Reconheça a mensagem e pare de processá-la, opcionalmente retornando o resultado registrado anteriormente.
  4. Se a chave ainda não existir no repositório, processe a mensagem e registre a chave em uma única operação atômica e, em seguida, reconheça a mensagem.

As seções a seguir fornecem diretrizes para tornar os consumidores idempotentes:

Escolha uma chave de deduplicação estável

A chave de desduplicação deve identificar de maneira única e consistente a mensagem lógica ao longo de todas as reentregas. Use um identificador de mensagem atribuído pelo produtor ou uma chave de idempotência de nível de negócio que identifique a operação lógica específica, não um contexto de correlação compartilhado que várias mensagens possam carregar.

Por exemplo, defina a propriedade Barramento de Serviço MessageId como um valor que identifique exclusivamente a mensagem lógica. Não use CorrelationId como chave, pois ela se refere a grupos de mensagens relacionadas, como uma solicitação e suas respostas. Para eventos que seguem a especificação CloudEvents, a combinação dos atributos source e id identifica de forma exclusiva um evento e permanece estável em reentregas.

Não se baseie em identificadores de nível de transporte que o broker regenera na reentrega, nem em valores derivados das tentativas de entrega. Esses valores mudam a cada entrega e impedem a detecção de duplicatas. Evite também derivar a chave de campos voláteis, como carimbos de data/hora de recebimento.

Quando mais de um consumidor independente processa o mesmo canal, como vários assinantes em um design de publicação e assinatura, cada consumidor recebe sua própria cópia de uma mensagem e precisa acompanhar independentemente a conclusão do processamento. Se os consumidores compartilharem um repositório de desduplicação, use uma chave composta pela identidade do consumidor e pela identidade da mensagem para os registros. Um repositório indexado apenas pela identidade da mensagem permitiria que o primeiro consumidor suprimisse o processamento de todos os outros.

Decidir onde armazenar chaves processadas

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

  • Uma tabela de eliminação de duplicação dedicada. O consumidor mantém uma tabela separada, às vezes chamada de caixa de entrada, que contém uma linha por chave processada. Essa abordagem mantém as preocupações de eliminação de duplicação separadas dos dados corporativos e funciona bem se muitos tipos de mensagens compartilham o mesmo mecanismo.

  • A entidade de negócios em si. O consumidor armazena a chave no registro que a mensagem cria ou atualiza. Essa abordagem evita uma tabela separada, mas vincula a deduplicação ao tipo de dado de negócio.

Confirmar a chave processada e os efeitos colaterais atomicamente

Um fluxo de verificar e depois processar tem uma janela de falha. Se um consumidor processar uma mensagem e depois registrar a chave em uma etapa separada, uma falha entre as duas operações deixará os efeitos colaterais aplicados, mas a chave não será registrada; assim, o consumidor reprocessará a próxima nova entrega.

Evite essa janela de falha ao gravar o marcador de deduplicação e os efeitos colaterais de negócio na mesma transação. Se um consumidor efetua o commit de ambas as operações em conjunto ou não efetua nenhuma delas, na reentrega ele encontra o marcador e ignora a mensagem, ou não encontra marcador algum porque a transação não foi concluída e reprocessa a mensagem com segurança. Essa variante transacional é o padrão Inbox e é a contraparte no lado do consumidor do padrão Transactional Outbox do produtor.

Evitar duplicatas simultâneas

Com a entrega 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 pela verificação de existência antes que qualquer uma delas efetive o commit; por isso, a verificação por si só não impede o processamento duplo.

Imponha a correção no repositório de dados em vez de na lógica do aplicativo seguindo as seguintes etapas:

  • Use uma restrição de unicidade na chave de desduplicação de forma que duas transações possam tentar inserir uma chave, mas apenas uma possa ter êxito. A outra transação falha na restrição e trata a mensagem como duplicada. Essa abordagem torna o banco de dados o único árbitro do conflito.

  • Evite condições de corrida do tipo verificar e definir em caches. Um padrão que verifica uma chave e a define em duas operações separadas tem uma janela que permite novas tentativas simultâneas de reivindicar a chave. Use uma gravação condicional atômica, como uma inserção que falha em conflitos ou em uma operação set-if-absent, para tornar a declaração da chave uma única etapa atômica.

Lidar com efeitos colaterais que não podem participar da transação

Alguns processos não podem participar da transação de banco de dados do consumidor, como chamar uma API de terceiros ou gravar em um repositório externo. Use a seguinte abordagem de duas fases para esses processos:

  1. Registre a chave com um estado em andamento e, então, execute a ação externa.
  2. Atualize o registro para ser concluído e armazene o resultado.

Na reentrega, um registro concluído informa ao consumidor que não repita a chamada. Um registro em andamento sinaliza que uma tentativa anterior pode ser parcialmente concluída ou está sendo processada por outro consumidor. O consumidor deve reconciliar registros desatualizados ou encaminhar casos não resolvidos para intervenção antes de confirmar a reentrega.

Problemas e considerações

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

  • Prefira operações naturalmente idempotentes. Algumas operações são inerentemente idempotentes e não precisam de contabilidade de eliminação de duplicação. Uma upsert com chave em um identificador de negócio, uma operação de gravação que define um valor absoluto em vez de um incremento, ou uma requisição HTTP PUT para um identificador de recurso produz os mesmos resultados, seja com execução única ou várias vezes.

    Às vezes, você pode tornar uma operação naturalmente idempotente usando a transferência de estado realizada por eventos. A mensagem carrega o estado absoluto resultante, como o novo status de um pedido, para que o consumidor a aplique como um upsert em vez de uma alteração relativa.

    Tip

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

  • Use um framework de mensagens em vez de configurar a desduplicação. A implementação correta do armazenamento com desduplicação, da gravação e da limpeza é propensa a erros. As estruturas baseadas em mensagens fornecem esse padrão como um recurso interno.

    Por exemplo, NServiceBus elimina duplicatas de mensagens recebidas com base em seus identificadores de mensagem e fornece retenção e limpeza configuráveis dos dados de desduplicação. A caixa de saída do consumidor do MassTransit rastreia mensagens recebidas pelos identificadores de mensagens para fornecer um comportamento de processamento exatamente uma vez.

  • Gerenciar o ciclo de vida dos registros de deduplicação. Os registros de desduplicação se acumulam, a menos que você os configure para expirarem. Mantenha cada registro pelo menos pelo tempo em que o broker ainda puder reentregar a mensagem original. O tamanho dessa janela depende do número máximo de tentativas de entrega do broker, do tempo de bloqueio ou de invisibilidade e do tempo de vida da mensagem.

    Defina um TTL (tempo de vida) nos registros de deduplicação que seja maior que essa janela, para que uma reentrega tardia ainda encontre seu marcador. Excluir registros muito cedo reabre a janela para duplicatas. Considere mensagens que os operadores reenviam de filas de mensagens inválidas, pois esses reenvios podem ocorrer muito tempo após o encerramento da janela normal de reentrega.

  • Não use a desduplicação do broker no lugar da lógica de consumidor idempotente. Algumas plataformas filtram duplicatas na camada de transporte. Por exemplo, a detecção de duplicidade do Barramento de Serviço descarta mensagens que repetem um MessageId em uma janela de tempo configurada, o que suprime repetições duplicadas de envio do produtor.

    Esse recurso opera no lado do envio e dentro de uma janela delimitada, portanto não impede que um consumidor processe a mesma mensagem duas vezes após uma reentrega. Você ainda precisa de uma lógica de consumidor idempotente. Use os recursos da plataforma para reduzir o volume de duplicações, não como substituto para o padrão Consumidor Idempotente.

  • Leve em consideração a ordem das mensagens. A desduplicação remove elementos duplicados, mas não garante a preservação da ordem. Se o consumidor depender da ordem de processamento, combine esse padrão com um mecanismo de ordenação, como Barramento de Serviço sessões de mensagem, ou inclua dados de sequência ou versão para que o consumidor possa rejeitar mensagens obsoletas.

  • Instrumento para observabilidade. Emita a chave de desduplicação e um identificador de correlação em logs estruturados e registre uma métrica para duplicatas detectadas. Uma taxa crescente de duplicatas pode indicar uma configuração incorreta do produtor, uma janela de confirmação ou de bloqueio subdimensionada, ou consumidores em estado inadequado. Use o rastreamento distribuído e a correlação para seguir uma mensagem entre serviços.

  • Propague a idempotência para chamadas subsequentes. Tornar o consumidor de mensagens idempotente não protege os serviços que ele invoca. 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 em seu próprio processamento.

Quando usar esse padrão

Use esse padrão quando:

  • Você consome mensagens de um agente que oferece entrega pelo menos uma vez, o que é o padrão para a maioria dos agentes.

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

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

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

  • As operações que o consumidor executa já são naturalmente idempotentes, portanto o reprocessamento é inofensivo, e o controle de deduplicação gera custo sem trazer benefício.

  • A carga de trabalho pode tolerar os efeitos do processamento duplicado eventual, e o custo de um armazenamento para deduplicação supera o impacto de uma duplicação.

Processamento idempotente além do sistema de mensagens

Esse padrão aplica a idempotência aos consumidores de mensagens, mas o processamento idempotente é um princípio de confiabilidade mais amplo que pode beneficiar qualquer operação executada mais de uma vez sobre uma tarefa idêntica. Esse princípio abrange transformações ETL (extração, transformação e carga) que reprocessam dados reexecutados, processamento de fluxo que retoma a partir de um ponto de verificação, tarefas agendadas que se sobrepõem ou são reiniciadas, e pontos de extremidade de webhook ou HTTP que recebem entregas duplicadas.

A mesma técnica de núcleo se aplica em cada caso.

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

Os mecanismos neste artigo, como chaves estáveis, marcadores atômicos e restrições de exclusividade, são transferidos para esses contextos mesmo quando nenhum agente de mensagens está envolvido.

Design de carga de trabalho

Avalie como utilizar o padrão Consumidor Idempotente no design de uma carga de trabalho para atender aos objetivos e princípios abordados nos pilares da Estrutura bem arquitetada do Azure. A tabela a seguir fornece diretrizes sobre como esse padrão dá suporte às metas de cada pilar.

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

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

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

Exemplo

O exemplo a seguir descreve um consumidor idempotente que processa pedidos provenientes do Barramento de Serviço e persiste o estado no Azure Cosmos DB para NoSQL.

  1. Um produtor define o Barramento de Serviço MessageId como um identificador de ordem de nível empresarial.
  2. O consumidor recebe a mensagem no modo PeekLock, o que disponibiliza a mensagem para reentrega caso o consumidor não a confirme antes que o bloqueio expire.
  3. O contêiner do Azure Cosmos DB do consumidor é particionado no identificador do pedido /orderId e define o documento id para o mesmo identificador de pedido, de modo que cada cópia de determinado pedido seja direcionada para a mesma partição lógica, e o próprio pedido id sirva como o marcador de eliminação de duplicação.

O consumidor executa as seguintes etapas para processar cada mensagem:

  1. Leia a mensagem e use MessageId como chave de desduplicação.
  2. Tente criar o documento do pedido com o id e a chave de partição, ambos definidos como o identificador do pedido.
  3. Se a criação for bem-sucedida, conclua a mensagem para que Barramento de Serviço a remova da fila.
  4. Se a criação falhar com um status 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 solicitação armazenado ou os campos de negócios imutáveis corresponderem, trate a mensagem como duplicada, conclua-a e ignore o processamento adicional.
  6. Se o hash de solicitação armazenado ou os campos de negócios imutáveis não corresponderem, envie a mensagem para uma fila de mensagens mortas e crie um alerta, em vez de descartar silenciosamente a mensagem. O produtor pode ter reutilizado o identificador para conteúdo diferente ou os detalhes da mensagem podem ter sido alterados desde que o pedido foi processado pela primeira vez.
  7. Se o processamento falhar por um motivo transitório, abandone a mensagem para que Barramento de Serviço a reenvia ou deixe o bloqueio expirar para que outro consumidor possa recebê-la.

A operação de criação é atômica; portanto, ela serve para a verificação de eliminação de duplicação e para a operação de gravação. Dois consumidores que recebem cópias da mesma mensagem não podem criar o pedido. Uma tentativa de criação tem êxito, e a outra resulta em um conflito e descarta com segurança a duplicata.

Quando o processamento precisar gravar mais de um documento, use um lote transacional que inclua tanto a chave de desduplicação quanto os documentos de negócio dentro da mesma chave de partição. Como um lote transacional opera em uma única partição lógica, você escolhe uma chave de partição que seja compartilhada por todos os documentos de uma mensagem. O lote efetiva todos os documentos juntos ou nenhum deles; portanto, uma falha entre o processamento e a confirmação de recebimento não pode deixar o marcador de desduplicação e os dados de negócio fora de sincronia. Um lote que tenta criar um documento que já existe retorna o status 409 (Conflito), que identifica a duplicata.

Para fazer com que esse consumidor idempotente também seja resiliente a tentativas duplicadas de envio, habilite a detecção de duplicidade na fila. Em uma fila Standard ou Premium, a detecção de duplicatas suprime os envios duplicados dentro da respectiva janela de histórico. O consumidor idempotente ainda lida com todas as duplicatas que ocorram fora dessa janela ou que resultem de reentrega.

Próximas etapas