Publicações

Garantindo transações distribuidas entre microsserviços

Renan Santana
Renan Santana Backend Developer
Elicácio C. de Farias
Elicácio C. de Farias Backend Developer

Introdução

Garantir que informações espalhadas em vários locais estejam consistentes é um dos desafios enfrentados quando utilizamos uma arquitetura de microsserviços.

Apesar deste tipo de arquitetura permitir definir estrategias ágeis de desenvolvimento, implantação, escalonamento, manutenção e comunicação, além de tornar flexível a escolha de linguagens e itens de infra para cada serviço, erros sempre podem acontecer nesse "meio de campo" e precisam de tratamento adequado para garantir a consistência dos dados de cada serviço.

É nossa responsabilidade como desenvolvedores assegurar que, mesmo com os recursos distribuídos, o resultado produzido por eles possua atomicidade, e que os dados não fiquem inconsistentes em casos de falha. Esse é o grande desafio que tentaremos resolver neste artigo utilizando algumas técnicas.

Conceitos

Antes de começar, vamos definir alguns conceitos que utilizaremos ao longo desta jornada:

- Transação: Conjunto de uma ou mais operações que alteram o estado de uma entidade.

- Operação: Ação que altera efetivamente algum atributo de uma determinada entidade.

- Entidade: Dependendo do contexto pode ser um objeto em memória, uma tabela do banco de dados, um arquivo, entre outros.

Caso de uso

O sistema proposto aqui, será de uma hipotética companhia aérea, que utiliza de vários microsserviços para fazer a reserva e o pagamento de suas passagens. O fluxo de compra de passagens feito nessa companhia segue os seguintes passos:

1 - O usuário, via interface seleciona o destino.

2 - Uma API que faz o controle das passagens aéreas altera o status de uma reserva para "pendente"

3 - Uma consulta é feita numa API externa que faz o controle de assentos para determinada seção do avião.

4 - Caso o assento esteja disponível, a API altera seu status para reservado.

5 - De volta a API de passagens, o status da mesma agora é alterado para "reservada"

6 - Agora o usuário informa os dados do cartão, para pagamento da passagem

7 - Uma API externa de pagamentos processa esses dados, e valida ou não a operação

8 - O pagamento estando validado e aprovado, o usuário recebe a confirmação no frontend

Neste fluxo hipotético, temos 2 pontos onde a transação pode falhar, são eles:

  • Verificação de disponibilidade de assentos.
  • Validação do pagamento.

Repare que, se alguma dessas operações falhar devemos realizar uma ação que desfaz o passo anterior.

  • Caso não exista assento disponíveis, é preciso alterar o status da passagem de "pendente" para "cancelado" e retorna uma mesagem para o usuário.
  • Caso o pagamento seja recusado, tanto a reserva do assento, quanto a reserva da passagem devem ser canceladas e uma mensagem deve ser enviada para o usuário.

Fluxograma

Para termos uma visão mais clara sobre o fluxo, segue diagrama

Diagrama contento o fluxo de venda de passagens, com seus respectivos tratamentos em caso de falhas em algum item

Solução proposta

Usaremos um exemplo bastante simples para mostrar como organizar a coreografia entre essas operações para garantir que todas produzam o resultado esperado, ou que nada seja alterado em casos de falha.

Vamos falar um pouco sobre SAGA.

Exemplo

O sitema está divido em 4 microsserviços:

  • Formulário
  • API de Passagens
  • API de Assentos
  • API de Pagamentos

Os trechos de código abaixo estão na ordem que são executados.

Para facilitar a explicação:

  • Todas as classes e funções foram escritas em português;
  • Algumas boas práticas foram deixadas de lado;
  • Não usamos bibliotecas externas;
  • Não usamos banco de dados.

O código da aplicação está nesse repositório.

Tudo começa em: formulario/main.py

def main():
    while True:
        destino = escolhe_um_destino()
        passagem_reservada = solicita_reserva_de_passagem_para(destino)
        referencia_da_passagem = pega_referencia_da_passagem(passagem_reservada)
        solicita_pagamento(referencia_da_passagem)

1 - O serviço de formulário solicita que o usuário escolha um destino

destino = escolhe_um_destino()

2 - Solicita uma reserva de passagem para o serviço de passagens

passagem_reservada = solicita_reserva_de_passagem_para(destino)

No serviço de passagens em: passagens/main.py,

class Handler(BaseHTTPRequestHandler):
    passagens = []

    def envia_de_volta(self, resposta):
        self.wfile.write(bytes(json.dumps(resposta), "utf-8"))

    def do_POST(self):
        if self.path == "/passagens/cancela":
            manipulador = ManipuladorDeConteudo(self)
            conteudo = manipulador.pega_conteudo()

            banco = BancoDeDadosDeMentira(self)
            referencia_da_passagem = banco.marca_como_cancelada(conteudo)

            solicitador = SolicitadorDeAssentosDeMentira(self)
            referencia_do_assento = solicitador.solicita_o_cancelamento_do_assento_com(
                referencia_da_passagem
            )

            resposta = manipulador.monta_resposta(
                referencia_da_passagem, referencia_do_assento
            )

            self.envia_de_volta(resposta)

        if self.path == "/passagens":
            manipulador = ManipuladorDeConteudo(self)
            conteudo = manipulador.pega_conteudo()

            banco = BancoDeDadosDeMentira(self)
            referencia_da_passagem = banco.marca_com_pendente(conteudo)

            solicitador = SolicitadorDeAssentosDeMentira(self)
            referencia_do_assento = solicitador.solicita_reserva_com(
                {"destino": conteudo["destino"], **referencia_da_passagem}
            )

            if referencia_do_assento["status_do_assento"] == "reservado":
                referencia_da_passagem = banco.marca_como_reservada(
                    referencia_do_assento
                )
                self.envia_de_volta(referencia_da_passagem)
            else:
                referencia_da_passagem = banco.marca_como_cancelada(
                    referencia_do_assento
                )
                self.envia_de_volta(referencia_da_passagem)

3 - Trata minimamente o conteúdo

manipulador = ManipuladorDeConteudo(self)
conteudo = manipulador.pega_conteudo()

4 - Cria uma passagem com status pendente

banco = BancoDeDadosDeMentira(self)
referencia_da_passagem = banco.marca_com_pendente(conteudo)

5 - Solicita um assento para o serviço de assentos

solicitador = SolicitadorDeAssentosDeMentira(self)
referencia_do_assento = solicitador.solicita_reserva_com(
    {"destino": conteudo["destino"], **referencia_da_passagem}
)

No serviço de assentos em: assentos/main.py

class Handler(BaseHTTPRequestHandler):
    assentos = []

    def envia_de_volta(self, resposta):
        self.wfile.write(bytes(json.dumps(resposta), "utf-8"))

    def do_POST(self):
        if self.path == "/assentos/cancela":
            manipulador = ManipuladorDeConteudoDeMentira(self)
            referencia_da_passagem = manipulador.pega_conteudo()

            banco = BancoDeDadosDeMentira(self)
            referencia_do_assento = banco.marca_como_cancelado(referencia_da_passagem)

            self.envia_de_volta(referencia_do_assento)

        if self.path == "/assentos":
            banco = BancoDeDadosDeMentira(self)

            manipulador = ManipuladorDeConteudoDeMentira(self)
            conteudo = manipulador.pega_conteudo()

            verificador = VerificadorDeAssentoDeMentira()
            existe_assento_disponivel = verificador.verifica(conteudo)

            if existe_assento_disponivel:
                referencia_do_assento = banco.marca_como_reservado(conteudo)
                self.envia_de_volta(referencia_do_assento)
            else:
                referencia_do_assento = banco.marca_como_cancelado(conteudo)
                self.envia_de_volta(referencia_do_assento)

6 - Abre uma conexão com o banco de dados fictício

banco = BancoDeDadosDeMentira(self)

7 - Trata minimamente o conteúdo

manipulador = ManipuladorDeConteudoDeMentira(self)
conteudo = manipulador.pega_conteudo()

8 - Verifica se existe assento disponível para o destino escolhido

verificador = VerificadorDeAssentoDeMentira()
existe_assento_disponivel = verificador.verifica(conteudo)

9 - Saga em ação. Primeiro ponto de falha:

Depois de verificar a disponibilidade de assentos, o serviço precisa decidir qual fluxo deve seguir:

Fluxo normal:

  • Criar um assento marcado com reservado e devolver a referência dele para o serviço de passagens.

Fluxo de compensação:

  • Criar um assento marcado com cancelado e devolver a referência dele para o serviço de passagens.
if existe_assento_disponivel:
    referencia_do_assento = banco.marca_como_reservado(conteudo)
    self.envia_de_volta(referencia_do_assento)
else:
    referencia_do_assento = banco.marca_como_cancelado(conteudo)
    self.envia_de_volta(referencia_do_assento)

Repare que, do ponto de vista de código, é apenas uma estrutura de decisão, no entanto, é exatamente essa decisão que garante que a transação ou siga em frente e produza um resultado consistente, ou retorne desfazendo tudo que já tinha feito.

De volta para o serviço de passagens em: passagens/main.py

10 - Saga em ação. Segundo ponto de falha:

Com a resposta do serviço de assentos, o serviço de passagens verifica se o assento foi reservado ou não, de acordo com a resposta, ele pode seguir:

Fluxo normal:

  • Alterar o status da passagem para reservada e devolver a referência dela para o serviço de formulário.

Fluxo de compensação

  • Alterar o status da passagem para cancelada e devolver a referência dela para o serviço de formulário.
if referencia_do_assento["status_do_assento"] == "reservado":
    referencia_da_passagem = banco.marca_como_reservada(
        referencia_do_assento
    )
    self.envia_de_volta(referencia_da_passagem)
else:
    referencia_da_passagem = banco.marca_como_cancelada(
        referencia_do_assento
    )
    self.envia_de_volta(referencia_da_passagem)

De volta para o serviço de formulário em: formulario/main.py

11 - Pega uma referência da passagem e solicita o pagamento

referencia_da_passagem = pega_referencia_da_passagem(passagem_reservada)
solicita_pagamento(referencia_da_passagem)

No serviço de pagamentos em: pagamentos/main.py

class Handler(BaseHTTPRequestHandler):
    pagamentos = []

    def envia_de_volta(self, resposta):
        self.wfile.write(bytes(json.dumps(resposta), "utf-8"))

    def do_POST(self):
        if self.path == "/pagamentos":
            gerador = GeradorDePagamentoDeMentira()
            banco = BancoDeDadosDeMentira(self)

            manipulador = ManipuladorDeConteudoDeMentira(self)
            conteudo = manipulador.pega_conteudo()

            verificador = VerificadorDePagamentoDeMentira()
            pagamento_esta_valido = verificador.verifica_pagamento(conteudo)

            if pagamento_esta_valido:
                pagamento_valido = gerador.gera_pagamento_valido(conteudo)
                referencia_do_pagamento = banco.salva(pagamento_valido)
                self.envia_de_volta(referencia_do_pagamento)
            else:
                referencia_da_passagem = manipulador.pega_referencia_da_passagem(
                    conteudo
                )
                solicitador = SolicitadorDePassagemDeMentira(self)
                referencia_da_passagem = (
                    solicitador.solicita_o_cancelamento_da_passagem_com(
                        referencia_da_passagem
                    )
                )
                pagamento_invalido = gerador.gera_pagamento_invalido(conteudo)
                referencia_do_pagamento = banco.salva(pagamento_invalido)
                self.envia_de_volta(referencia_do_pagamento)

12 - Abre uma conexão com o banco fictício, e outra com o gerador de pagamentos

gerador = GeradorDePagamentoDeMentira()
banco = BancoDeDadosDeMentira(self)

13 - Trata mimimamente o conteúdo

manipulador = ManipuladorDeConteudoDeMentira(self)
conteudo = manipulador.pega_conteudo()

14 - Verifica se o pagamento é válido

verificador = VerificadorDePagamentoDeMentira()
pagamento_esta_valido = verificador.verifica_pagamento(conteudo)

15 - Saga em ação. Terceiro ponto de falha.

Mas uma vez, só que agora no serviço de pagamentos, chegamos em um ponto de decisão na transação, onde mais vez, a sequência de operações pode seguir:

Fluxo normal:

  • Gerar um pagamento válido, criar um pagamento com status pago devolver a referência dele para o serviço de formulário.

Fluxo de compensação:

  • Gerar um pagamento inválido, criar um pagamento com status cancelado e devolver a referência dele para o serviço de formulário.
if pagamento_esta_valido:
    pagamento_valido = gerador.gera_pagamento_valido(conteudo)
    referencia_do_pagamento = banco.salva(pagamento_valido)
    self.envia_de_volta(referencia_do_pagamento)
else:
    referencia_da_passagem = manipulador.pega_referencia_da_passagem(
        conteudo
    )
    solicitador = SolicitadorDePassagemDeMentira(self)
    referencia_da_passagem = (
        solicitador.solicita_o_cancelamento_da_passagem_com(
            referencia_da_passagem
        )
    )
    pagamento_invalido = gerador.gera_pagamento_invalido(conteudo)
    referencia_do_pagamento = banco.salva(pagamento_invalido)
    self.envia_de_volta(referencia_do_pagamento)

Transação completa

Por fim, depois de processar o pagamento, a execução volta para o serviço de formulário, que mostra uma mensagem de sucesso ou erro dependendo do resultado das operações dentro da transação.

  • Se tudo correu bem, uma passagem e um assento foram marcado como reservados e um pagamento foi registrado como pago.
  • Se não havia assento disponível para um determinado destino, tanto a passagem quanto o assento foram marcados como cancelados e nenhum pagamento foi solicitado.
  • Se as informações de pagamento estiverem inválidas, passagem, assento e pagamento serão marcados como cancelados.

Conclusão

Conforme visto, garantir a atomicidade de transações distribuídas pode se mostrar um desafio e tanto, mas é possível utilizando o padrão de projeto correto.

É importante se atentar a todo possível ponto de falha, e escrever um fluxo de rollback para cada um destes pontos.

Novamente, o código utilizado neste exemplo você encontra aqui. Tentamos manter nosso exemplo o mais simples possível, e livre de dependências externas, para focar no padrão SAGA em si.

Referências

https://docs.python.org/3/library/http.server.html

https://docs.python.org/3/library/http.client.html

https://docs.python.org/3/library/http.html

https://microservices.io/patterns/data/saga.html

Solução de problemas, melhoria de gestão e aumento de produtividade

Com tecnologias renomadas, aplicamos práticas ágeis e capacitamos sua equipe.

Fale com um consultor