Microserviços: dos grandes monólitos às pequenas rotas

"A regra de ouro: Você consegue alterar e publicar um serviço por si só sem alterar nada mais?" (Sam Newman)

Ultimamente estamos presenciando o nascimento de infinitas novas tecnologias, sejam elas em formas de frameworks (JavaScript na maioria das vezes), paradigmas, modelos, práticas ou até mesmo infraestrutura.

A mais nova palavra na boca dos engenheiros de software é o tão famigerado "Microserviço". Mas o que significa isso? O que é um monólito? Qual deles é melhor? Existe um melhor?


À moda antiga

"Roda o deploy aí. Agora é só esperar 3 horas para finalizar" (Qualquer desenvolvedor em algum momento da carreira)

Se você é um desenvolvedor há algum tempo e já trabalhou em alguma empresa relativamente grande, com certeza teve contato com os famosos "legados" ou então o grande monólito da aplicação.

Uma aplicação monolítica é aquele tipo de aplicação na qual toda a base de código está contida em um só lugar, ou seja, todas as funcionalidades estão definidas no mesmo bloco. Geralmente este bloco é dividido em 3 partes:

  • Apresentação: Essencialmente é a camada de visualização, que será mostrada para o usuário. No caso de uma aplicação web, esta camada contém as páginas HTML com JS e CSS que serão renderizadas no browser de quem as acessar
  • Negócio: É a camada que contém a lógica da aplicação. Nesta camada geralmente se encontram todas as bases de código, chamadas, API's e literalmente toda a inteligência do sistema em questão
  • Dados: Na cama de dados temos apenas as classes responsáveis pela conexão com o sistema de armazenamento de dados utilizado (geralmente um banco de dados relacional, como: Oracle, SQLServer e etc)

A arquitetura monolítica ainda é muito utilizada hoje e tem sentido para muitas aplicações. Desta forma existem empresas que não pensam em migrar este modelo para qualquer outro porque ele simplesmente resolve o problema da empresa no momento, mas, assim como qualquer coisa em desenvolvimento, temos o lado bom e o ruim.

Vantagens de uma aplicação monolítica:

  • Simplicidade da arquitetura: Não existem muitas camadas com o que se preocupar
  • Agregação de tecnologia: Toda a aplicação é desenvolvida em uma mesma tecnologia, facilitando a coesão da equipe
  • Fluxo de publicação simples: Alterou? Compilou? É só publicar
  • Rápido desenvolvimento: Por ser uma arquitetura mais simples, o seu desenvolvimento tende a ser muito mais rápido

Desvantagens de uma aplicação monolítica:

  • Único ponto de falha: Problema no sistema de newsletters? Não conseguimos fazer o pagamento dos funcionários porque o sistema de folha não funciona
  • Baixa escalabilidade: Temos que copiar TODA a stack para escalar horizontalmente
  • Base de código gigante: Quanto maior o sistema, maior é a base de código, já que está tudo no mesmo lugar
  • Agregação de tecnologia: Viu isso ali em cima né? Pois então, utilizar uma única tecnologia faz com que problemas que seriam resolvidos mais facilmente com outras tecnologias sejam ignorados (o clássico "Quando você tem um martelo, todos os problemas se tornam pregos")
  • Desperdício de esforço: Imagine que você tenha que mudar o texto de uma das telas de uma aplicação monolítica de 50.000 linhas, quando você for publicar isso, ela vai ter que ser totalmente recompilada, tudo isso por causa de um texto, será que valeu a pena?
  • Demora de aculturamento: Um novo funcionário vai demorar bastante para entender como tudo funciona

Pensando nesses problemas, algumas indústrias optaram por utilizar outros modelos monolíticos baseados em duas camadas, separando o banco de dados do processo principal e renderizando a apresentação e a lógica de negócio juntas, de forma que o DB funcionaria independentemente da aplicação em si funcionando através de um balanceador de carga.

Monólito de duas camadas

Após este movimento (que foi muito facilitado pelos provedores cloud como a AWS), outras empresas começaram também a separar a apresentação da camada de negócio (o que foi facilitado por frameworks como o Angular), transformando a visualização do site em um consumidor de uma API no back-end. Removendo a interdependência entre elas e facilitando um pouco o escalonamento e a escalabilidade de cada uma das partes.

Estes foram os primeiros passos em direção a uma arquitetura baseada em microserviços.

Uma nova arquitetura

"A simplicidade é a maior sofisticação" (Clare Boothe Luce)
Escalabilidade horizontal e vertical

Com o aumento da arquitetura por API’s e rotas cada vez mais genéricas, surgiu também a palavra do século XXI: Escalabilidade. A possibilidade de ter uma alta disponibilidade de algum serviço através da escala horizontal do mesmo.

Para deixar bastante claro o conceito, a escalabilidade vertical é quando temos que aumentar a capacidade da máquina que estamos utilizando, por exemplo, aumentar a memória RAM ou espaço no HD. Já a escalabilidade horizontal é quando replicamos a mesma máquina ou serviço N vezes, como mostra a imagem ao lado.

Após este boom, algumas empresas começaram a adotar uma prática pouco comum no ambiente de desenvolvimento: Quebrar a lógica de negócio em minúsculos pedaços independentes que se completavam, criando uma espécie de rede de API's interna totalmente (ou parcialmente) conectada.

Uma rede de minúsculos serviços interconectados

Este tipo de arquitetura trazia a vantagem de que era possível escalar qualquer tipo de serviço individualmente sem a necessidade de escalar o ambiente todo, como era feito anteriormente no modelo arquitetural monolítico.

Como esta prática não era difundida e não tinha uma definição exata, cada empresa trabalhava com ela da maneira que melhor lhes convinha. Eis que então a primeira denominação de "MicroServiço" apareceu em um artigo publicado pelo Martin Fowler em seu site, desde então temos a chamada arquitetura de microserviço.

O que é um microserviço?

Resumidamente, a arquitetura de microserviços é descrita por Fowler como:

"…Uma abordagem que desenvolve um aplicativo único como uma suite de pequenos serviços…" (Martin Fowler, 2014)
O que é um microserviço? (ThoughtWorks, 2015)

A ideia é muito simples, mas também é muito complicada de se desenvolver de forma sustentável. Em partes porque ao invés de ter que lidar com um único monstro, vamos dividi-lo em minúsculos "monstrinhos".

O deploy e a tecnologia de cada serviço pode variar (Fowler, 2014)

Em geral, um microserviço é um sistema simples (geralmente uma API) que se comunica através de mecanismos leves (como o HTTP). Estes pequenos sistemas devem ser totalmente autônomos, ou seja, devem possuir sistemas de deploy totalmente independentes e totalmente automáticos (estamos falando de um CI. Se você não sabe o que é, veja aqui).

É de suma importância ter o mínimo possível de gerenciamento centralizado destes sistemas, uma vez que estamos tentando remover pontos de falha, e não adicionar mais.

Uma característica importante dos microserviços é que eles podem, individualmente, serem desenvolvidos em uma linguagem de programação diferente, utilizando diferentes tecnologias de persistência de dados.

Aplicação monolítica usando único banco vs. Microserviços utilizando persistência própria (Fowler, 2014)

Smart endpoints, dumb pipes

Temos um roteador que faz café. Mas minha internet ainda cai (Anônimo)

Um ponto muito importante que é considerado uma das bases desta arquitetura é o chamado Smart endpoints & Dumb pipes (que, em uma tradução literal seria "Endpoints inteligentes e canos burros").

Modelo utilizado pela Casa do Código ilustra bem este conceito (Caelum, 2015)

Isto significa que toda a lógica deve estar presente no final do serviço, ou seja, na API em si, deixando que dados sejam enviados erroneamente para diversos locais, mas eles devem decidir o que fazer com eles. Isto vem da aplicação daquela definição de "mecanismos de transporte simples".

A regra geral é que as rotas em si não devem possuir nenhuma lógica de negócio e nem devem saber para quem ou para que serviço devem mandar o conteúdo da requisição, elas devem apenas se preocupar em trafegar este conteúdo entre as API's de forma rápida e confiável. Quem deve se preocupar para onde cada coisa deve ir são os próprios serviços.

Se formos transpor esta lógica para a prática, vamos ter basicamente o funcionamento de um roteador:

  1. Pacote é enviado pela rede
  2. Roteador recebe o pacote
  3. O pacote pertence ao meu endereço?
  4. Se sim, encaminhe para o endpoint
  5. Se não, ignore

Só que neste exemplo, os pacotes são as requisições com o conteúdo que os usuários enviam ou transições de estado entre serviços que se comunicam. Se um serviço recebeu um pacote que não é dele, deve apenas ignorar.


A arquitetura de um microserviço

Explicar o que é uma arquitetura baseada em microserviços é bem simples. Agora, explicar como é a arquitetura de um microserviço é um pouco mais complicado.

Sendo muito minimalista, um microserviço deve ter quatro coisas:

  1. Alta coesão
  2. Baixo Acoplamento
  3. Autonomia
  4. Independência

Novamente caímos no mito da "alta coesão e baixo acoplamento" que sempre ouvimos nossos professores de computação dizerem quando fazemos alguma matéria relacionada a Engenharia de Software. Basicamente, ser altamente coeso significa que o serviço deve possuir um, e somente um, propósito, ou seja, o serviço que cuida de usuários não pode cuidar de compras.

Arquitetura de um microserviço (Fonte: O bom Programador)

Ter um acoplamento mínimo é basicamente o efeito de ser altamente coeso (com algumas exceções). Um serviço que é pouco acoplado permite uma grande abstração e, por consequência, uma maior generalização das coisas. Em outras palavras, o serviço deve ser reutilizável e extensível de forma que seu código não seja "acoplado" a sua regra de negócio.

Autônomo, pois o mesmo deve ter total capacidade de gerenciar e executar suas próprias tarefas sem depender de outros serviços externos. Nos casos aonde um fluxo de execução deve ser seguido, então teremos serviços encadeados, ainda sim obedecendo ao modelo autônomo de execução.

E, não menos importante, temos a independência. Este é o ideal que todos os programadores querem chegar no código escrito. Um serviço (ou código) independente é aquele que funciona totalmente a parte de qualquer outro quesito externo (como outros serviços, dependências), para isso é importante que cada microserviço seja altamente coeso, de forma que qualquer deslize nesta regra vai afetar fortemente a premissa da independência.

Como sei se meu serviço é independente?

Responda a pergunta que está no subtítulo deste artigo:

"Eu consigo fazer alterações e publicar este serviço sem precisar alterar nada mais?"

Além disso, é importante também perceber que esta pergunta pode ser estendida para um conceito ainda mais abrangente, pare e pergunte a si mesmo: "Se eu desativar este serviço, todos os demais serviços continuarão funcionando?"

Claro, uma aplicação não é e nunca vai ser totalmente independente como um todo. No entanto, é possível alcançar a independência modular, por exemplo, esta pergunta não seria respondida se você tivesse um sistema de usuários que fosse o responsável por realizar o login e atribuir um token ao mesmo, você precisa de um usuário logado para fazer as ações em seu sistema. Porém se o token existir (o usuário já está logado), ele não sentiria nada se o serviço de login ficasse fora do ar. Isto é independência.

O labirinto da comunicação

Ter uma quantidade grande de microserviços já trás algo importante à cabeça: Como vamos comunicar um com o outro? E o meu front-end? Vai ter que chamar um serviço por vez?

Chamada direta do serviço

Com um sistema levemente complexo, chamadas da interface para API's internas podem ser frequentes. Empresas grandes como a Amazon (que tem sua arquitetura baseada em web services) dizem que, em algumas de suas interfaces, chegam a consumir mais de cem serviços, o que, mesmo com uma internet de boa qualidade, transformaria a experiência de qualquer usuário em um inferno.

O modelo mais antigo e mais simples é comunicar com cada serviço utilizando o próprio endpoint do mesmo, como ilustra a figura anterior. Mas imagine se você precisa realizar mil requisições para serviços diversos, fica complicado não?

O modelo acima ainda é muito usado inicialmente, porém o padrão mais utilizado é o chamado back-ends for front-ends, que consiste na aplicação de um agregador de requisições, ou API gateway, para balancear as cargas e distribuir para os serviços corretos. De forma que é possível definir o grau de especificidade que a API vai possuir, a chamada granularidade.

Para usuários utilizando redes móveis, o gateway agrega diversas chamadas de API's em uma única requisição, provendo uma menor granularidade. Já para usuários de desktops o gateway age como um proxy, simplesmente redirecionando as requisições para uma rede interna de velocidade muito mais elevada. Desta forma a comunicação entre os serviços e seus consumidores é feita em partes por uma intranet e em partes por uma internet, assim é possível aproveitar o máximo de ambas as redes.

Múltiplas faces, múltiplas equipes

Fonte: O bom programador

Por sua característica independente e autônoma, um microserviço é totalmente agnóstico de linguagem ou de tecnologias. Isso significa que, em um mesmo sistema, você pode ter serviços rodando PHP, Go, Python, C, Javascript e outras centenas de linguagens totalmente independentes e se comunicando através de um canal comum, o protocolo HTTP universal.

Por esta característica, criamos uma facilidade de obter times multidisciplinares, capazes de se organizarem não por equipes, mas sim por competências de negócio em times fechados com todo o necessário para operar o maquinário que faz um determinado serviço rodar.

Este fenômeno foi descrito por Melvyn Conway em 1967 no que é chamado de "lei de Conway". Que é basicamente a seguinte:

"Qualquer organização que desenha um sistema (amplamente definido) vai produzir um design no qual a estrutura é uma cópia de sua própria estrutura de comunicação." (Melvyn Conway)

Isto nos dá a base que precisamos para afirmar que times que produzem aplicações monolíticas tendem a possuir um sistema de comunicação mais lento e mais complexo, com muitas competências organizadas em uma estrutura de torre, chamado de modelo silo.

Times estacados e divididos por competências (Fowler, 2014)

Assim, nesta arquitetura, não teremos mais times estacados, no modelo silo, conforme a imagem ao acima, mas sim um modelo distribuído com times totalmente funcionais, contendo todos os especialistas necessários para que o serviço que este time em questão cuide tenha total capacidade de se manter e evoluir sozinho, completamente separado dos demais, conforme a imagem abaixo.

Times fechados com todo o necessário para operar seu serviço (Fowler, 2014)

Colocando na balança

Assim, podemos descrever uma série de vantagens e desvantagens dos microserviços.

Vantagens:

  • Arquitetura individual simples
  • Mecanismo de comunicação universal e leve
  • Sistemas totalmente independentes
  • Serviços coesos e desacoplados
  • Facilidade de deploy e testes unitários
  • Não se prende a uma tecnologia específica
  • Times multidisciplinares completos
  • Ausência de um ponto de falha único

Desvantagens:

  • Se não for bem planejado e bem executado, pode se transformar em uma grande bagunça
  • A arquitetura geral pode se tornar complexa se não for bem documentada
  • A ausência de uma documentação que descreva o escopo global pode onerar a visualização geral do sistema
  • Em um início de projeto, os problemas resolvidos por esta arquitetura são inexistentes. Portanto um dos grandes desafios é saber em qual parte do ciclo do projeto ela deve ser implementada, isto demanda mais tempo de desenvolvimento
  • O desenvolvimento de recursos novos que abrangem mais de um serviço devem ser cuidadosamente orquestrado em todas as equipes

Como eu começo?

Existe uma grande gama de modos de se criar microserviços. Por serem aplicações pequenas e bastante contidas, muitas das melhores técnicas estão no uso de containers (como Docker) e gerenciadores distribuídos. Vou ser direto ao ponto.

Podemos definir três categorias, infraestrutura, desenvolvimento e monitoramento:

Infraestrutura

  • Kubernetes: Sistema de orquestração de containers Docker. Ideal para a orquestração de microserviços pois é baseado totalmente em API's e ele próprio é dividido em diversos hosts (se tornando um microserviço em si), muito fácil de instalar e utilizar. Agnóstico de infraestrutura, pode ser executado em diversos provedores
  • AWS ECS: Serviço de containers hospedado pela AWS (muito similar ao kubernetes, em sua própria forma)
  • Apache Mesos + Marathon + ZooKeeper: Abstrai as configurações de diversos computadores e transforma em um único "super computador"

E mais uma sequencia infinita de combinações.

Desenvolvimento

Aqui vou citar alguns frameworks em linguagens diversas que são muito bons para esta arquitetura:

Monitoramento

Até agora testei diversas ferramentas de monitoramento, mas nenhuma superou o Datadog ou o Trace para obtenção de métricas.

Se você quiser ver mais frameworks ou outras informações de links muito interessantes sobre esta arquitetura, veja esta lista.

Conclusão

Note que, isto não é um guia de escolha de tecnologia, mas sim um descritivo de ambos os lados da moeda, de forma que é totalmente possível que sistemas grandes e complexos façam mais sentido na arquitetura monolítica do que na apresentada por mim.

Não existem balas de prata na tecnologia, nenhuma delas vai resolver todos os seus problemas. Pese suas necessidades e os benefícios e malefícios de cada escolha, isto fará com que sua aplicação seja bem desenvolvida.

Até mais!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.