O que é um micro servico ou microservice?

Eu já falei sobre esse assunto antes aqui no blog, em outros artigos, sendo que o principal deles e que recomendo que leia na sequência é o Introdução a Arquitetura de Micro Serviços. A ideia do artigo de hoje é explorar mais o que é um microservice e lhe ajudar no entendimento e no desenvolvimento estudando alguns princípios básicos.

O objetivo principal de usar micro serviços (ou microservices) é ajudar o time de engenharia a entregar produtos mais rápido, de maneira mais segura e com qualidade mais alta. Pequenos serviços desacoplados permitem que os times tenham ciclos de desenvolvimento (sprints ou iterações) rápidos e com mínimo impacto ao resto do sistema.

Um micro serviço é uma pequena porção de software que roda de maneira independente, focada em um único e pequeno (daí o nome micro) conjunto de atividades dentro de um conjunto de serviços muito maior, formando uma arquitetura de micro serviços. Resumidamente é uma versão mais enxuta de web services e web APIs que tradicionalmente os desenvolvedores já faziam há anos.

Em uma arquitetura de micro serviços, múltiplos serviços fracamente acoplados (ou seja, com poucas ou nenhuma dependência entre si) trabalham juntos. Cada serviço foca em um único propósito e tem uma alta coesão (congruência, coerência) de comportamentos e dados relacionados. Note que esta última definição inclui três princípios básicos do design de micro serviços, que vamos ver a seguir.

Os três princípios do design de micro serviços

Quando estamos trabalhando em sistemas que usam a arquitetura de micro serviços devemos sempre ter em mente três princípios básicos.

Propósito Único: cada serviço deve focar em um único propósito e fazê-lo bem. Esta é regra é muito semelhante com o Single Responsibility Principle do acrônimo SOLID, muito utilizado em orientação à objetos.

Assim como o SRP do SOLID diz que uma classe Aluno deve se focar apenas em atender às necessidades de um aluno, o princípio do Propósito Único diz que um micro serviço ‘/alunos’ deve se focar apenas em realizar operações sobre alunos. Precisa lidar com questões financeiras do aluno? Crie outro micro serviço para focar nisso. Precisa fazer uma matrícula, onde tem de lidar com o aluno e com questões financeiras? Crie um terceiro micro-serviço que vai chamar os dois anteriores.

Garantir que cada micro serviço tenha um propósito único não é tão fácil e simples, mas é uma das dicas essenciais principalmente quando você está refatorando uma aplicação monolítica em micro serviços.

Baixo Acoplamento:  cada serviço não deve saber nada ou muito pouco dos outros serviços. Eles não devem compartilhar bibliotecas de código, ou apenas o que for genérico e essencial para ter o mínimo de código repetido. Eles devem ter suas próprias validações, seus próprios testes unitários e inclusive não precisam compartilhar sequer a mesma tecnologia ou o mesmo mecanismo de persistência.

Isso tudo para que uma mudança em um serviço não exija mudar outros serviços. Ou que um bug inserido no desenvolvimento de um serviço não atinja o código de outros também. Inclusive a comunicação entre serviços deve ser feita através de interfaces públicas e protocolos leves, funcionando em processos separados.

Este também é um princípio herdado da Orientação à Objetos, onde trabalhamos Loose Coupling ou Low Coupling dentro dos design patterns como sendo um objetivo desejável para garantir boas arquiteturas, muito mencionado no clássico Padrões de Arquitetura de Aplicações Corporativas, do Martin Fowler.

Alta Coesão: cada serviço encapsula todos os comportamentos e dados relacionados juntos. Isso garante que se precisarmos construir uma nova feature sobre um mesmo domínio ou propósito, todas as mudanças devem ser localizadas em um único serviço.

Note que o próprio princípio do Propósito ou Responsabilidade Única dá os subsídios necessários para garantir uma alta coesão, outro tema bem forte nos design patterns da orientação à objetos.

No gráfico abaixo, extraído da Internet, temos a relação entre estes três princípios e como eles se interseccionam para gerar a base desejada para a construção de micro serviços que atendem ao padrão deste tipo de arquitetura.

Princípios de Micro Serviços
Princípios de Micro Serviços

Quando modelamos microservices nós temos que ser disciplinados em seguir ao menos estes três princípios básicos de design. É o único caminho para atingir o potencial pleno de uma arquitetura de microservices. Deixar qualquer um deles de lado fará com que o seu padrão se torne um antipattern ou anti-padrão, algo que vai te levar a resultados ruins, ao invés do que você espera.

Sem um propósito único, por exemplo, cada microservice pode terminar fazendo muitas coisas, crescendo em uma arquitetura de vários serviços monolíticos. Isso fará com que, além de não colher os benefícios completos de uma arquitetura de microservices, você terá um custo operacional alto para manter isso.

Sem um acoplamento baixo, mudanças em um serviço vão afetar outros serviços, lhe impedindo de liberar releases de maneira rápida, frequente e livre de bugs, que é justamente o benefício principal de uma arquitetura dessas. Mais importante ainda, problemas causados por um alto acoplamento podem ser desastrosos como inconsistências de dados ou mesmo perda deles.

E por fim, sem alta coesão, acabamos gerando uma aplicação monolítica distribuída, muito comum no passado e que há décadas sofremos com legados deste tipo onde, apesar de termos muitos serviços, só podemos aplicar novas features em produção se atualizarmos e implantarmos todos eles em produção, muitas vezes em uma ordem específica e ao mesmo tempo. Na minha opinião e de outros engenheiros com ainda mais experiência, isso é pior do que ter uma grande aplicação monolítica porque a complexidade e o custo operacional disso é muito alto.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

O que não é um micro serviço?

Muitas vezes fica mais fácil de entender o que algo é quando definimos o que ele não é, certo?

Um micro serviço não é um serviço que tem um número pequeno de linhas de código ou que faz ‘micro tarefas’. Esta confusão vem do próprio nome, mas não é uma regra e nem é o objetivo desta arquitetura. Os serviços devem possuir a complexidade e quantidade de linhas de código necessárias desde que atendam os três princípios anteriormente descritos.

Um micro serviço não é um serviço construído com a tecnologia mais bacana e hipster que o mercado oferece no momento. Embora a arquitetura de micro serviços te permita sim testar novas tecnologias de maneira muito rápida e fácil, não é um objetivo primário desta arquitetura. Está completamente ok usar qualquer tecnologia que atenda aos princípios anteriores e não faz sentido algum refatorar todos seus micro serviços cada vez que uma nova stack aparece no mercado.

Um micro serviço não tem de ser sempre programado do zero. Quando você está iniciando um processo de refatoração de uma aplicação monolítica, é muito comum que você extraia códigos da aplicação original para os micro serviços e não há problema algum nisso, desde que respeite os três princípios.

Então por hoje é isso, espero que tenha gostado e, caso queira saber mais sobre o assunto, recomendo meu curso de Node.js e MongoDB ou o meu livro sobre micro serviços abaixo.

Dicas para refatorar um monolito em micro serviços

Erroneamente creditei a fonte original em Inglês à Rising Stack, mas na verdade é da Nginx.

O processo de transformar uma aplicação monolítica em micro serviços é uma forma de modernização de aplicação, algo que os desenvolvedores já fazem há décadas. Como resultado, existem algumas ideias que você pode reutilizar quando estiver refatorando uma aplicação em microservices. Se você não sabe o que é um microservice, recomendo este artigo primeiro.

Uma estratégia é não fazer uma reescrita Big Bang, ou seja, esqueça a ideia de reescrever toda aplicação do zero, direcionando todos os esforços do time para isso. Embora muitas vezes isso soe interessante, é algo extremamente arriscado e geralmente resulta em problemas. Como Martin Fowler já disse algumas vezes, em uma tradução livre, a única garantia de um Big Bang é que algo vai explodir!

Ao invés de fazer uma reescrita Big Bang, você deve incrementalmente refatorar sua aplicação monolítica. Construa uma aplicação de microservices gradualmente, rodando cada novo microservice em conjunto do monolito original. Com o passar do tempo, a quantidade de funcionalidades implementadas em microservices versus a quantidade de funcionalidades remanescentes no monolito vai evidenciar que o mesmo encolheu ao ponto de que em algum momento ele irá sumir.

Martin Fowler refere-se à essa estratégia de modernização de Strangler Application (Aplicação Estranguladora). O nome vem da vinha estranguladora, uma espécie de cipó que cresce em árvores tropicais. Esta vinha vai se fixando na árvore original, consumindo seus recursos enquanto ela própria cresce mais rápido que sua hospedeira, não raro causando a morte da árvore original e deixando no lugar um monte de vinhas no formato de uma árvore.

Estratégia #1 – Pare de Cavar

A Lei dos Buracos (Law of Holes) diz que toda vez que você estiver em um buraco, você deve parar de cavar. Este é um ótimo conselho quando sua aplicação monolítica se tornar ingerenciável. Em outras palavras, você deve parar de tornar seu monolito maior. Se tiver de implementar uma nova funcionalidade você não deve adicionar este código ao monolito. Ao invés disso, a grande ideia com esta estratégia é colocar o novo código em um microservice próprio pra isso. O diagrama a seguir, do site Nginx, mostra a arquitetura do sistema depois de aplicar esta abordagem.

To start migrating from a monolith to a microservices architecture, implement new functionality as microservices; continue routing requests for legacy functionality to the monolith until there is a replacement microservice [Richardson microservices references architecture]

Repare no uso de um request router à frente de ambas soluções (monolito e microservice) que pode facilmente ser um API Gateway. Esta camada de software recebe as requisições originais e verifica se elas devem ser direcionadas para o legado ou para o microservice.

O outro componente que surge com esta arquitetura híbrida foi chamado de Glue Code no diagrama e nada mais é do que as dependências responsáveis por acesso a dados. Geralmente o serviço irá usar libs e componentes de acesso a dados do monolito original. Com isso em mente, esse glue code pode usar uma de três estratégias:

  • Invocar uma API remota fornecida pelo monolito;
  • Acessar a base de dados do monolito diretamente;
  • Manter sua própria cópia da parte da base de dados que lhe interessa, a qual deve estar sincronizar com a base do monolito;

Este glue code muitas vezes é chamado de camada anti-corrupção. Isto porque este glue code evita que o micro serviço seja poluído por conceitos do modelo de domínio legado. O termo camada de anti-corrupção foi introduzido pela primeira vez no livro Domain Driven Design do Eric Evans e depois foi refinando em um white paper. Desenvolver uma camada anti-corrupção não é algo exatamente trivial, mas é essencial se quiser sair do inferno do monolito.

Implementar novas funcionalidades como um micro serviço tem uma série de benefícios. Essa estratégia impede que seu monolito se torne ainda mais ingerenciável, ao mesmo tempo que permite que o serviço possa ser desenvolvido, implantado e escalado de maneira independente do monolito. A cada serviço novo que você cria, você experimenta um pouco mais dos benefícios desta arquitetura.

Entretanto, esta abordagem não endereça os problemas do monolito. Para consertar estes problemas você precisa quebrar o monolito, o que falaremos a seguir.

Curso Node.js e MongoDB
Curso Node.js e MongoDB

Estratégia #2 – Separe Frontend e Backend

Uma estratégia que ajuda a encolher a aplicação monolítica é separar a sua camada de apresentação da sua lógica de negócio e do acesso a dados também. Uma típica aplicação corporativa consiste de ao menos três diferentes tipos de componentes:

  • Presentation layer – Componentes que lidam com requisições HTTP e implementam APIs ou UIs. Em uma aplicação que tenha uma interface de usuário muito sofisticada terá uma quantidade de código de front-end substancialmente grande.
  • Business logic layer – Componentes que são o core da aplicação e implementam as regras de negócio.
  • Data‑access layer – Componentes que acessam outros componentes de infraestrutura como bancos de dados e message brokers.

Geralmente existe uma separação muito clara entre a lógica de apresentação em um lado e as regras de negócio e de dados em outro. Geralmente a sua camada de negócio vai ter uma API consistindo de uma ou mais fachadas/interfaces que por sua vez encapsulam os componentes de lógica de negócio. Esta API é o que permite fazer a separação do seu monolito em duas aplicações menores. Uma aplicação vai ser o front-end, a outra o back-end. Uma vez separados, o front-end irá fazer chamadas remotas ao back-end, sendo que o diagrama abaixo mostra como isso fica, antes e depois:

Refactor a monolith into two apps: one for presentation logic and another for business and data-access logic [Richardson microservices reference architecture]

Separar um monolito dessa maneira tem dois benefícios principais. Ele permite que você desenvolva, implante e escale duas aplicações de maneira independente. Principalmente quando se fala de front-end e rápidas iterações, testes A/B, etc. Outro benefício é que você combina com a estratégia #1 ao fornecer uma API do monolito para ser consumida pelos novos micro services que você desenvolver.

Esta estratégia, no entanto, é somente uma solução parcial. É bem comum que você troque um problemão gigante por dois problemas menores, mas que ainda são um problema a ser resolvido. Você terá de usar uma terceira estratégia para eliminar os monolitos restantes.

Estratégia #3 – Extrair Serviços

A terceira técnica de refatoração é transformar os módulos existentes dentro de um monolito em micro services standalone. Cada vez que você extrai um módulo e o transforma em um serviço, o monolito encolhe. Uma vez que você tenha convertido muitos módulos, o monolito irá parar de ser um problema. Ou ele irá sumir ou vai virar apenas um serviço por si só.

Priorizando quais módulos vão virar serviços

Uma aplicação monolítica grande geralmente terá dezenas ou centenas de módulos, todos candidatos para extração. Saber quais módulos converter primeiro pode ser desafiador. Uma boa abordagem é começar com alguns módulos que são fáceis de extrair. Isto vai lhe dar experiência com microservices em geral e com o processo de extração em particular. Depois que você extrair estes módulos mais fáceis, poderá partir para os mais importantes.

Converter um módulo em um microservice é algo que tipicamente requer tempo. Uma técnica de priorização é priorizar aqueles que mudam com frequência. Uma vez que você tenha convertido um módulo desses para um serviço, você pode desenvolver e implantar ele de maneira independente do monolito, o que vai acelerar o seu desenvolvimento.

Outra abordagem interessante é extrair os módulos que possuem requisitos significantemente diferentes do resto do monolito. Por exemplo, é útil pegar aquele módulo que usa uma base de dados in-memory e transformá-lo em um microservice, o qual poderá ser implantado em um servidor com muita memória. Da mesma forma, aquele módulo que consome muita CPU pode ser transformado em um microservice e feito deploy em um servidor com muito mais CPU do que RAM. Conforme você vai extraindo estes módulos específicos, você vai ver como se tornará mais fácil escalar os mesmos.

Como extrair um módulo

O primeiro passo ao extrair um módulo é definir a interface entre o módulo e o monolito. Geralmente será uma API bidirecional pois é comum o monolito precisar de dados do microservice e vice-versa. Se a sua lógica de negócio do monolito estiver com muitas associações entre suas classes talvez seja difícil de expor apenas o que importa para o microservice utilizar e não é raro que uma refatoração interna no monolito seja necessária para continuar avançando com essa estratégia de extração.

O diagrama abaixo mostra o passo a passo da extração de um módulo de um monolito para um microservice:

Extract a module/microservice from a monolith by defining a coarse-grained interface between the module and the monolith [Richardson microservices reference architecture]

Neste exemplo, o módulo Z é o candidato a ser extraído. Seus componentes são usados pelos módulos X e Y. O primeiro passo de refatoração é definir um par de APIs de alto nível. A primeira interface é de entrada e é usada pelo módulo X para invocar o Z. A segunda é uma interface de saída usada pelo módulo Z para invocar o Y.

O segundo passo da refatoração transforma o módulo em um serviço separado. Uma vez que você tenha extraído um módulo, é mais um serviço que você tem que lhe permite desenvolver, implantar e escalar mais facilmente do restante do monolito. Você poderá inclusive reescrever este serviço do zero mais tarde, uma vez que o módulo já foi isolado.

Cada vez que você extrair um novo serviço, estará um passo mais perto de ter uma aplicação 100% em microservices. Com o passar do tempo, seu monolito vai encolher e a virada de chave entre as arquiteturas vai se tornar algo natural e inevitável.

Até a próxima!

Este artigo é uma adaptação livre do original Refactoring a monolith into microservices, de Chris Richardson.

Curtiu o post? Então clica no banner abaixo e dá uma conferida no meu livro sobre microservices com Node.js!

Tutorial CRUD em Node.js com driver nativo do MongoDB – Parte 2

O tutorial de hoje é uma continuação de um outro tutorial aqui do blog, muito acessado aliás, onde ensino como fazer um sistema de cadastro bem simples em Node.js, usando o web framework ExpressJS e o banco de dados MongoDB. Ou seja, o famoso CRUD.

Nesta segunda parte, parto para um conceitos mais avançados em cima do mesmo projeto: paginação de resultados.

Para conseguir acompanhar todos os códigos, é importante que você tenha realizado a parte anterior ou pelo menos baixe os códigos-fonte que se encontram no formulário ao final do tutorial anterior.

Vamos lá!

Atenção, este mesmo tutorial está disponível em videoaula em meu curso de Node.js e MongoDB.

Paginação com Node.js e MongoDB

Nossa aplicação simplesmente lista todos os documentos da nossa coleção no MongoDB, sem qualquer distinção. Nas primeiras duas etapas deste tutorial vamos restringir isso, primeiramente através de uma paginação de resultados e depois através de uma busca.

Conforme trato em detalhes no artigo Tutorial MongoDB para iniciantes em NoSQL: Parte 2, fazemos paginação no MongoDB usando as funções skip e limit de maneira apropriada.

A função skip indica ao MongoDB que deve-se ignorar um número x de resultados da consulta na hora de retornar do servidor de banco. Já a função limit diz ao Mongo que ele deve retornar apenas um número limitado de documentos, independente se a busca retornaria mais elementos normalmente. Ambas funções devem ser usadas após um find, como veremos a seguir.

A lógica para criar paginação é bem simples: determine um tamanho de página, por exemplo 10 elementos, descubra o total de elementos que a consulta retornaria, por exemplo 21 elementos, e depois dividida o total pelo tamanho de página, arredondando sempre pra cima. Pronto, você tem a quantidade de páginas para aquela consulta! Neste exemplo de 21 elementos com uma paginação de tamanho 10, serão 3 páginas, sendo que as duas primeiras terão exatamente 10 elementos e a última apenas 1.

Entendeu?

Para determinar o tamanho de página ideal para seu sistema você tem de levar em consideração a performance da sua aplicação e a experiência do seu usuário. Muitas páginas com poucos elementos é fácil do banco retornar, mas ruim pro usuário ficar navegando. Poucas páginas com muitos elementos é o oposto e o segredo está no equilíbrio. Neste exemplo, ficaremos com tamanho 10 mesmo, para fins de teste.

Paginação de Resultados em Node.js

Então nosso primeiro passo será modificar uma function já existente no módulo db.js da nossa aplicação para que ela retorne os resultados de maneira paginada, como abaixo:

Note que comecei definindo uma constante com o tamanho das páginas sendo 10. Depois, adicionei um novo parâmetro na função findAll que espera a página que a aplicação deseja apresentar. Este parâmetro eu uso para calcular o skip, ou seja, quantos elementos da consulta eu devo ignorar. Se a página for a primeira, o skip será zero e serão mostrados os primeiros 10 elementos. Se a página for a segunda, o skip será 10 pela fórmula e serão mostrados os elementos das posições 11 a 20 (ordinal, ou 10 a 19 considerando um array zero-based).

Agora vamos modificar onde esta função findAll é chamada: na pasta routes, módulo index.js, que vamos modificar levemente a rota GET padrão apenas para adicionar o parâmetro página nela:

Note que coloquei o parâmetro no path como sendo opcional (?) e se ele não tiver sido passado na URL, será atribuído como um. Esse truque do || para atribuir um valor default eu ensinei no artigo 15 Dicas de JavaScript, lembra?

Certifique-se de que esta rota seja a última do arquivo index.js, logo antes do module.exports!

Como as rotas são testadas da primeira até a última para ver qual que processará a requisição deixaremos esta pro final para não interferir nas demais avaliações.

Agora execute a aplicação e teste a index passando a página na URL, como abaixo. Note que tive de adicionar muitos customers no banco de dados para podermos ter alguma paginação.

Paginação funcionando via URL
Paginação funcionando via URL

Mas obviamente não faremos nosso usuário ter de mudar parâmetros na URL para acessar as páginas, certo?

Está curtindo o post? Para uma formação ainda mais completa como programador web recomendo meu livro sobre programação web com Node.js clicando no banner abaixo!

Paginação de resultados com ExpressJs e EJS

Para que o usuário saiba quantas páginas existem e para que consiga acessar as mesmas, vamos ter de criar uma lógica para construir o HTML de paginação no frontend e uma lógica para retornar algumas informações importantes no backend.

Vamos começar pelo backend que é mais a minha praia. XD

Abra seu arquivo db.js novamente e vamos criar uma function countAll que executa o callback passado por parâmetro retornando um erro (opcional) e a quantidade de documentos na coleção customers. Note que atualizei o module.exports com a nova função e também exportando a constante com o tamanho de página.

Agora na routes/index.js, mais especificamente no nosso GET default (a última rota do arquivo), vamos ajustar as chamadas para construir o model com as informações que precisaremos no frontend EJS:

Note que tive de encadear as duas chamadas de banco uma dentro da outra, para garantir que a página somente será renderizada quando tivermos tanto os documentos da consulta quanto a quantidade total de documentos na coleção. Esse é justamente o ponto fraco do uso de callbacks, o chamado Callback Hell, mas falarei disso em outro artigo.

A cereja do bolo fica para o cálculo de quantidade de páginas que fizemos ali, dividindo o total de documentos pelo tamanho da página, usando a constante existente no módulo db.js. Ah sim, você não esqueceu de expor esta constante no module.exports, certo?

Agora que nosso model está completo, vamos mexer na index.ejs, nossa view inicial da aplicação para renderizar a quantidade total de elementos e o HTML de paginação para que o usuário possa navegar entre as páginas:

Esse código eu coloquei bem no final do arquivo EJS, onde antes ficava apenas o botão de Cadastrar novo cliente. Se você fez tudo certo até aqui, quando testar novamente você deve ver o seguinte resultado na interface da listagem:

Paginação funcionando
Paginação funcionando

Melhorando usabilidade da paginação

Para encerrar este tutorial, vamos adicionar uma perfumaria para melhorar ainda mais a usabilidade desta página: a página atual não deve ter link, apenas as demais páginas, assim, o usuário saberá em que página ele se encontra atualmente.

Vamos iniciar ajustando o model retornado no nosso index.js para informar também a página atual solicitada pelo usuário:

Note que a mudança foi bem sutil, apenas uma nova propriedade no JSON ‘pagina’.

Para fazer a lógica necessário para que o HTML ora tenha link, ora não tenha, basta adicionar um if na lógica de construção do nosso EJS usando a informação da página oriunda do model:

Ou seja, se a variável de iteração ‘i’ for diferente da página atual, escreve um link HTML na tela, caso contrário, escreve apenas o número da página. Visualmente falando temos a imagem abaixo como referência:

Indicando a página atual
Indicando a página atual

Note como ficou evidente que estamos na página 2, pois não tem link pra ela!

Outras melhorias poderiam incluir legendas quando se passa o mouse sobre as páginas, links de próxima página e página anterior, lógica para quando tivermos muitas páginas (e se tiver 100 páginas, como ficará a tela?) e até mesmo informação textual de que você está vendo os itens x a y da página z.

Mas…isso fica para você pensar meu amigo ou minha amiga! 🙂

Até o próximo post!

Gostou do tutorial? Quer aprender ainda mais sobre Node.js, ExpressJS, EJS e MongoDB comigo? Conheça meu curso de Nodejs e MongoDB clicando no banner abaixo!

Curso Node.js e MongoDB
Curso Node.js e MongoDB