Microsserviços: lidando com a consistência eventual

22

Suponha que temos uma função que atualiza a senha de um usuário.

Depois que o botão 'Atualizar senha' é clicado, um UpdatePasswordEvent é enviado para um tópico em que outros três serviços estão inscritos:

  1. Um serviço que realmente atualiza a senha do usuário
  2. Um serviço que atualiza o histórico de senhas do usuário
  3. Um serviço que envia um email informando ao usuário que sua senha foi alterada.

Com base no que entendi sobre a consistência eventual, todos esses serviços (consumidores) receberão o evento ao mesmo tempo e os processarão separadamente, o que, em um bom cenário, levará à consistência dos dados.

No entanto, e se um serviço falhar ao processar o evento? por exemplo, desconexão súbita, erro no banco de dados, etc ... Qual é um bom padrão / prática para lidar com essas falhas de transação?

Eu estava pensando em criar um RollbackTopic onde, se algum evento falhar ao ser processado, um RollbackEvent será criado em um tópico em que "serviços de reversão" farão seu trabalho e reverterão os dados.

mpmp
fonte
11
Você não pode desfazer um e-mail enviado :-)
LAIV
2
Porque todos eles devem fazer parte do mesmo serviço. Os microsserviços se opõem aos monólitos, não significa que você precise projetá-los o menos "fisicamente" possível. Embora este não está diretamente relacionado, você deve ler esta pergunta e as duas principais respostas: softwareengineering.stackexchange.com/questions/339230/...
Walfrat
1
Você pode considerar atualizar a senha do usuário no banco de dados de forma síncrona, para fornecer feedback imediato ao usuário e acionar outros serviços de forma assíncrona, emitindo uma mensagem que a senha mudou em um tópico, para que sua mensagem não precise ser contenha a senha.
Cr3
É o email para informar ao usuário que a transação foi concluída ou está lá para informar ao usuário que alguém (espero que eles) tenha alterado a senha. "Se não fosse você, você precisa agir". Se o segundo, basta enviar um e-mail agora, da melhor maneira possível.
CTRL-ALT-DELOR #

Respostas:

29

Com base no que entendi sobre a consistência eventual, todos esses serviços (consumidores) receberão o evento ao mesmo tempo e os processarão separadamente , o que, em um bom cenário, levará à consistência dos dados.

Não, não necessariamente. Como eu comentei, não podemos desfazer um email enviado, por isso ainda precisamos de uma espécie de "sequência". O IPC sobre gerenciamento de dados orientado a eventos não é isento de orquestração 1 .

Por exemplo, o email não deve ser enviado, a menos que as transações anteriores sejam concluídas com êxito e o serviço de email obtenha uma prova disso. 3

No entanto, e se um serviço falhar ao processar o evento? por exemplo, desconexão súbita, erro no banco de dados, etc ... Qual é um bom padrão / prática para lidar com essas falhas de transação?

Diga olá às falácias da computação distribuída . São elas que complicam as coisas e, como sempre, não há balas de prata para lidar com elas.

Antes de iniciar nossa jornada em busca da Arca Perdida, devemos considerar perguntar à organização primeiro. Freqüentemente, a solução está em como a organização enfrenta esses problemas no mundo real .

O que todos (departamentos) fazem quando determinados dados estão ausentes ou incompletos?

Veremos que departamentos diferentes têm soluções diferentes que, no total, compõem a solução a ser implementada.

Enfim, aqui estão algumas práticas que podem nos ajudar com a estratégia a seguir.

Consistência eventual

Em vez de garantir que o sistema esteja em um estado consistente o tempo todo, podemos aceitar que o sistema o obtenha em algum momento no futuro. Essa abordagem é especialmente útil para operações comerciais de longa duração.

O caminho para o sistema atingir a consistência varia de sistema para sistema. Pode envolver de processos automatizados a algum tipo de intervenção humana. Por exemplo, o típico tentando novamente mais tarde ou o contato com o Atendimento ao Cliente .

Abortar todas as operações

Coloque o sistema novamente em um estado consistente por meio de transações compensatórias . No entanto, temos que levar em conta que essas transações também podem falhar, o que poderia nos levar a um ponto em que a inconsistência é ainda mais difícil de ser resolvida. E, novamente, não podemos desfazer um email enviado.

Para um número baixo de transações, essa abordagem é viável, porque o número de transações compensatórias também é baixo. Se houvesse várias transações comerciais envolvidas no IPC, seria difícil lidar com uma transação compensadora para cada uma delas.

Se optarmos por compensar transações , acharemos o padrão de design do disjuntor muito útil - e obrigatório, ouso dizer -

Transações distribuídas

A idéia é abranger várias transações em uma única transação, por meio de um processo geral de controle conhecido como Gerenciador de Transações . Um algoritmo comum para lidar com transações distribuídas é o commit de duas fases .

A principal preocupação das transações distribuídas é que elas dependem do bloqueio dos recursos durante sua vida útil e, como sabemos, as coisas também podem dar errado para o Transaction Manager .

Se os gerenciadores de transações ficarem comprometidos, podemos acabar com vários bloqueios nos diferentes contextos limitados, resultando em comportamentos inesperados devido ao enfileiramento das mensagens. 2

Decomposição de operações. Por quê?

Se você estiver decompondo um sistema existente e encontrar uma coleção de conceitos que realmente desejam estar dentro de um único limite de transação, talvez deixe-os até o final.

Sam Newman

Na linha dos argumentos acima, Sam - em seu livro Building Microservices - declara que, se realmente não pudermos pagar a consistência eventual, devemos evitar dividir a operação agora.

Se não pudermos dividir certas operações em duas ou mais transações, pode-se dizer que - provavelmente - essas transações pertencem ao mesmo contexto limitado ou, pelo menos, a um contexto transversal que ainda precisa ser modelado.

Por exemplo, no nosso caso, percebemos que as transações 1 e 2 estão intimamente relacionadas entre si e provavelmente ambas podem pertencer ao mesmo contexto limitado Contas , usuários , registro , qualquer que seja ...

Considere colocar as duas operações dentro dos limites da mesma transação. Facilitaria toda a operação. Pesar também o nível de criticidade de cada transação. Provavelmente, se a transação nº 2 falhar, não deverá comprometer toda a operação. Em caso de dúvida pergunte à organização .


1: Não é o tipo de orquestração que você pensa. Não estou falando da orquestração do ESB. Estou falando de fazer com que os serviços reajam ao evento apropriado.

2: Você pode encontrar opiniões interessantes de Sam Newman sobre transações distribuídas.

3: Verifique a resposta de David Parker sobre esse assunto.

Laiv
fonte
3
Resposta muito boa. Eu enfatizaria apenas a importância de levar em consideração os riscos que surgem ao usar transações distribuídas - principalmente o bloqueio de recursos que produz impasses e interrupções de sistemas. Em um produto de comércio eletrônico em que trabalhei há cerca de três anos, tivemos que substituir as DTs pelo sistema de mensagens, porque, com a quantidade de usuários disponíveis nos sistemas, o sistema era muito propenso a erros. Problemas com DTs ocorrem principalmente quando a base de usuários cresce.
Andy Andy
7

No seu caso, você não pode simplesmente processar as três coisas ao mesmo tempo. O que você precisa é de um processo. Aqui está um exemplo extremamente simplificado:

Orquestração de comandos e eventos

É importante saber que as operações de alteração de estado DEVEM ser sempre feitas em uma entidade consistente. A menos que você possa garantir uma consistência forte , isso deve ser feito em um registro mestre.

Seu sistema deve garantir que, antes que qualquer evento seja gerado, as alterações DEVEM ser persistidas primeiro com segurança transacional. Isso é para garantir que um evento gerado seja realmente uma confirmação do que realmente aconteceu.

Existem várias partes complicadas do processo atual e vou ignorar as óbvias - como: E se o servidor de banco de dados morrer quando persistir um usuário com a senha alterada? Você simplesmente emite o UpdatePassword novamente. No entanto, algumas partes precisam ser cuidadas por você e são elas:

  • lidar com duplicação de mensagens,
  • lidar com o envio de e-mail.

Em um sistema, o orquestrador de processos (PO) nada mais é do que outra entidade, que contém estado interno - também no termo literal - e permite transições entre os estados, atuando efetivamente como algum tipo de máquina de estados. Graças ao estado interno, você pode remover o processamento da duplicação de mensagens.

Quando o pedido está em um Newestado e processo UserPasswordHasBeenUpdated, ele muda seu estado para UserPasswordHasBeenUpdated(ou o nome do estado que funciona para você). Se o pedido ainda estivesse em um UserPasswordHasBeenUpdatede outro UserPasswordHasBeenUpdatedchegasse, o pedido ignoraria completamente a mensagem, sabendo que é uma duplicação. Mecanismo semelhante seria implementado para outros estados também.

Lidar com o envio real do e-mail é um pouco mais complicado. Aqui você tem duas opções:

  1. envie no máximo uma vez,
  2. envie pelo menos uma vez.

Envie no máximo uma vez

Com esta opção, quando o pedido atinge o UserPasswordHistoryHasBeenSavedestado, um comando para enviar um email é despachado como uma reação à alteração de estado. Seu sistema garantiria a UserPasswordHistoryHasBeenSavedpersistência do estado antes de enviar o e-mail, ou seja, a mensagem duplicada não acionaria o envio de e-mail novamente. Com essa abordagem, você garante que o estado correto seja salvo para o pedido, mas não pode garantir nenhuma operação a seguir.

Envie pelo menos uma vez

É para isso que eu iria.

Em vez de salvar UserPasswordHistoryHasBeenSavede enviar o email como uma reação a ele, tente enviar o email primeiro. Se a operação de envio falhar, o estado do pedido nunca será alterado UserPasswordHistoryHasBeenSavede outra mensagem do mesmo tipo ainda será processada. Se o envio do e-mail fosse realmente bem-sucedido, mas seu sistema falharia durante a persistência do pedido de compra com seu novo UserPasswordHistoryHasBeenSavedestado, outra mensagem do UserPasswordHistoryHasBeenSavedacionaria novamente o comando para enviar o e-mail e o usuário o teria recebido várias vezes. .

No seu caso, você deseja garantir que o usuário realmente receba o email. É por isso que eu escolheria as segundas opções sobre as primeiras.

Andy
fonte
2

Os sistemas de filas não são tão frágeis quanto você imagina.

Se estivéssemos gravando todos os três processos em um banco de dados relacional, poderíamos usar uma transação para lidar com uma falha no meio do processo.

Sem a confirmação final, o trabalho parcial seria descartado.

Em um sistema de bases de filas, você terá opções semelhantes ao ler uma mensagem da fila para lidar com falhas no meio do processo.

O Amazon SQS, por exemplo, simplesmente oculta mensagens que são lidas. a menos que um comando de exclusão final seja enviado, a mensagem reaparecerá ou será colocada em uma fila de devoluções.

Você pode implementar 'transações' semelhantes de várias maneiras, mantendo essencialmente uma cópia da mensagem até receber a confirmação do processamento bem-sucedido. Se a confirmação não for recebida a tempo. você pode enviar a mensagem novamente ou mantê-la para obter atenção manual.

Potencialmente, você pode criar um 'serviço de reversão' que monitora essas mensagens erradas, conhece as mensagens relacionadas e o estado passado e executa uma reversão.

Contudo! Geralmente é melhor apenas reenviar as mensagens erradas. Afinal, esses tendem a ser casos extremos. Um servidor falhou catastroficamente ou houve um erro no tratamento de um tipo de mensagem específico.

Depois de alertado sobre o erro, o serviço pode ser reparado e as mensagens processadas com êxito. Trazendo o sistema como um todo de volta a um estado consistente.

Ewan
fonte
2

O que você está enfrentando aqui é o problema dos dois generais . Em essência: como você pode ter certeza de que uma mensagem é recebida e uma resposta a essa mensagem ocorre? Em muitos casos, uma solução perfeita não existe. De fato, em um sistema distribuído, muitas vezes é impossível receber uma entrega exata de mensagens.

Uma primeira observação óbvia é que o serviço que altera a senha deve enviar o evento de alteração da senha. Dessa maneira, o histórico de senhas e os serviços de envio de email são acionados apenas quando a senha realmente muda, independentemente do motivo.

Para resolver seu problema, eu não consideraria as transações distribuídas, mas sim a direção da entrega de mensagens pelo menos uma vez e do processamento idempotente.

  • Pelo menos uma vez

    Para garantir que o evento de alteração de senha seja realmente visto por todos os consumidores, é necessário usar um canal de comunicação durável, no qual as mensagens possam ser consumidas no estilo "pelo menos uma vez". Os consumidores só reconhecem uma mensagem como consumida quando a processam completamente. Se, por exemplo, o serviço de histórico de senhas falhar durante a gravação de uma entrada no histórico, ele relerá o mesmo evento de alteração de senha após a reinicialização e tentará novamente, reconhecendo esse evento como somente leitura depois de ter sido gravado no histórico. Você deve escolher uma solução de fila de mensagens com base em sua capacidade de reenviar mensagens até que sejam reconhecidas.

  • Idempotência

    Após obter a entrega pelo menos uma vez, há o problema de ações duplicadas que ocorrem quando uma mensagem foi parcialmente processada antes de o consumidor ser interrompido e depois reprocessado posteriormente. Isso deve ser resolvido projetando cada serviço para que ele seja idempotente. As gravações executadas podem ocorrer várias vezes sem efeitos adversos ou mantêm seu próprio armazenamento de quais ações foram executadas e evitam a execução de uma ação mais de uma vez. No caso de envio de e-mail, você provavelmente não vale a pena tentar fazer com que ele se comporte de maneira idempotente e apenas fique bem com ocasionalmente um e-mail sendo enviado duas vezes.

De qualquer forma, tenha cuidado com o tamanho de seus serviços. Seu serviço de histórico de senhas realmente precisa ser independente do serviço de alteração de senha?

Joeri Sebrechts
fonte
1

Eu discordo de muitas respostas.

  1. Envie o e-mail agora “Alguém mudou sua senha. Se era você, então você não precisa fazer nada. Se não estiver em pânico. ”Isso chegará quando chegar.
  2. Mude a senha. Embora você tenha consistência eventual. Você deseja garantir que esta sessão veja as alterações feitas pelo usuário.

Existem outras promessas de consistência que você pode adicionar.

  • Certifique-se de que as mudanças ocorram na ordem do tempo.
  • Verifique se um usuário nunca vê uma reversão, mas outros usuários ainda podem não ver a alteração.
  • Há outros

Essas consistências adicionais precisarão ser implementadas, dependendo das ações do aplicativo.


Não faço ideia do que você quer dizer com "atualiza o histórico", mas nunca altere o histórico. Se você está apenas estendendo o DAG, isso deve causar a alteração no estado atual. Eles não são independentes. Se eles são, então você não pode confiar na história que reflete o que aconteceu. (e por último, mas não menos importante, não armazene senhas, veja como não armazenar senhas )

ctrl-alt-delor
fonte
Se você pode enviar o e-mail no início, sua abordagem está correta. Se você precisar enviar algo junto com o email. Talvez um tipo de link / dados que só possa ser obtido após a consistência seja alcançada, você não poderá enviar o email primeiro. Isso é o que eu comentei consider asking the organization first.. Você provavelmente está certo. No entanto, achei importante condicionar aqueles eventos que não podemos desfazer. Por exemplo, notificações para o usuário final. A notificação no estado real dos dados do usuário pode causar uma má impressão.
LAIV
Dito isto, para este cenário específico (notificação de alteração de senha), concordei com essa abordagem. Assim que atender aos requisitos.
LAIV