DISCLAIMER: Não tome esse texto por verdade absoluta. São apenas pensamentos que eu tive, e que acho que poderão ser úteis para alguém. Na medida do possível, tentarei acrescentar bastante links com informações aprofundadas sobre o assunto.
Talvez esse texto esteja atrasado uns anos. O auge do hype de microsserviços já passou, mas talvez esse texto possa ser útil para alguém, ainda que um pouco defasado. Microsserviços foi adotado por algumas pessoas como sendo o ápice que a arquitetura de software chegaria, com vantagens visíveis a todos. A pergunta que fica, então, é: será que a utilização de microsserviços é tão benéfica assim? A resposta a essa pergunta é depende (que, aliás, é a melhor resposta para quase todas as perguntas relevantes na computação (quiçá na vida)).
Para responder isso, primeiro vamos voltar umas casas, e entender seu surgimento. O antônimo de microserviços é um monolito, que pode ser pensado como um único software, contendo todas as funcionalidades previstas. Por exemplo, pensando em uma empresa como o Spotify (deixando claro, aqui é só um exemplo hipotético. Eu não faço a menor ideia de como os serviços deles estão divididos), teríamos em um único serviço, gigante, todas as funções relacionadas a playlists, podcasts, anúncios, controle de usuários (se é premium ou não), etc.
Aqui surgem duas grandes motivações para o uso de microsserviços: a primeira, é que mais e mais desenvolvedores vão trabalhando em uma mesma base de código. O risco de começar a dar conflitos nos commits, ou um time ficar bloqueado por causa de outro aumenta. A outra motivação é: digamos que esse software rode em instâncias grandes em algum provedor de nuvem, mas que alguma das funcionalidades comece a apresentar degradação no desempenho ("tá gargalando"). Para solucionar isso, há duas opções: escalabilidade vertical (aumentar os recursos computacionais da máquina, seja CPU, RAM ou disco), ou escalabilidade horizontal (aumentar a quantidade de máquinas em execução). Em ambos os cenários, os custos aumentam. A grande questão, quando escala-se horizontalmente, é que se aumentou o número de máquinas (caras) por uma funcionalidade apenas. As demais funcionalidades rodavam muito bem na quantidade anterior. Daí surge a pergunta: e se removermos essa funcionalidade, deixando-a como um serviço próprio que possa ser executado em máquinas menores? O custo diminuiria, já que máquinas com menos recursos são, obviamente, mais baratas. Talvez rodar 5 instâncias menores para esse recurso seja mais eficiente do que escalar máquinas maiores.
Dessas motivações (há outras, claro, mas ao meu ver essas duas são as mais fortes), surge a ideia de quebrar esse monolito em serviços bem-definidos, com responsabilidades claras, que podem ser deployados separadamente. Já voltaremos a parte das responsabilidades, mas antes, uma volta aqui:
Ora, se os serviços são feitos separadamente, cada um deles pode ser feito na linguagem que eu quiser, correto? Sim, mas, como diz o Raffael Chess (para quem curte xadrez, recomendo seguir os canais dele, do Xadrez Brasil e do GM Rafael Leitão): poder posso, a questão é: será que devo? Discorro: 99% das empresas não é uma das FAANG (ou será que agora é MANGA, dado o nome novo do Facebook, Meta?), e a quantidade de desenvolvedores não é tão grande assim. Por exemplo, se eu tiver 10 serviços, escritos em 6 linguagens diferentes, como fica a mobilidade de um desses desenvolvedores? Se trocar de squad, indo para uma que utilize outra linguagem, haverá toda uma curva de aprendizagem da linguagem, caso o dev não conheça essa outra linguagem. Outro ponto: como fica a base de conhecimento desses times? Pois se dois times enfrentarem um problema semelhante, mas utilizando linguagens de paradigmas diferentes, a solução não necessariamente será a mesma. Claro que há cenários que usar mais de uma linguagem faça sentido. Por exemplo, se há cálculos matemáticos em algum dos serviços, talvez uma solução seja fazer esse serviço utilizando numPy (que utiliza C por baixo dos panos), enquanto os demais serviços, IO-intensive, são feitos em Java ou outra linguagem. O que eu tenho visto, e acho uma boa prática, é que alguém da empresa, geralmente um arquiteto de software, crie um chassi de serviços, a serem utilizados pelos desenvolvedores. Esse chassi já traz um log configurado, questões de observabilidade (que falarei mais a frente), versão padrão do framework e outros detalhes. Com isso, os times rodam sobre uma mesma base, e a troca de desenvolvedores e de informações é facilitada.
Dito isso, entremos para o que eu considero as questões fundamentais que, na minha opinião, você precisa considerar ao implementar microsserviços: quão micro cada serviço será, bem como sistemas distribuídos. Primeiro, pensemos aqui num exemplo hipotético e não muito complexo. Um vendedor de algum serviço qualquer (que possua mais uma opção), ele precisa fazer cotações com clientes em potenciais. Então aqui, até o momento, temos duas entidades, correto? Temos o serviço que ele presta (digamos, seguros, tem diversos planos, do mais básico ao mais completo), e temos o orçamento (aqui até um ponto bem interessante, qual é a nomenclatura utilizada pelo pessoal de negócio? É orçamento, é proposta? Esses termos são intercambiáveis? Essa definição é super importante para que tanto desenvolvedores quanto os especialistas no negócio possam estar na mesma página, e é um assunto central do DDD, a linguagem ubíqua - tem essa playlist do Elemar Jr muito boa, recomendo). Mas voltando ao tópico, essas duas entidades pertencem a um mesmo domínio/serviço? E no momento que a proposta é concretizada, e torna-se um contrato, ainda é a mesma entidade, mas com outro status (de pendente para concluído)? Ou virou uma terceira, contrato? E informações referentes ao pagamento, são um serviço apartado? Perceba que há várias perguntas aqui, e estamos falando de um exemplo simples.
E modelar corretamente esses serviços têm duas complicações: o quão micro os serviços serão, e quão complexa será a comunicação entre eles. Sobre o primeiro ponto, deve-se evitar que o serviço fique nano. Todavia, isso não significa que eles não possam ser reduzidos em escopo. Exemplifico: em uma companhia em que trabalhei, um dos serviços consumia uma fila de eventos, e gerava um PDF a partir de imagens salvas no S3, salvando o próprio PDF em outro diretório no S3. A primeira vista, ele poderia parecer um pouco pequeno, mas seu propósito era claro (a geração do PDF) e, por se tratar de um processo pesado (alguns PDF chegavam a pesar 50mb), mantê-lo como um serviço próprio fazia (e, para mim, continua fazendo) total sentido. Percebe então como decidir o domínio é uma tarefa complicada? (Esse é um dos motivos pelos quais há pessoas que defendem que todo novo projeto deve nascer como um monolito, pois ainda não há o entendimento correto do domínio. Após a maturação, pode-se separar o monolito em serviços menores, já com mais experiência. A questão é: uma vez em produção, será dado tempo para quebrar esse monolito, sabendo que novas demandas nunca param de surgir?)
Agora entramos num ponto que é crucial (e preparem-se para links, tentarei deixar a maior quantidade de links relevantes para vocês): a comunicação entre serviços. Nenhum serviço é uma ilha. Sempre há comunicação entre eles, ainda que nem sempre seja uma comunicação de todos para todos. Talvez seja algo do tipo "A conversa com B, que conversa com C, que não conversa com mais ninguém". Essa troca de mensagens pode se dar de duas formas: síncrona ou assincronamente. Quando a comunicação é síncrona, o serviço que envia a mensagem fica aguardando o retorno dela. Na comunicação assíncrona, o serviço envia a mensagem e sabe que, em algum momento do futuro, ela será processada, mas ele não fica aguardando a resposta. (Pequena nota: em inglês, a palavra usada é eventually, que é traduzida para o português como eventualmente. Porém, aqui temos o hábito de usar eventualmente como sendo "algo que possa acontecer". Porém, o sentido não é esse. Quando tu ler eventually saiba que se refere a algo que vai "por fim" ocorrer).
Vamos ver dois exemplos aqui, um de cada forma de fazer, seus prós e contras:
- Síncrono: O cliente, usando um navegador, faz uma requisição HTTP (síncrona). Chega no serviço A, que processa a requisição. Porém, ele depende de informações do serviço B. O serviço A pode, então, fazer uma nova requisição, seja por HTTP ou gRPC (que aceita modos síncrono ou assíncrono) para o serviço B. A principal vantagem desse modo, ao meu ver, é a propagação de erros. Digamos que o serviço B falhou. Ele irá retornar algum status de erro (pelo amor, não retornem 200 com o erro interno), seja 500 ou 422. Caso o serviço A esteja usando uma transação (aqui como se faz em Laravel, e aqui em Java (Spring)), ela pode facilmente desfazer as mudanças, e retornar um erro ao usuário que, poderá corrigir os erros ou entrar em contato com o suporte. Mas ele sabe, na hora, caso ocorra um erro. A principal desvantagem é que esse tipo de operação é bloqueante (o usuário vai ficar vendo um loading na tela enquanto o processamento não termina), e é lento. A latência da rede é ordens de grandeza mais lenta que processar tudo a partir da memória RAM, passando as instruções diretamente à CPU).
- Assíncrono: Nesse cenário, o cliente, usando um navegador, faz uma requisição HTTP (síncrona). O serviço A faz o seu processamento e faz uma requisição assíncrona ao serviço B, retorna um status (usualmente 202, accepted) . Usualmente, isso se dá através de uma fila de mensagens, que são executadas ordenadamente. O problema, nesse caso, é que a mensagem na fila pode demorar um tempo considerável até ser processada, ou, quando finalmente for processada, ocorrer um erro. Nesse caso, como fica o retorno para o cliente, que recebeu aquele status anterior? Ou, o pior cenário, é que uma transação feita em diversos microsserviços só é completada em alguns deles, dando erros em outros? Nesse caso, temos um estado inconsistente. Para contornar esses erros, existem algumas estratégias. Por exemplo, no caso da mensagem na fila não ser processável, podemos ter uma dead letter queue, uma fila separada onde essas mensagens "problemáticas" ficam para receber intervenções humanas. Em uma empresa em que trabalhei, utilizávamos o Rocket.chat como ferramenta de comunicação, e tínhamos um canal em que recebíamos mensagens quando uma mensagem entrava nessa fila, para que pudéssemos intervir e, se necessário fosse, modificar o payload da mensagem e reprocessar. Essa é uma estratégia que pode funcionar se o volume das mensagens falhas for suficientemente pequeno. Em relação a transação distribuída, pode-se aplicar o conceito de SAGA para coordenar essas transações. (Um ponto interessante aqui é que esses problemas de sistemas distribuídos são conhecidos há décadas, literalmente, e são abordados nos livros texto dessa disciplina, e mesmo assim, ainda são desafiadores).
Outro ponto que eu considero crucial, mas às vezes relegado, são logs e rastreamento. Por exemplo, em um fluxo que a mensagem passe por 3 ou 4 pedidos diferentes, e quebre em algum deles, como ter o controle do fluxo pelo qual a mensagem passou? Uma solução para isso é ter um distributed tracing, onde há um identificador único associado a request, sendo esse id propagado para todos os serviços envolvidos. Assim, tu consegue agrupar os logs pelos ids, mostrando todo o fluxo do processo. Isso, agregado a ferramentas de monitoramento como Datadog, permitem uma ótima visualização do que está acontecendo com seus serviços.
Talvez esse texto tenha dado a impressão de que eu sou contra microsserviços. Longe disso, acho uma ótima solução. Todavia, eu não acredito em balas de prata. Há casos em que monolitos são excelentes, como quando a empresa é pequena, ou não possuem experiência com sistemas distribuídos, ou o domínio do negócio ainda não está claro. Monolitos podem escalar sim (Shopify que o diga), desde que os desenvolvedores tenham bastante zelo pelo produto, e haja procedimentos bem definidos na empresa. Se optar por microsserviços, está tudo bem, desde que se atentem aos pontos que eu levantei aqui. Espero que esse post tenha sido útil ;)
Top comments (0)