Transações distribuídas em micro-serviços

Carlos Augusto Grahl
Senior Sistemas
Published in
8 min readJul 27, 2018

O uso de micro-serviços para o desenvolvimento de aplicações é quase uma hype hoje em dia, sendo considerada a forma mais moderna de desenvolvimento de aplicações. Afinal traz vários benefícios:

  • É agnóstico de linguagem e de forma de armazenamento. Cada serviço pode ser desenvolvido em uma linguagem diferente, conforme a necessidade ou a expertise da equipe (Isto, na minha opinião, se for realmente feito em grande escala acaba trazendo outros problemas de gerenciamento, mas isso é outra história).
  • Atualização independente.
  • Escalabilidade independente em produção. Conforme a necessidade podem ser carregadas mais instâncias de um determinado serviço para atender a alguma demanda momentânea.
  • Menor custo na cloud (teoricamente, levando em conta o item acima… se for possível escalar partes pequenas, o aumento do uso da cloud é pequeno também. Levantar vários monolitos que contém o sistema inteiro só porque uma pequena parte dele requer mais instâncias fica mais caro).
  • Mais fácil de testar, pois o escopo é menor.

Existem mais benefícios, estes foram alguns que lembrei agora. Junto com o uso de containers, Kubernetes, deploy contínuo, etc.. fazem o desejo de consumo de muitas equipes de desenvolvimento.

Mas nem tudo são flores. Quanto mais distribuído for um sistema, mais complexo é mantê-lo rodando e consistente.

Nunca se esqueça da Chaos Engineering (veja mais detalhes aqui e aqui): tudo vai acabar falhando algum dia: a rede vai cair ou ficar muito lenta, o disco vai estragar, o servidor vai cair.. e como garantir consistência em um cenário caótico?

Uma grande alteração na forma de desenvolver sistemas distribuídos é a forma como as grandes transações de negócio são tratadas. Vejamos um exemplo simples: ao ser gerada uma nova Nota Fiscal de venda, devem ser gerados títulos a receber (afinal o cliente vai pagar aquela NF :-)) e também deve ser realizada a baixa dos produtos vendidos do estoque.

Existem três grandes domínios de negócio ali:

  • Comercial, que trata a Nota Fiscal;
  • Financeiro, que trata os títulos a receber;
  • Estoque, que gerencia o estoque dos produtos;

No sistema monolítico tradicional, isto tudo é tratado como uma única transação de banco, conforme a figura a seguir:

Transação em um sistema monolítico

Já em um sistema baseado em micro-serviços provavelmente teremos três serviços para fazer esta mesma operação, cada um com sua base de dados independente e cada um realizando a sua transação localmente, conforme a figura abaixo:

Transações em um sistema baseado em micro-serviços

Agora suponha que não tenha mais em estoque o produto adquirido pelo cliente. No sistema monolítico isso é facilmente resolvido através do rollback da transação toda: quando a falta de produto em estoque foi detectada, basta dar um rollback que a nota e o título não serão gravados e a vida segue.

Já no sistema distribuído, a Nota Fiscal e o título já estão gravados e commitados nos seus respectivos bancos de dados. Estes serviços precisam desfazer esta operação em caso de erro no estoque.

A operação de desfazer nem sempre uma operação simples. Um delete muitas vezes não é o inverso de um insert. Um lançamento feito em uma conta bancária ou no seu cartão de crédito nunca é apagado. É feito um novo lançamento descontando o valor debitado anteriormente. São dois inserts. Outro exemplo: um dos passos do processo pode ser enviar um email para o comprador. Não tem como “desenviar” um email. O jeito é enviar um email com o texto de “Por favor, ignore o email anterior”. E por aí vai, são incontáveis situações que podem ocorrer.

Então como resolver esta situação? Uma forma de tratar isso é através do pattern SAGA. Este pattern foi citado em um paper de Hector Garcia Molina, em 1987. Obviamente ele foi escrito antes da criação dos micro-serviços mas a ideia dele é basicamente dividir uma grande transação em transações menores. Este conjunto de pequenas transações menores formam uma SAGA, que deve ao seu final devem deixar o sistema consistente, seja realizando todos os passos ou nenhum deles. Quando ele foi escrito, o intuito era reduzir ao máximo possível os travamentos de registros (SELECT FOR UPDATE, por exemplo) do banco de dados relacional. Fazendo várias transações curtas o risco de travamento é bem menor.

A diferença agora é que estas transações estão distribuídas em bases e micro-serviços distintos, mas a ideia da SAGA é mantida.

A SAGA pode ser realizada de duas formas: coreografia e orquestração.

  • Coreografia: Não existe uma figura central que coordena a execução. Todos os elementos sabem o que tem que fazer e quando devem fazê-lo. Eles têm consciência de que fazem parte de um todo maior. Imagine uma apresentação de dançarinos, com sua respectiva coreografia: Eles sabem que ao final de um determinado ato, outros dançarinos vão realizar outra parte da dança, que depois segue para outros dançarinos e por aí vai..
  • Orquestração: Existe um coordenador, que diz o que cada parte deve executar. O participante não sabe o que ocorreu antes nem o que vai ser executado depois, apenas executa a sua parte quando recebe a ordem para fazê-lo. É o caso de uma orquestra: o maestro vai coordenando os músicos e cada um faz sua parte.

A SAGA baseada em coreografia tende a ficar muito complexa quando existem muitos serviços e muitas interações entre eles. Todos os serviços devem escutar os eventos gerados por todos os demais e tomar decisões baseados nos mesmos. Veja a figura abaixo, por exemplo:

Exemplo de SAGA baseada em coreografia

A transação inicia no serviço comercial, que gera a Nota Fiscal e emite um evento de Nota Gerada. O serviço financeiro recebe este evento e gera o título. Ao final, lança o evento de Título gerado. O serviço de estoque então recebe este evento e tenta dar baixa no produto e ocorre um erro. Ele lança então o evento de Erro estoque, que é recebido pelo serviço do financeiro, que deve então cancelar o título já criado de alguma forma e lançar o evento de Título excluído, que será então processado pelo serviço Comercial, para cancelar a Nota Fiscal e retornar o erro para quem solicitou a criação da mesma.

Perceba que alterar o processo ou mesmo executar mais de um processo de negócio com os mesmos participantes é bem complexo. Para processar uma Nota Fiscal de Brinde, por exemplo, que não envolve o financeiro, o serviço de estoque deve escutar o evento de Nota Gerada para fazer diretamente a baixa no estoque. E caso não consiga, o serviço do comercial deve processar o evento de Erro estoque também. Os mesmos eventos são escutados por todos os serviços e com comportamentos diferentes conforme a transação de negócio que está sendo executada.

Imagine a implementação de um método que trata estes eventos como seria!

Por exemplo:

public void erroEstoqueEventListener {if (notaVenda) {// não faz nada, pois primeiro precisa cancelar o título. Aguarda o evento de título cancelado} else if (notaBrinde) {//cancela a nota, já que não foi gerado título}

E a complexidade iria aumentar para cada tipo de Nota Fiscal existente (e olha que são muitas).

Imagine agora adicionar um novo participante neste processo: um outro serviço que faz a integração com alguma operadora de cartão de crédito para receber o pagamento do cliente. Depois pode-se adicionar outro serviço de WMS (Gerenciamento de depósitos de mercadorias), para fazer a separação do item vendido na Nota para entregá-lo ao cliente. São mais eventos, mais comunicação entre todos os serviços. Para perder o controle sobre isso é muito fácil.

Outro problema aqui é todos os serviços precisam conhecer os demais. Isto pode acabar gerando uma dependência entre eles. Em casos extremos, pode levar até ao “Monolito distribuído”, em que a dependência é tanta que todos os serviços (ou grande parte deles) precisam ser atualizados juntos para manter o sistema rodando. É o pior dos dois mundos: junta todos os problemas do monolito e todos os problemas de um sistema distribuído.

Eu particularmente prefiro o uso de um SEC — Saga Execution Coordinator (Coordenador de execução de SAGA). Estes coordenadores funcionam como orquestradores para as SAGAS. Só eles conhecem todos os participantes e a forma como eles se relacionam. Por exemplo:

Exemplo de SAGA baseada em orquestração

Repare que no desenho acima os serviços nunca se comunicam diretamente (isso pode ocorrer, mas não para resolver a transação de negócio e sim por outros motivos). Apenas o orquestrador da SAGA do Comercial sabe quem participa e qual a interação entre os serviços. Seguindo a numeração, fica fácil acompanhar a execução da SAGA:

  1. Inicialmente o SEC manda o serviço Comercial gerar a Nota
  2. Quando a nota está gerada, o SEC recebe o evento de Nota Gerada
  3. Então ordena ao serviço financeiro para ele gerar o título a receber
  4. O SEC recebe então o evento de Título gerado
  5. Depois de gerado, então ordena ao serviço de estoque para efetuar a baixa dos produtos da Nota
  6. Ocorreu um erro ao efetuar a baixa, o serviço de estoque lança o evento Erro estoque
  7. O SEC recebe o evento e sabe que foi gerado o título, então ordena ao serviço financeiro para efetuar o cancelamento do mesmo
  8. O SEC depois recebe o evento de título cancelado e pode então mandar cancelar a Nota Fiscal (isto não está ilustrado no diagrama, mas já deu para entender, né?)

Qual seria o trabalho aqui para adicionar novos participantes à SAGA? Apenas alterar o algoritmo que sabe quais os passos que a SAGA deve executar. Nenhum serviço precisaria de alteração por causa disso.

Para processar outros tipos de notas fiscais (como a de brinde, citada acima), bastaria criar outra SAGA (no mesmo SEC) que sabe tratar notas de brinde.

Basicamente, o SEC é um conjunto de máquinas de estado, uma máquina para cada tipo de transação de negócio que ela processa.

Alguns cuidados devem ser tomados:

  • Não criar SECs inteligentes chamando serviços burros. A única função da SEC é orquestrar as chamadas entre os serviços. Nenhuma regra de negócio deve estar ali.
  • As SECs devem apenas fazer a comunicação entre serviços. Não devem orquestrar chamadas dentro do mesmo serviço. Isso é implementação interna dele.

O tratamento de transações de negócio distribuídas entre micro-serviços não é algo fácil nem muito corriqueiro. É algo relativamente novo e não existem muitos cases de sucesso públicos em relação a isso (se você souber de algum, compartilhe com a gente aí embaixo nos comentários).

O uso de SAGA e SECs parece ser a melhor alternativa no momento.

--

--

Carlos Augusto Grahl
Senior Sistemas

Arquiteto de software na HBSIS/Ambev, com mais de 30 anos de experiência em TI, com formação em eletrônica e Análise & Desenvolvimento de sistemas. Nerdzão.