Hoje em dia sabemos que a utilização de microsserviços está bastante difundida em vários projetos e em várias empresas. E entre vários desafios que esse modelo arquitetural carrega, está o de lidar com consistência de dados em operações que envolvem mais de um serviço e possivelmente vários bancos de dados.
E dentro desse cenário de sistemas distribuídos, e com a necessidade de lidar com contexto transacional, temos um padrão que cai como uma luva, o SAGA Pattern.
Apesar de muitos já terem ouvido falar, talvez nem todos tenham compreendido. Porém, podemos afirmar que uma vez entendido, ao tentar implementá-lo podem surgir questionamentos e desafios.
Existe bastante material na internet sobre esse pattern, porém meu objetivo aqui é tentar contribuir de alguma forma para o seu bom entendimento.
Origem e conceito
Bom, a ideia original do SAGA surgiu na verdade em 1987 através de um paper de dois cientistas da computação da universidade de Princeton. Em sua concepção o objetivo era suportar transações ou operações de longa duração, o que eles deram o nome de LLTs (Long Lived Transactions).
Mais recentemente, as sagas foram aplicadas para ajudar a resolver problemas de consistência em sistemas distribuídos modernos - conforme documentado em um artigo de sagas distribuídas de 2015.
Desde então o SAGA evoluiu e passou a se encaixar bem em situações onde você precisa lidar com operações que envolvam diversos microsserviços que precisam apresentar alguma consistência nos dados.
Se no modelo monolítico você faz uma transação no banco e grava tudo, no modelo de microsserviços essa transação será "virtualizada" uma vez que a operação em si é composta por transações que acontecem em cada serviço participante.
A ideia é simples, é fazer com que cada microsserviço execute uma operação alterando sua base de dados . No final do processo todos estarão com suas bases de dados atualizadas e consistentes.
Se ocorrer alguma falha na execução do processo em algum serviço, esse processo faz com que ações compensatórias sejam executadas nos serviços que operaram antes.
Vale ressaltar aqui que não importa para o SAGA falhas condicionais ou parciais.
Deixa eu me explicar. Se em um sistema de e-commerce a falha acontece no serviço de payment devido o cartão do buyer estar inválido ou algo do tipo, o sistema ainda sim poderia tentar concluir a transação utilizando outros cartões que o cliente possa ter cadastrado na plataforma ou outras formas de pagamento disponíveis (aqui não entro no mérito se essa funcionalidade faz sentido ou não).
Só após esgotadas todas as tentativas do sistema concluir a transação ele deve “avisar” aos demais para dar início às ações compensatórias. O padrão SAGA se interessa por sucesso definitivo ou falha definitiva.
SAGA também pode ser mais um nome dado para consistência eventual. Mas de certa forma ele expõe a ferida, no sentido que temos que pensar em um tratamento caso as operações do seu sistema não tenham sucesso, que seria as ações compensatórias.
Então se pudesse resumir o padrão em uma frase e de forma bem informal, caberia dizer que é basicamente um conceito de pilha, porém com uma funcionalidade de “Desfazer (CTRL+Z)”.
E, obviamente, sempre que se fala em SAGA precisamos falar sobre as duas estratégias que existem para sua implementação (Orquestração e coreografia).
Orquestração
No modelo orquestrado existe um componente central que se encarrega de acionar todos os serviços que fazem parte de uma SAGA. Caso sucesso, o agente ativa o próximo serviço. Caso falhe, esse agente orquestra as ações compensatórias. Esse componente é também conhecido como Saga Execution Coordinator (SEC).
Nesse modelo se tira dos microsserviços o conhecimento sobre o fluxo. Existe um outro componente que conhece o fluxo e vai coordenar a execução da operação de modo que os microsserviços vão se concentrar em fazer suas operações.
Aqui não devemos nos prender em uma estratégia de comunicação sempre síncrona. É bastante comum o uso de um Message Broker dando suporte ao fluxo possibilitando que os serviços sejam acionados ao receberem uma mensagem de um tópico que lhe interessa e poste de volta um novo evento como resultado da sua execução. Esse evento disparado permite que o orquestrador acione o próximo processo a ser executado.
Vale ressaltar também que o agente orquestrador pode ser usado como uma espécie de máquina de estado das diversas SAGAS que passam por ele.
Tá, mas nem tudo são flores! Algumas questões podem vir à nossa mente sobre os desafios que essa estratégia trás. Alguns deles seriam:
E se um step da saga demorar demais para ser executado?
Pode acontecer o que o seu negócio precisar que aconteça! Seu sistema pode trabalhar com um tempo de timeout que cancela a operação e a partir daí pode-se executar retentativas ou consultar o serviço chamado para saber se a operação foi concluída antes de realizar algum tipo de reprocessamento ou início de ações compensatórias. Aqui quem manda é o negócio!
Para a abordagem orquestrada, ao usarmos ela, não estaríamos criando um single point of failure?
Acredito que é preciso pensar na criticidade disso e principalmente ter maneiras de saber que sua SAGA foi interrompida. Por isso, considero que é ideal utilizar um serviço de mensageria para ter o registro da mensagem para ser trabalhada. Talvez não tentar fazer com que o erro seja impossível mas que quando aconteça, que ele seja visível.
Além disso, pode ser legal utilizar a "máquina de estado" do orquestrador para sabermos em que estado ele estava quando houve a falha.
Será que sempre conseguiríamos fazer o orquestrador ser de fato apenas um orquestrador?
É um ponto de atenção importante pois vejo um grande risco de termos anomalias no desenho da solução. O modelo orquestrador pode até ser mais “agradável aos olhos”, mais "visual". Porém com o passar do tempo pode receber regras de negócio que não deveria. Regra de negócio dos microsserviços “vazando” para o orquestrador.
Existe, entre outros, um framework em Java que propõe uma implementação do padrão SAGA para microsserviços em Java utilizando a estratégia orquestrada com JDBC/JPA e Spring Boot/Micronaut: Lib Eventuate Tram
Coreografia
Essa outra estratégia é baseada em eventos. Cada “bailarino”, ou cada serviço sabe qual evento disparar e qual evento ouvir para que a saga seja completa. Caberá a cada serviço "avisar" ao seu sucessor que conseguiu realizar a operação com sucesso ou ao seu antecessor caso ocorra alguma falha.
Já de antemão, essa abordagem possui a vantagem de não introduzir um ponto único de falha, já que as responsabilidades são distribuídas entre os participantes do Saga.
A própria utilização de um mecanismo de mensageria já traz consigo algumas boas possibilidades de resiliência como retentativas caso haja qualquer tipo de indisponibilidade em algum serviço no meio do processo, por exemplo.
Além disso, essa estratégia pode se encaixar bem para fluxos de trabalho simples que exigem poucos participantes e não precisam de uma lógica de coordenação.
Mas novamente, não existe solução perfeita. Podemos citar algumas fraquezas desse modelo como por exemplo:
Risco de dependência cíclica entre os participantes do Saga porque eles precisam consumir os comandos uns dos outros;
Quanto maior os passos, mais difícil o tracking do caminho por onde acontece a execução da SAGA;
O fluxo de trabalho pode ficar confuso ao adicionar novas etapas, pois é difícil controlar quais participantes do Saga ouvem quais comandos.
Qual a mais adequada?
Aqui vale a tão famosa resposta: Depende!
A decisão de qual estratégia utilizar vai depender muito da característica que seu negócio possui, além da expertise do time para implementar o pattern e também das ferramentas disponíveis para suportar tal abordagem.
Outros pontos importantes a serem considerados
Existem SAGAS onde as operações ocorrem em paralelo. Não necessariamente precisam ser sequenciais;
Para incluir um sistema em uma SAGA, não é algo tão trivial. Seu método, operação ou transação precisa ser reversível;
Tempo de duração: Uma SAGA pode demorar alguns segundos ou pode demorar mais tempo como alguns dias, dependendo da característica do seu sistema.
Cenários com as ações compensatórias
Compartilho aqui questionamentos ou cenários que imaginei enquanto me aprofundava sobre o tema das ações compensatórias dentro desse pattern e possíveis formas de tratá-los.
Mas se a ação de compensação também estiver sujeita a falhas?
Podemos usar aqui padrões de resiliência como Retry e Circuit Breaker em um cenário de comunicação síncrona, mais comum no modelo orquestrado.
No cenário de coreografia, pode-se utilizar ferramentas de mensageria que já incorporem certa resiliência como no caso em que ela envie a notificação e não receba o Ack ela mantém a mensagem na fila e tenta reenviar algumas vezes até enviar e receber o ack do Consumer/Subscriber.
Devo sempre realizar um "rollback"?
Uma ação compensatória pode não ser simplesmente voltar para o status que estava antes. Por exemplo: num caso de falha no processamento de uma pagamento de uma compra, o correto seria realizar o estorno do valor pago e não voltar o saldo como era pois pode ter havido outras transações na conta do usuário.
Um outro exemplo emblemático dessa não trivialidade seria uma da etapa do processo ser o envio de e-mail. Se houver falha que interrompa a SAGA em execução, não tem como desfazer esse envio. O correto seria, suponho, enviar outro e-mail de errata.
Devo realmente disparar uma compensação para falha em toda e qualquer transação?
No exemplo utilizado nas ilustrações acima, pode não fazer sentido para o negócio que seu sistema atende já acionar o cancelamento de uma order quando o gateway de pagamento estiver indisponível momentaneamente e, como consequência, impactando a execução da transação dentro do serviço de payment. Isso seria prejudicial já que se perderia uma venda e o cliente impactado talvez não voltasse a comprar em tal e-commerce novamente. O ideal seria ter mecanismos de tratamento e retentativas para ser possível concluir toda a SAGA e garantir a efetivação daquela intenção de compra.
Com isso, podemos afirmar que uma das coisas mais difíceis é pensar em como lidar com as ações compensatórias. É necessário projetar as etapas de sua saga de forma que você sempre consiga dar rollback de uma visão negocial. Inclusive, projete seu sistema de forma que uma vez que você concluiu uma etapa e precisa dar um rollback você não tenha que dar rollback do rollback.
Considerações Finais
Acredito que para uma boa implementação do padrão SAGA, o que manda no final das contas é a regra de negócio da sua aplicação. A arquitetura da sua solução tem que estar totalmente alinhada com as necessidades que seu sistema propõe atender.
Apesar de bastante útil e eficiente em diversos cenários. Talvez essa complexidade de uma SAGA não seja necessária pelo o que diz a regra. Além disso, ele pode não ser tão adequado quando se tem transações fortemente acopladas ou até mesmo dependências cíclicas.
Por fim, espero ter contribuído de alguma forma para o entendimento desse pattern importante para lidar no mundo onde sistemas distribuídos estão cada vez mais presentes no nosso dia a dia. Obrigado pelo tempo dedicado à leitura!
Referências
Distributed Sagas for Microservices
Pattern: Saga - Chris Richardson
Top comments (0)