Aplicações resilientes com SAGA

Francisco Junior
Microservices Arquitetura

Ter aplicações resilientes implica que, à medida que a arquitetura se torna mais extensa e complexa, maiores são as chances de alguma coisa dar errado. No entanto, mesmo diante de contratempos, a aplicação resiliente possui a capacidade de continuar operando e se recuperar dessas falhas.

Ao lidar com software, nos deparamos com uma infinidade de transações, o que podemos ilustrar facilmente em um contexto de banco de dados. Vamos considerar, por exemplo, uma simples transferência bancária entre duas contas:

BEGIN TRANSACTION;

INSERT INTO bank.transaction (id, type, amount) VALUES (1, 'debit', 100);
INSERT INTO bank.transaction (id, type, amount) VALUES (2, 'credit', 100);

COMMIT TRANSACTION;

O que acontece se o primeiro insert tiver sucesso e o segundo insert falhar? Nesse caso a transação não é confirmada, logo, nenhuma das inserções será efetivada no banco de dados, mantendo-o em um estado consistente anterior à transação.

Uma forma comum de pensar em transação é pelo conceito de ACID ou Atomicidade, Consistência, Isolamento e Durabilidade. Esses princípios são fundamentais para garantir a confiabilidade e a integridade das operações em ambientes de banco de dados, especialmente em situações de concorrência e falhas do sistema.

Mas nem todas as transações são realizadas dentro do banco de dados.

Trabalhando com transações

Transação é a abstração de um conjunto de operações que devem ser tratada como uma única unidade lógica, onde para ter sucesso, todas as suas operações devem ser bem sucedidas ou serem desfeitas.

Quando trabalhamos com transações distribuídas, podemos usar como exemplo a compra de um lanche em um aplicativo de fast food, é esperado que todos os serviços da transação estejam disponíveis, desde o serviço de Order até o serviço de Delivery.

Considerando que esses dois serviços possuam uma disponibilidade de 99.5% a disponibilidade da transação será de 99%, e cada serviço adicionado reduzirá o percentual de disponibilidade da transação. Isso nos leva ao teorema CAP, criado por Erick Brewer, que nos diz que um sistema só pode ter duas das três propriedades:

  • Consistency (Consistência): Neste contexto, significa que todos os serviços envolvidos em uma transação (por exemplo, fazer um pedido e debitá-lo do estoque) apresentem uma visão consistente dos dados. Em outras palavras, todas as operações de leitura refletem a versão mais recente dos dados após uma escrita. Manter a consistência implica que não haja divergência nas respostas às consultas, independentemente de qual nó é consultado.
  • Availability (Disponibilidade): Refere-se à capacidade de um sistema distribuído responder a solicitações, mesmo diante de falhas. No nosso exemplo a disponibilidade seria crucial para garantir que os serviços estejam sempre acessíveis para aceitar pedidos, processar pagamentos e atualizar os dados. Mesmo se um serviço específico falhar, os demais serviços continuam operando para garantir que o aplicativo permaneça funcional.
  • Partition Tolerance (Tolerância a Partições): Significa que o sistema continua operando mesmo quando ocorrem falhas de comunicação entre os nós, resultando em partições de rede. Por exemplo, mesmo se o serviço de pagamento não puder se comunicar diretamente com o serviço de pedidos, ambos os serviços continuam a funcionar, garantindo que os clientes possam fazer pedidos e pagamentos.

Hoje é comum optarmos por disponibilidade ao invés de consistência. Para resolver as complexidades que envolvem a consistência dos dados na arquitetura de microsserviços devemos usar mecanismos para construir aplicações com baixo acoplamento, serviços assíncronos.

Saga

Saga é um mecanismo para manter a consistência dos dados em uma arquitetura de microsserviços sem a utilização de transações distribuídas. O padrão Saga é uma sequencia de transações locais onde cada transação atualiza os dados de um único serviço.

Com Saga iremos trabalhar com 3 tipos de transação:

  • Pivot Transaction: Referem-se a uma estratégia em que uma transação realiza uma série de operações, mas só efetiva essas operações após a execução bem-sucedida de uma etapa crucial ou “pivô”. Se a etapa crucial falhar, a transação é revertida.
  • Compensable Transaction: As transações de compensação são usadas para reverter operações realizadas em uma transação anterior que falhou ou foi interrompida. Em vez de reverter diretamente as operações originais, uma transação de compensação executa operações inversas para restaurar o sistema ao estado consistente anterior à transação.
  • Retriable Transactions: Transações retráteis referem-se a transações que podem ser repetidas (retried) com segurança em caso de falha temporária. Se uma transação falhar devido a condições transitórias, como uma falha de rede, a transação pode ser repetida sem causar problemas no sistema.

Considerando o nosso aplicativo de Fast Food, abaixo podemos ver a criação de um pedido (Order) com Saga:

Podemos considerar o seguinte fluxo de transações:

  1. Order Service: Cria um pedido com o status APPROVAL_PENDING.
  2. Consumer Service: Verifica se o cliente pode fazer um pedido.
  3. Kitchen Service: Valida os detalhes do pedido e cria um Ticket com o status CREATE_PENDING.
  4. Accounting Service: Autoriza o pagamento por cartão de crédito do cliente.
  5. Kitchen Service: Muda o status do Ticket para AWAITING_ACCEPTANCE.
  6. Order Service: Muda o status do pedido para APPROVED.

Saga utiliza transações de compensação para realizar alterações de rollback e esse fluxo pode falhar por vários motivos:

  • O cliente não pode criar pedidos;
  • O restaurante não está apto a receber pedidos;
  • O método de pagamento do cliente foi reprovado.

Os primeiros três passos do fluxo podem terminar em transações de compensação, já o quarto passo é uma transação pivô, pois ela é o ponto de go/no go do restante da transação. Já os dois últimos passos são transações retriable, pois serão executadas e bem sucedidas.

A implementação do Saga consiste na lógica de coordenação dos seus passos, e temos duas formas para isso: Orquestrado ou Coreografado.

Orquestrado

Nesse cenário existe uma lógica centralizada que faz a coordenação de cada um dos passos do Saga. Abaixo podemos ver como seria a execução dos passos no fluxo de criação de pedido.

Prós:

  • Dependências simples: Esse modelo evita a inclusão de dependências cíclicas, como a classe de orquestração conhece e aciona os passos do Saga, o mesmo não é válido para os serviços. Como resultado o orquestrador depende dos participantes mas o inverso não ocorre, evitando a criação de dependências cíclicas.
  • Baixo acoplamento: Cada serviço implementa uma API que chama o orquestrador, então ele não precisa saber sobre os eventos dos outros participantes que são publicados no Saga.
  • Melhora a separação dos conceitos(separation of concerns) e simplifica a regra de negócio: A lógica de coordenação está centralizada no orquestrador, logo os participantes do Saga só precisam se preocupar com a sua própria regra de negócio.

Contras:

  • Risco da centralização: Existe um risco de centralizar muita regra de negócio no orquestrador, para evitar isso devemos ter em mente que essa classe deve ser responsável apenas por sequenciar os passos do Saga e não conter alguma regra de negócio.

Coreografado

Aqui cada participante publica e trata eventos de forma independente, decidindo como realizar sua parte.

Prós:

  • Simplicidade: Cada serviço publica eventos quando criados, atualizados ou deletados.
  • Baixo acoplamento: Os participantes escutam os eventos sem ter conhecimento de quem os cria.

Contras:

  • Maior dificuldade de entendimento: Sem um orquestrador é mais difícil de entender o fluxo do Saga.
  • Dependência cíclica entre serviços: Os participantes do Saga podem escutar eventos um do outro, gerando dependências cíclicas.
  • Risco de acoplamento: Cada participante do Saga precisa escutar os eventos que o afetam. Por exemplo, o Account Service deve escutar todos os eventos onde o cartão de crédito do cliente é cobrado ou reembolsado. Como resultado existe um risco de alterar a implementação do ciclo de vida do pedido.

Conclusão

Essa é uma boa forma de construir aplicações resilientes mantendo um baixo acoplamento garantindo uma alta disponibilidade e uma consistência eventual.

Saga orquestrado pode ser utilizado em diferentes cenários, desde os mais simples até os mais complexos atendendo bem cada um deles. No caso do Saga coreografado ele combina muito bem com arquiteturas orientadas a eventos, fora disso é recomendado para fluxos mais simples. Além do mais existem algumas ferramentas que auxiliam o rastreamento dos eventos em um Saga coreografado utilizando OpenTelemetry por exemplo.

Referências

Compartilhe esse post