É simples começar a criar um novo aplicativo como um microsserviço até encontrarmos problemas de consistência de dados ao fazer solicitações entre microsserviços.
Em aplicativos monolíticos, geralmente usamos transações de banco de dados com propriedades ACID (Atomicity, Consistency, Isolation, Durability). No caso de uma reversão, é fácil garantir a consistência dos dados desde o início. Mas agora que nossas transações de banco de dados estão espalhadas por vários microsserviços, como podemos garantir a consistência dos dados entre eles?
Uma solução é o padrão saga - um conceito de arquitetura mais antigo que ainda é altamente relevante para microsserviços hoje. Os padrões de saga são promissores para nossos sistemas, então gostaria de compartilhar alguns de nossos principais insights, pois exploramos o uso de sagas em nossos próprios microsserviços.
O padrão da saga
A saga é uma sequência de transações locais em cada um dos microsserviços participantes. Ele tem suas próprias etapas que devem ser executadas e, quando cada uma é concluída, existe algum tipo de lógica para decidir o que fazer a seguir.
A saga deve garantir que todas as etapas sejam executadas com sucesso, ou então deve realizar um rollback, se necessário.
A imagem abaixo ilustra o fluxo de solicitações entre microsserviços para atender a uma saga de criação de pedido.
Exceções de regra de infraestrutura ou lógica de negócios podem surgir ao fazer solicitações. Todas essas exceções devem ser tratadas e, se tivermos uma exceção em uma etapa específica, também teremos que desfazer todas as alterações das etapas anteriores.
Às vezes, para fazer uma reversão completa, temos que fazer solicitações adicionais aos microsserviços. Essas solicitações adicionais são conhecidas como transações de compensação.
Tudo isso significa que o uso de sagas pode aumentar a complexidade. Como implementamos os mecanismos de reversão por conta própria, cada cenário deve ser considerado cuidadosamente.
Duas maneiras de implementar
Existem duas maneiras de implementar padrões de saga, cada uma delas com uma abordagem diferente para coordenar o fluxo de trabalho.
Orquestração (centralizada): Um coordenador central é responsável por chamar serviços remotos para cumprir a saga;
Coreografia (distribuída): Não há coordenador central, então cada serviço deve ouvir e produzir eventos e tomar decisões sobre as ações a serem realizadas.
Vamos dar uma olhada mais detalhada nesses métodos de implementação.
1. Orquestração
O método de orquestração é quando temos um coordenador centralizado que gerencia toda a lógica e sabe quando se comunicar com outros microsserviços e qual etapa fazer a seguir ou como reverter.
É uma boa prática ter um microsserviço separado para coordenação.
O método de orquestração funciona melhor quando a lógica da saga é mantida por uma ou duas equipes. Caso contrário, o código pode se tornar complicado sem ninguém responsável pela qualidade e manutenção do código.
Por outro lado, o método de orquestração é melhor para a compreensão de fluxos de trabalho mais complexos. Este também é um método mais limpo do ponto de vista arquitetônico, uma vez que os microsserviços não estão acoplados uns aos outros.
Reverter na orquestração
O coordenador deve saber como fazer o rollback em caso de falha. Por esse motivo, o coordenador deve armazenar um log de eventos para cada fluxo e realizar transações de compensação em cada microsserviço correspondente ao fazer um rollback.
Se ocorrer alguma falha em qualquer uma das solicitações, o log de eventos ajuda o coordenador a identificar quais microsserviços são afetados e em que sequência as transações de compensação devem ser enviadas.
Como você pode ver na imagem acima, a solicitação de processamento do pagamento falhou, o pedido foi rejeitado e o pagamento foi reembolsado. Também deixamos de enviar uma fatura ao cliente. Como alternativa, uma fatura pode ser enviada com a informação de que o pagamento falhou.
Este diagrama é apenas um exemplo. Na realidade, existem algumas melhorias que poderiam ser feitas neste processo, mas este exemplo torna o processo mais fácil de entender.
2. Coreografia
Esse tipo de saga é baseado em evento / assíncrono e é implementado quando o código em execução em cada serviço decide como tratar os eventos dentro de seu escopo e o que fazer em seguida.
Você também pode pensar nisso como uma cadeia de microsserviços relacionados a eventos. Cada serviço ouve os eventos dos outros e publica os seus próprios, mas infelizmente isso leva ao acoplamento entre si.
Esse método é melhor quando há mais equipes envolvidas no gerenciamento das sagas. O benefício é que cada equipe pode trabalhar exclusivamente nas sagas dentro de seu escopo.
Como não há um coordenador central, não precisamos de um microsserviço separado responsável pela coordenação de um fluxo de trabalho. No entanto, isso pode dificultar a compreensão de fluxos de trabalho mais complexos e como certos serviços estão inter-relacionados. Além disso, lembre-se de que pode haver uma dependência cíclica entre microsserviços.
Rollback na coreografia
Como não há um coordenador centralizado, os microsserviços correspondentes detectam eventos de falha para poderem retroceder.
No diagrama acima, a API do Warehouse falhou ao reservar um estoque e publica o evento “Falha na reserva” correspondente. Outros serviços respondem a esse evento - a API de pedidos rejeita o pedido e a API de pagamentos reembolsa o pagamento.
Estilos de mistura
Mesmo que esses dois tipos de saga sejam muito diferentes, eles podem ser misturados. Podemos ter fluxos de trabalho de criação de pedidos quando uma saga tem um orquestrador, mas uma das transações (por exemplo, reservar o estoque) pode ser coreografada.
Outras dicas da saga
IDs de transações globais : é uma boa ideia fornecer IDs de transações globais ao sagas. Eles podem ajudar no monitoramento ou depuração, mas compartilhar o ID da transação global em todos os eventos permite identificar qual transação falhou e tomar as ações apropriadas.
Bloqueios semânticos : ao usar o mecanismo saga em cada etapa, as alterações são comprometidas com microsserviços separados. A filosofia da saga não nos permite usar bloqueio real, o que pode levar à inconsistência de dados enquanto a saga ainda está em andamento.
Você pode incluir valores de estado temporário para evitar que outras transações alterem os valores errados. Vamos usar o status PENDENTE para indicar que um pedido está em processamento. Agora podemos evitar o cancelamento do pedido se seu status for PENDING, e outros microsserviços poderão ler o estado PENDING também.
Isso significa que devemos prestar atenção especial aos casos em que a propriedade de isolamento do CID pode não ser cumprida e garantir sua manutenção.
Idempotência : ações idempotentes ajudam a evitar problemas quando a mesma solicitação é enviada duas vezes, especialmente para transações de compensação quando reversões estão em andamento.
Retornando a resposta : Às vezes, as sagas podem abranger muitas transações que podem demorar um pouco para serem concluídas. Isso levanta a questão: devemos retornar respostas depois que todas essas transações forem concluídas ou tornar seus trabalhos assíncronos?
Para sagas mais longas, pode se tornar vital retornar respostas instantaneamente e processar as transações da saga de forma assíncrona. Eu recomendo fortemente esta abordagem. Ele segue as melhores práticas para APIs restantes. Certifique-se de que todas as solicitações sejam pequenas e executadas rapidamente.
Nesses casos, geralmente retornamos o status de http 202, que significa "aceito". O cliente pode então verificar o resultado usando o endpoint correspondente ou deve ser notificado pelo serviço API de que o saga foi executado.
Conclusão
Vimos várias maneiras de implementar o padrão de saga e seus prós e contras.
Em geral, se você precisa de processos de reversão com compensação de transação ou está lidando com transações de longa duração, as sagas são uma boa ou mesmo sua melhor opção.
Mesmo que o padrão saga introduza maior complexidade, é preferível quando você precisa gerenciar a consistência de dados entre microsserviços sem acoplamento rígido.