Publicações

Garantindo transações distribuidas entre microsserviços

Renan Santana
Renan SantanaBackend Developer
Elicácio C. de Farias
Elicácio C. de FariasBackend Developer
Publicação:  16-05-2022

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