Transações entre microsserviços REST?

195

Digamos que tenhamos um microsserviço de Usuário, Carteira REST e um gateway de API que cole as coisas. Quando Bob se registra em nosso site, nosso gateway de API precisa criar um usuário através do microsserviço de usuário e uma carteira através do microsserviço de carteira.

Agora, aqui estão alguns cenários em que as coisas podem dar errado:

  • A criação do usuário Bob falha: tudo bem, apenas retornamos uma mensagem de erro ao Bob. Estamos usando transações SQL para que ninguém nunca tenha visto Bob no sistema. Tudo otimo :)

  • O usuário Bob é criado, mas, antes que nossa Carteira virtual possa ser criada, nosso gateway de API falha muito. Agora temos um usuário sem carteira (dados inconsistentes).

  • O usuário Bob é criado e, ao criar a Carteira virtual, a conexão HTTP cai. A criação da carteira pode ter sido bem-sucedida ou não.

Quais soluções estão disponíveis para impedir que esse tipo de inconsistência de dados ocorra? Existem padrões que permitem que as transações abranjam várias solicitações REST? Eu li a página da Wikipedia no commit de duas fases que parece abordar esse problema, mas não sei como aplicá-lo na prática. Este Transações Distribuídas Atômicas: um documento de design RESTful também parece interessante, embora eu ainda não o tenha lido.

Como alternativa, eu sei que o REST pode não ser adequado para este caso de uso. Talvez a maneira correta de lidar com essa situação abandone totalmente o REST e use um protocolo de comunicação diferente como um sistema de fila de mensagens? Ou devo impor consistência no código do meu aplicativo (por exemplo, tendo um trabalho em segundo plano que detecta inconsistências e as corrige ou tendo um atributo "state" no meu modelo de usuário com valores "criando", "criado" etc.)?

Olivier Lalonde
fonte
3
Link interessante: news.ycombinator.com/item?id=7995130
Olivier Lalonde
3
Se um usuário não faz sentido sem uma carteira, por que criar um microsserviço separado para ela? Pode ser que algo não está certo com a arquitetura em primeiro lugar? Por que você precisa de um gateway de API genérico, btw? Existe alguma razão específica para isso?
Vladislav Rastrusny
4
@ VladislavRastrusny, foi um exemplo fictício, mas você poderia pensar no serviço de carteira como sendo tratado pelo Stripe, por exemplo.
Olivier Lalonde
Você pode usar um gerenciador de processos para rastrear a transação (padrão do gerenciador de processos) ou fazer com que cada microsserviço saiba como acionar uma reversão (padrão do gerenciador de saga) ou realizar algum tipo de confirmação em duas fases ( blog.aspiresys.com/software-product-engineering / producteering /… )
andrew pate
@VladislavRastrusny "Se um usuário não faz sentido sem uma carteira, por que criar um microsserviço separado para ela" - por exemplo, além do fato de um usuário não poder existir sem uma carteira, eles não têm nenhum código em comum. Portanto, duas equipes desenvolverão e implantarão microsserviços de usuário e carteira independentemente. Não é o objetivo de fazer microsserviços em primeiro lugar?
Nik

Respostas:

148

O que não faz sentido:

  • transações distribuídas com serviços REST . Os serviços REST, por definição, são sem estado, portanto, eles não devem ser participantes de um limite transacional que abranja mais de um serviço. Seu cenário de caso de uso de registro de usuário faz sentido, mas o design com microsserviços REST para criar dados de Usuário e Carteira não é bom.

O que lhe dará dores de cabeça:

  • EJBs com transações distribuídas . É uma daquelas coisas que funcionam na teoria, mas não na prática. No momento, estou tentando fazer uma transação distribuída funcionar para EJBs remotos nas instâncias do JBoss EAP 6.3. Estamos conversando com o suporte da RedHat há semanas, e ainda não funcionou.
  • Soluções de confirmação de duas fases em geral . Eu acho que o protocolo 2PC é um ótimo algoritmo (há muitos anos eu o implementei em C com RPC). Requer mecanismos abrangentes de recuperação de falhas, com novas tentativas, repositório de estados etc. Toda a complexidade está oculta na estrutura da transação (por exemplo: JBoss Arjuna). No entanto, 2PC não é à prova de falhas. Existem situações em que a transação simplesmente não pode ser concluída. Então você precisa identificar e corrigir inconsistências do banco de dados manualmente. Pode acontecer uma vez em um milhão de transações, se você tiver sorte, mas pode acontecer uma vez a cada 100 transações, dependendo da plataforma e do cenário.
  • Sagas (transações compensatórias) . Há a sobrecarga de implementação da criação das operações de compensação e o mecanismo de coordenação para ativar a compensação no final. Mas a compensação também não é à prova de falhas. Você ainda pode acabar com inconsistências (= alguma dor de cabeça).

Qual é provavelmente a melhor alternativa:

  • Consistência eventual . Nem as transações distribuídas do tipo ACID nem as transações de compensação são à prova de falhas e ambas podem levar a inconsistências. A consistência eventual geralmente é melhor que a "inconsistência ocasional". Existem diferentes soluções de design, como:
    • Você pode criar uma solução mais robusta usando comunicação assíncrona. No seu cenário, quando Bob se registra, o gateway da API pode enviar uma mensagem para uma fila NewUser e responder imediatamente ao usuário dizendo "Você receberá um email para confirmar a criação da conta". Um serviço de consumidor da fila pode processar a mensagem, executar as alterações no banco de dados em uma única transação e enviar o email a Bob para notificar a criação da conta.
    • O microsserviço de usuário cria o registro do usuário e um registro da carteira no mesmo banco de dados . Nesse caso, o armazenamento de carteira no microsserviço de usuário é uma réplica do armazenamento de carteira mestre visível apenas para o microsserviço de Wallet. Existe um mecanismo de sincronização de dados baseado em acionador ou ativado periodicamente para enviar alterações de dados (por exemplo, novas carteiras) da réplica para o mestre e vice-versa.

Mas e se você precisar de respostas síncronas?

  • Remodele os microsserviços . Se a solução com a fila não funcionar porque o consumidor do serviço precisa de uma resposta imediatamente, prefiro remodelar a funcionalidade de Usuário e Carteira para ser colocada no mesmo serviço (ou pelo menos na mesma VM para evitar transações distribuídas) ) Sim, está um passo mais longe dos microsserviços e mais perto de um monólito, mas salvará você de alguma dor de cabeça.
Paulo Merson
fonte
4
A consistência eventual funcionou para mim. Nesse caso, a fila "NewUser" deve estar disponível e resiliente.
22817 Ram Ramiredired
@RamBavireddi do Kafka ou RabbitMQ suportam filas resilientes?
v.oddou
@ v.oddou Sim, eles fazem.
Ram Bavireddi
2
@PauloMerson Não tenho certeza de como você diferencia as transações de compensação com a consistência eventual. E se, em sua eventual consistência, a criação da carteira falhar?
balsick
2
@balsick Um dos desafios de eventuais configurações de consistência é o aumento da complexidade do projeto. Frequentemente, são necessárias verificações de consistência e eventos de correção. O design da solução varia. Na resposta, sugiro a situação em que o registro da Carteira virtual é criado no banco de dados ao processar uma mensagem enviada por meio de um intermediário de mensagens. Nesse caso, poderíamos definir um canal de mensagens não entregues, ou seja, se o processamento dessa mensagem gerar um erro, podemos enviar a mensagem para uma fila de mensagens não entregues e notificar a equipe responsável por "Carteira".
Paulo Merson
66

Essa é uma pergunta clássica que me foi feita durante uma entrevista recentemente Como chamar vários serviços da Web e ainda preservar algum tipo de tratamento de erro no meio da tarefa. Hoje, na computação de alto desempenho, evitamos confirmações em duas fases. Eu li um artigo há muitos anos sobre o que foi chamado de "modelo Starbuck" para transações: pense no processo de pedido, pagamento, preparação e recebimento do café que você pede na Starbuck ... Eu simplifico demais as coisas, mas um modelo de confirmação em duas fases seria sugira que todo o processo seja uma transação única para todas as etapas envolvidas até você receber seu café. No entanto, com esse modelo, todos os funcionários esperariam e parariam de trabalhar até você tomar seu café. Você vê a foto?

Em vez disso, o "modelo Starbuck" é mais produtivo, seguindo o modelo de "melhor esforço" e compensando os erros no processo. Primeiro, eles garantem que você pague! Depois, há filas de mensagens com seu pedido anexado ao copo. Se algo der errado no processo, como você não recebeu seu café, não foi o que pediu, etc., entramos no processo de compensação e garantimos que você obtém o que deseja ou o devolve. Este é o modelo mais eficiente para aumentar a produtividade.

Às vezes, a starbuck está desperdiçando um café, mas o processo geral é eficiente. Existem outros truques para pensar quando você cria seus serviços da Web, como projetá-los de uma maneira que eles podem ser chamados inúmeras vezes e ainda assim fornecem o mesmo resultado final. Então, minha recomendação é:

  • Não fique muito bem ao definir seus serviços da Web (não estou convencido sobre o hype de microsserviços que acontece hoje em dia: muitos riscos de ir longe demais);

  • Async aumenta o desempenho; portanto, prefira ser assíncrono, envie notificações por email sempre que possível.

  • Crie serviços mais inteligentes para torná-los "recuperáveis" várias vezes, processando com um uid ou taskid que seguirá o pedido até o final, validando as regras de negócios em cada etapa;

  • Use filas de mensagens (JMS ou outras) e desvie para processadores de tratamento de erros que aplicarão operações à "reversão" aplicando operações opostas. A propósito, trabalhar com ordem assíncrona exigirá algum tipo de fila para validar o estado atual do processo, então considere isso;

  • Por último, (como isso pode não acontecer com frequência), coloque-o em uma fila para processamento manual de erros.

Vamos voltar com o problema inicial que foi postado. Crie uma conta e crie uma carteira e verifique se tudo foi feito.

Digamos que um serviço da Web seja chamado para orquestrar toda a operação.

O pseudo-código do serviço da web ficaria assim:

  1. Ligue para o microsserviço de criação de conta, passe algumas informações e um ID de tarefa exclusivo 1.1 O microsserviço de criação de conta verificará primeiro se essa conta já foi criada. Um ID de tarefa está associado ao registro da conta. O microsserviço detecta que a conta não existe e, portanto, a cria e armazena o ID da tarefa. NOTA: este serviço pode ser chamado 2000 vezes, sempre executará o mesmo resultado. O serviço responde com um "recibo que contém informações mínimas para executar uma operação de desfazer, se necessário".

  2. Ligue para a criação do Wallet, fornecendo o ID da conta e o ID da tarefa. Digamos que uma condição não seja válida e a criação da carteira não possa ser executada. A chamada retorna com um erro, mas nada foi criado.

  3. O orquestrador é informado do erro. Ele sabe que precisa interromper a criação da conta, mas não fará isso sozinho. Ele solicitará que o serviço de carteira faça isso passando seu "recibo mínimo de desfazer" recebido no final da etapa 1.

  4. O serviço de conta lê o recebimento de desfazer e sabe como desfazer a operação; o recebimento de desfazer pode até incluir informações sobre outro microsserviço que ele poderia ter chamado para fazer parte do trabalho. Nessa situação, o recebimento de desfazer pode conter o ID da conta e possivelmente algumas informações adicionais necessárias para executar a operação oposta. No nosso caso, para simplificar, digamos que é simplesmente excluir a conta usando seu ID.

  5. Agora, digamos que o serviço da Web nunca tenha recebido o sucesso ou a falha (neste caso) de que o desfazer da criação da conta foi realizado. Ele simplesmente chamará o serviço de desfazer da conta novamente. E esse serviço normalmente nunca falha, porque seu objetivo é que a conta não exista mais. Por isso, verifica se existe e vê que nada pode ser feito para desfazê-lo. Portanto, retorna que a operação é um sucesso.

  6. O serviço da web retorna ao usuário que a conta não pôde ser criada.

Este é um exemplo síncrono. Poderíamos ter gerenciado de maneira diferente e colocado o caso em uma fila de mensagens direcionada ao suporte técnico, se não quisermos que o sistema recupere completamente o erro ". Já vi isso sendo realizado em uma empresa onde não é suficiente ganchos podem ser fornecidos ao sistema de back-end para corrigir situações.O suporte técnico recebeu mensagens contendo o que foi executado com êxito e tinha informações suficientes para corrigir coisas, assim como nosso recibo de desfazer poderia ser usado de uma maneira totalmente automatizada.

Eu realizei uma pesquisa e o site da microsoft tem uma descrição padrão para essa abordagem. É chamado de padrão de transação compensadora:

Padrão de transação de compensação

user8098437
fonte
2
Você acha que poderia expandir esta resposta para fornecer conselhos mais específicos ao OP. Tal como está, essa resposta é um tanto vaga e difícil de entender. Embora eu entenda como o café é servido na Starbucks, não está claro para mim quais aspectos desse sistema devem ser emulados nos serviços REST.
#
Eu adicionei um exemplo relacionado ao caso inicialmente fornecido na postagem original.
precisa saber é o seguinte
2
Acabei de adicionar um link ao padrão de transação compensadora, conforme descrito pela Microsoft.
user8098437
3
Para mim, esta é a melhor resposta. Tão simples
Oscar Nevarez
1
Observe que as transações de compensação podem ser totalmente impossíveis em certos cenários complexos (conforme destacado nos documentos da Microsoft). Neste exemplo, imagine antes que a criação da carteira falhe, alguém poderia ler os detalhes sobre a conta associada fazendo uma chamada GET no serviço de Conta, que idealmente não deveria existir em primeiro lugar desde que a criação da conta falhou. Isso pode levar à inconsistência dos dados. Esse problema de isolamento é bem conhecido no padrão SAGAS.
Anmol Singh Jaggi 25/04
32

Todos os sistemas distribuídos têm problemas com a consistência transacional. A melhor maneira de fazer isso é como você disse, com um commit em duas fases. Faça com que a carteira e o usuário sejam criados em um estado pendente. Após a criação, faça uma chamada separada para ativar o usuário.

Esta última chamada deve ser repetida com segurança (caso sua conexão caia).

Isso exigirá que a última chamada conheça as duas tabelas (para que isso possa ser feito em uma única transação JDBC).

Como alternativa, você pode pensar sobre o motivo de sua preocupação com um usuário sem carteira. Você acredita que isso causará um problema? Nesse caso, talvez seja uma má idéia tê-las como chamadas de descanso separadas. Se um usuário não existir sem uma carteira, provavelmente você deverá adicionar a carteira ao usuário (na chamada POST original para criar o usuário).

Rob Conklin
fonte
Obrigado pela sugestão. Os serviços de Usuário / Carteira eram fictícios, apenas para ilustrar o ponto. Mas concordo que devo projetar o sistema para evitar a necessidade de transações o máximo possível.
Olivier Lalonde
7
Eu concordo com o segundo ponto de vista. Parece que o seu microsserviço, que cria usuário, também deve criar uma carteira, porque esta operação representa a unidade atômica do trabalho. Além disso, você pode ler este eaipatterns.com/docs/IEEE_Software_Design_2PC.pdf
Sattar Imamov
2
Esta é realmente uma ótima idéia. Desfazer é uma dor de cabeça. Mas criar algo em um estado pendente é muito menos invasivo. Todas as verificações foram executadas, mas nada definitivo foi criado ainda. Agora, precisamos apenas ativar os componentes criados. Provavelmente, podemos fazer isso sem transação.
Timo
10

IMHO, um dos principais aspectos da arquitetura de microsserviços é que a transação está confinada ao microsserviço individual (princípio de responsabilidade única).

No exemplo atual, a criação do usuário seria uma transação própria. A criação do usuário enviaria um evento USER_CREATED para uma fila de eventos. O serviço da Carteira assinaria o evento USER_CREATED e faria a criação da Carteira.

mithrandir
fonte
1
Supondo que desejamos evitar todo e qualquer PC 2 e assumindo que o serviço do Usuário grave em um banco de dados, não podemos enviar a mensagem para uma fila de eventos pelo Usuário como transacional, o que significa que ele nunca poderá o serviço Wallet.
Roman Kharkovski
@RomanKharkovski Um ponto importante, de fato. Uma maneira de resolver isso pode ser iniciar uma transação, salvar o Usuário, publicar o evento (não faz parte da transação) e confirmar a transação. (Pior caso, altamente improvável, a submissão falha, e que responderam ao evento será incapaz de localizar o usuário.)
Timo
1
Em seguida, armazene o evento no banco de dados e na entidade. Tenha um trabalho agendado para processar eventos armazenados e enviá-los ao intermediário de mensagens. stackoverflow.com/a/52216427/4587961
Yan Khonski
7

Se minha carteira fosse apenas mais um monte de registros no mesmo banco de dados sql que o usuário, provavelmente colocaria o código de criação de usuário e carteira no mesmo serviço e lidaria com isso usando os recursos normais de transação do banco de dados.

Parece-me que você está perguntando sobre o que acontece quando o código de criação de carteira exige que você toque em outro sistema ou sistemas? Eu diria que tudo depende de quão complexo e ou arriscado é o processo de criação.

Se for apenas uma questão de tocar em outro armazenamento de dados confiável (por exemplo, um que não possa participar de suas transações sql), dependendo dos parâmetros gerais do sistema, eu posso estar disposto a arriscar a chance muito pequena de que a segunda gravação não ocorra. Talvez eu não faça nada, mas crie uma exceção e lide com os dados inconsistentes por meio de uma transação compensadora ou mesmo algum método ad-hoc. Como sempre digo aos meus desenvolvedores: "se esse tipo de coisa está acontecendo no aplicativo, não passa despercebido".

À medida que a complexidade e o risco da criação de carteira aumentam, você deve tomar medidas para melhorar os riscos envolvidos. Digamos que algumas das etapas exijam a chamada de várias APIs de parceiros.

Nesse ponto, você pode introduzir uma fila de mensagens junto com a noção de usuários e / ou carteiras parcialmente construídos.

Uma estratégia simples e eficaz para garantir que suas entidades acabem sendo construídas adequadamente é que os trabalhos sejam repetidos até que sejam bem-sucedidos, mas depende muito dos casos de uso do seu aplicativo.

Eu também pensava muito sobre por que tive uma etapa propensa a falhas no meu processo de provisionamento.

Robert Moskal
fonte
4

Uma solução simples é criar usuário usando o Serviço do Usuário e usar um barramento de mensagens em que o serviço do usuário emite seus eventos, e o Serviço Wallet se registra no barramento de mensagens, ouve o evento Criado pelo Usuário e cria a Carteira para o Usuário. Enquanto isso, se o usuário acessar a interface do usuário da Carteira virtual do Google Wallet, verifique se o usuário acabou de ser criado e mostre que a criação da sua carteira está em andamento, verifique-a em algum momento

techagrammer
fonte
3

Quais soluções estão disponíveis para impedir que esse tipo de inconsistência de dados ocorra?

Tradicionalmente, os gerenciadores de transações distribuídas são usados. Alguns anos atrás, no mundo Java EE, você pode ter criado esses serviços como EJBs, que foram implementados em diferentes nós e seu gateway de API teria feito chamadas remotas para esses EJBs. O servidor de aplicativos (se configurado corretamente) garante automaticamente, usando a confirmação em duas fases, que a transação seja confirmada ou revertida em cada nó, para garantir a consistência. Mas isso exige que todos os serviços sejam implantados no mesmo tipo de servidor de aplicativos (para que sejam compatíveis) e, na realidade, apenas trabalhem com serviços implantados por uma única empresa.

Existem padrões que permitem que as transações abranjam várias solicitações REST?

Para SOAP (ok, não REST), existe a especificação WS-AT, mas nenhum serviço que eu já tive que integrar suporta isso. Para o REST, o JBoss tem algo em andamento . Caso contrário, o "padrão" é encontrar um produto que você possa conectar à sua arquitetura ou criar sua própria solução (não recomendado).

Publiquei esse produto para Java EE: https://github.com/maxant/genericconnector

De acordo com o documento que você mencionou, também há o padrão Try-Cancel / Confirm e o Produto associado da Atomikos.

Os mecanismos BPEL lidam com a consistência entre serviços implantados remotamente usando compensação.

Como alternativa, eu sei que o REST pode não ser adequado para este caso de uso. Talvez a maneira correta de lidar com essa situação abandone totalmente o REST e use um protocolo de comunicação diferente como um sistema de fila de mensagens?

Existem várias maneiras de "vincular" recursos não transacionais a uma transação:

  • Como você sugere, você pode usar uma fila de mensagens transacionais, mas será assíncrona; portanto, se você depender da resposta, ela ficará confusa.
  • Você pode escrever o fato de que precisa chamar os serviços de back-end no banco de dados e, em seguida, chamar os serviços de back-end usando um lote. Novamente, assíncrono, para que possa ficar confuso.
  • Você pode usar um mecanismo de processo de negócios como seu gateway de API para orquestrar os microsserviços de backend.
  • Você pode usar o EJB remoto, conforme mencionado no início, pois ele suporta transações distribuídas prontas para uso.

Ou devo impor consistência no código do meu aplicativo (por exemplo, tendo um trabalho em segundo plano que detecta inconsistências e as corrige ou tendo um atributo "state" no meu modelo de usuário com valores "criando", "criado" etc.)?

O papel dos diabos defendem: por que construir algo assim, quando existem produtos que fazem isso por você (veja acima) e provavelmente o fazem melhor do que você pode, porque eles são experimentados e testados?

Formiga Kutschera
fonte
2

Pessoalmente, gosto da ideia de Micro Services, módulos definidos pelos casos de uso, mas, como sua pergunta menciona, eles têm problemas de adaptação para empresas clássicas como bancos, seguros, telecomunicações, etc ...

As transações distribuídas, como muitos mencionadas, não são uma boa escolha, as pessoas agora buscam mais sistemas eventualmente consistentes, mas não tenho certeza se isso funcionará para bancos, seguros, etc.

Eu escrevi um blog sobre a minha solução proposta, pode ser que isso possa ajudá-lo ....

https://mehmetsalgar.wordpress.com/2016/11/05/micro-services-fan-out-transaction-problems-and-solutions-with-spring-bootjboss-and-netflix-eureka/

posthumecaver
fonte
0

A consistência eventual é a chave aqui.

  • Um dos serviços é escolhido para se tornar o principal manipulador do evento.
  • Este serviço manipulará o evento original com confirmação única.
  • O manipulador primário se responsabilizará pela comunicação assíncrona dos efeitos secundários com outros serviços.
  • O manipulador primário fará a orquestração de outras chamadas de serviços.

O comandante é responsável pela transação distribuída e assume o controle. Ele conhece a instrução a ser executada e coordenará a execução. Na maioria dos cenários, haverá apenas duas instruções, mas ele pode lidar com várias instruções.

O comandante assume a responsabilidade de garantir a execução de todas as instruções, e isso significa se aposentar. Quando o comandante tenta efetuar a atualização remota e não obtém uma resposta, não é possível tentar novamente. Dessa forma, o sistema pode ser configurado para ser menos propenso a falhas e se recupera.

Como temos novas tentativas, temos idempotência. Idempotência é a propriedade de poder fazer algo duas vezes, de forma que os resultados finais sejam os mesmos, como se tivessem sido feitos apenas uma vez. Precisamos de idempotência no serviço remoto ou na fonte de dados para que, no caso em que receba a instrução mais de uma vez, a processe apenas uma vez.

Consistência eventual Isso resolve a maioria dos desafios de transações distribuídas, no entanto, precisamos considerar alguns pontos aqui. Toda transação com falha será seguida de uma nova tentativa; a quantidade de tentativas repetidas depende do contexto.

A consistência é eventual, ou seja, enquanto o sistema está fora do estado consistente durante uma nova tentativa, por exemplo, se um cliente encomendou um livro, efetuou um pagamento e atualiza a quantidade em estoque. Se as operações de atualização de estoque falharem e supondo que esse foi o último estoque disponível, o livro continuará disponível até que a operação de repetição da atualização de estoque tenha sido bem-sucedida. Depois que a nova tentativa for bem-sucedida, seu sistema será consistente.

Viyaan Jhiingade
fonte
-2

Por que não usar a plataforma API Management (APIM) que suporta scripts / programação? Portanto, você poderá criar um serviço composto no APIM sem perturbar os microsserviços. Eu projetei usando o APIGEE para essa finalidade.

sra
fonte