🇧🇷 Pizzaria e Sistemas Resilientes

Quando pensamos em serviços distribuídos, sempre dependemos de transferência de dados pela rede, mas você acha que a rede é confiável ?

Sabemos que não. Certamente você já viu problemas com hardware, software ou segurança quando fazia uma comunicação a um serviço remoto.

Focando um pouco no código agora, eu tenho certeza que você já viu isso uma vez na sua vida:

var service = new MyWebService();
var result = service.Process(request);

Existe uma espécie de mágica acontecendo aqui:
Primeiro o dado é serializado, enviado pela rede, chega ao destino, o dado é deserializado e mapeado para um objeto, o código é invocado com o dado da requisição, o resultado é obtido e serializado, enviado de volta, deserializado e finalmente o código que fez a chamada é capaz de continuar a execução.

Existe um grande processo acontecendo entre as duas linhas de código, como explicado acima, e muitos problemas podem acontecer, por exemplo, o mais comum: timeouts. Veja na simples imagem abaixo a complexidade que isso pode ter considerando toda a mágica.

Request síncrona (request x response)

O que desenvolvedores geralmente fazem é: Um timeout aconteceu, vamos criar um log informando que algo de errado aconteceu e consertar mais tarde.

Então o seu sistema se torna menos resiliente a falhas e seu time teria que ser proativo, tentar achar o problema e falhar ao tentar encontrar, porque a maioria das vezes da certo, exceto algumas vezes que falha sem motivo aparente. Também sempre existirão as perguntas: Quando aconteceu o timeout? A requisição chegou no destino ? O servidor processou a mensagem ? Todos os efeitos colaterais aconteceram ? Se não, o que ainda preciso fazer para que os dados fiquem coerente ?

Eu poderia enumerar dezenas de perguntas, que seu time poderia fazer e gastar muito tempo para consertar cenários como esse.

Um fato engraçado nesse tipo de cenário é que sempre costumamos ouvir a famosa frase:

Funciona na minha máquina!

Tendo dito isso, quero propor para você que é necessário uma mudança de mindset e arquitetura para resolver esses casos.

Para introduzir esse mindset, quero que você tente responder essa pergunta pra você mesmo: Porque você precisa que os processos no seu sistema precisam ser síncronos ?

Na maioria dos casos você não precisa fazer uma requisição e receber a resposta no mesmo tempo. Você pode lidar de certa forma que eventualmente você receberá uma resposta no futuro.

Deixe-me contar um paralelo em um caso real para um fácil entendimento:

Você vai a uma pizzaria, o garçom vem até você e então você pede uma pizza de pepperoni. O garçom leva seu pedido à cozinha, que eventualmente o chef vai pegar, preparar, colocar no forno e depois de alguns minutos, quando sua pizza ficar pronta, o chef “grita” na cozinha: Pedido 87 pronto! O garçom pega o pedido e leva até você.

Se request-response fosse possível na vida real, o garçom ficaria parado na sua frente de forma constrangedora até que a sua pizza ficasse pronta. E ele não pode fazer isso, pois ele precisa aceitar pedidos de outros clientes e redirecioná-los para a fila da cozinha.

Não há nada de diferente entre essa história e sistemas distribuídos resilientes.

Um sistema (você) deve enviar uma mensagem através de um message broker (garçom) requisitando algo (order). Um worker (chef) eventualmente vai pegar a mensagem e processar (preparar o pedido). Uma vez o processo está pronto o worker envia uma mensagem (pedido pronto) através do message broker e o pedido é entregue para um serviço que está interessado nessa mensagem (você).

Se você introduzir esse conceito no seu sistema, ele se tornará menos dependente de quem processa a request, enviando uma mensagem pelo broker contendo os detalhes da sua requisição (envio de comandos) e mais tarde escutando uma eventual mensagem que algo aconteceu com aquela requisição (também conhecido como eventos).

Fluxos assíncronos

Voltando às falhas de redes, agora você desacoplou a maioria dos problemas relacionados a rede e o message broker agora tem essa responsabilidade de tentar resolver possíveis problemas para você.

Existem algumas estratégias disponíveis para um message broker:

  • Se um worker falhar ao processar uma mensagem, o message broker pode adotar uma estratégia de repetição progressiva, repetindo a requisição em 1 segundo, depois 5 segundos, 30 segundos, 1 minuto e assim por diante… Você deverá definir a melhor estratégia.
  • Se mesmo assim o processar da mensagem continua falhando, parece que não se trata de um problema transiente e alguém precisa olhar. Então depois da estratégia de repetição progressiva o message broker redireciona a mensagem para um local diferente (dead letter queues), onde alguém pode olhar a mensagem, consertar o problema e repetir a requisição manualmente.

Todas essas estratégias adicionam uma camada de resiliência no seu sistema e seu time não precisa de se preocupar com timeouts ou qualquer problema eventual. O seu time pode começar a se preocupar com os problemas depois que o message broker garantiu que não era um problema eventual. Assim é mais fácil de ter precisão ao identificar um problema.

Introduzir esse tipo de comunicação é um desafio técnico e cultural, pois algumas mudanças no estilo de codificação e arquitetura são necessárias. Abaixo vou descrever algumas consideração que serão necessárias.

Workers são reentrantes (Idempotência)

Considerando o fato de que uma mensagem mensagem pode ser enviada várias vezes para um worker, o resultado do processamento deve ser sempre o mesmo.

Por exemplo, se você tem um serviço que cria uma fatura para um cliente, você precisa codificar seu worker de forma que ele gere uma fatura por mensagem. Você não quer criar notas fiscais todas as vezes que uma mesma mensagem é enviada a um worker.

Garanta os efeitos colaterais

Se o seu worker processa algo que outros sistemas podem estar interessados no resultado dele, tenha certeza que você sempre envie suas mensagens, mesmo se a operação se trata de uma mensagem repetida por causa de uma falha anterior.

Pode haver o caso que você processou a mensagem, persistiu e falhou ao enviar a mensagem informando que o processo finalizou com sucesso. A próxima vez que o worker receber essa mensagem, o dado estará no estado desejado, mas as mensagens ainda não foram enviadas.

Enviar essas mensagens nunca deve ser um problema, uma vez que todos os workers são idempotentes.

Monitore a saúde dos seus workers

Pelo fato de que é impossível um sistema ser 100% resiliente, você deve adotar estratégias para acompanhar a saúde do sistema.

  • Crie alertas
    Você não quer fechar os olhos e ficar cego ao que está acontecendo com o sistema, como o tempo de processamento de mensagens ou uso de memória e CPU.
  • Resolva as mensagens com falha o mais rápido possível
    Se você não ter atenção, as mensagens poderão acumular de uma forma que você poderá perder mensagens muito importantes que possivelmente estão falhando.

Essa é só uma introdução de como sistemas resilientes podem te ajudar no desenvolvimento e suporte diário. E há muito mais para falar em posts futuros. Se você acha o tópico importante, me siga @_arleypadua no twitter para se manter atualizado.

Te vejo na próxima!

Esse artigo também está disponível em inglês.

Software Engineer and passionate about distributed systems

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store