DEV Community

Cover image for Arquitetura de Microsserviços: um Guia para construir Sistemas Resilientes
Pedro Mafra
Pedro Mafra

Posted on • Updated on

Arquitetura de Microsserviços: um Guia para construir Sistemas Resilientes

Introdução

Talvez você já ouviu falar de microsserviços em algum lugar e ainda não sabe exatamente o que é, ou talvez queira apenas relembrar as principais características da arquitetura e implementação. Este artigo é para você!

Primeiro de tudo, o que são? Como já diz o próprio nome, é um serviço ‘micro’, mas vai muito além disso. Um microsserviço (MS) é uma aplicação como outra qualquer, em qualquer linguagem, porém possui um escopo e responsabilidade delineada, fazendo parte de um ecossistema maior. Quando não faz parte, é possível que seja apenas um monolito pequeno.
Mas e um monolito, o que é exatamente? 🤔

Para tudo ficar mais claro, podemos diferenciar um monolito de um microsserviço de forma simples:

Monolito:

  • Serviço que engloba todo um ecossistema, todos (ou quase todos) os domínios da aplicação
  • Atualizações podem impactar diferentes domínios ao mesmo tempo
  • Geralmente feito todo em uma mesma linguagem
  • Mais difícil de escalar e separar times

Microsserviço:

  • Serviço que representa um domínio específico da aplicação, e que faz parte de um ecossistema maior
  • São serviços independentes, logo possuem deploy independente, banco de dados independente, e apresentam menos riscos de impactar todo o sistema caso haja algum tipo de problema
  • Podem ser realizados cada um em uma tecnologia diferente, para obtenção de performance por exemplo
  • São mais facilmente divididos entre times

Image description

Tudo bem, mas então porque vou querer utilizar monolitos? Bom, como em nossa área sabemos que não existe bala de prata, podemos entender algumas situações já conhecidas em que trabalhar com um ou outro traz maiores vantagens. Então se liga nesses 2 próximos parágrafos:


Começando pelos monolitos, geralmente são muito vantajosos quando vamos iniciar uma nova ideia de um projeto. Nesses casos raramente temos de cara todo o conhecimento dos domínios e escopos, sem falar nas possíveis mudanças de mercado e de clientes, que impactam diretamente neste delineamento de responsabilidades. Por isso são também são muito vantajosos para POCs (provas de conceito).
Outro ponto é quando queremos uma governança simplificada: é muito mais simples se trabalhar com apenas uma tecnologia, contratar novos profissionais e introduzi-los ao projeto, principalmente devs mais iniciantes, que não conhecem muitas linguagens, comunicação assíncrona, etc.
Além disso, temos um shared kernel, ou seja, um compartilhamento claro de libs dentro do mesmo codebase. Usando MS geralmente vamos ter um repo separado para bibliotecas, porém manter a compatibilidade de versões será muito mais complicado.

Já engatando para os microsserviços, começam a ser vantajosos quando temos contextos e áreas de negócio bem definidas em nossa aplicação. Se queremos escalar/separar melhor times, trabalhar com alguma tech específica para obter performance, e temos maturidade nos processos de entrega (time de plataformas, templates para criação de novos repos do zero, maturidade técnica dos times, etc.) são bons indícios para começarmos a trabalhar com MS.
Além disso, se queremos escalar apenas uma parte de nosso sistema, nada impede que comecemos extraindo apenas esta parte que já está mais madura para funcionar como um serviço separado.


Podemos concluir aqui que nem um nem outro é o certo, depende da situação, se ligou?

Image description

Porém, também é importante salientar que o processo de migração de monolito para arquitetura de microsserviços não é algo simples, e temos que nos atentar em diversos pontos deste processo. Alguns deles listei abaixo e você pode usar como um checkbox caso precise:

  • Separar bem os domínios/contextos da aplicação - Domain Driven Design
  • Evitar excesso de granularidade - “nanosserviços”
  • Verificar dependências - um MS não pode depender de outro, um “monolito distribuído” é o pior dos casos
  • Planejar processo de migração de banco - para simplificar, podemos começar criando o MS utilizando um mesmo banco, e posteriormente migrar o banco - aqui não podemos ter medo extremo de duplicação de dados
  • Começar a pensar em comunicação assíncrona - arquitetura baseada em eventos (Event Driven Design)
  • Lembrar que com MS teremos consistência eventual dos dados
  • Precisamos de maturidade para trabalhar com CI/CD, testes,rate limiting, autenticação, etc.
  • Começar pelas beiradas é uma boa opção - “Strangler Pattern” - ir quebrando as partes periféricas do serviço principal em MS, até chegar nas partes principais

Ainda vamos expandir melhor alguns desses pontos nos próximos tópicos, então fica tranquilo. 😉

Características por Martin Fowler

Agora vamos mapear bem resumidamente as principais características de um Microsserviço de acordo com o seguinte artigo https://martinfowler.com/articles/microservices.html escrito por Martin Fowler:

Componentização via serviços

Microsserviços são serviços “out of process”, diferentemente de bibliotecas que são componentes “in memory”. Sendo assim, são separados do processo principal da aplicação que está rodando, e independentemente “deployaveis”.

Organização através de áreas de negócio

Em microsserviços, estamos pensando menos em divisões de funções da empresa, e mais nas divisões de áreas de negócio da empresa.

Produtos e não Projetos

Um projeto tem início, meio e fim. A ideia aqui é tratar o software como produto e ter um time “owner” que irá mantê-lo.

Smart endpoints e Dump Pipes

Os canais para comunicação com os MS não devem ter regras - devem sair de um jeito e chegar do mesmo jeito (o “pipe” deve ser “dumb”). Caso contrário, estaremos gerando um acoplamento de nossa aplicação.

Governança descentralizada

Eventualmente precisaremos de soluções diferentes para resolver certos problemas - os MS resolvem este problema de padronização. Uma vez que poderemos ter tecnologias diferentes, temos que ter uma comunicação que funcione bem (“Consumer Driven Contract”), sempre com um contrato muito claro e pré-definido.

Gerenciamento descentralizado de dados

Em MS, teremos vários bancos separados e autônomos - não garantiremos a consistência das informações 100% do tempo, ou seja, teremos duplicações e delays nas sincronizações.

Automação de infraestrutura

Em monolitos, temos uma esteira de CI, testes, segurança, deploy, etc., mas em MS, precisamos de vários. Sendo assim, vem a tona a necessidade de uma automação de infra, time de plataformas, criação de templates, para facilitar este processo. Além disso, se não temos uma automação, a falta de padronização entre os MS tornará as manutenções mais complicadas.

Desenhado para falhar

Desde o dia zero, precisamos pensar em Resiliência, que será o próximo tópico deste artigo.

Design Evolutivo

Precisamos criar aplicações independentes e possíveis de substituição. Se precisamos de mais de um MS para substituir uma feature, é sinal que existe uma dependência entre eles e talvez possam ser agrupados em apenas um.


Acho que de características estamos bem acertados né? Caso tenha alguma dúvida em relação a algum dos tópicos, fica a sugestão para fazer a leitura completa do artigo linkado acima.

Image description

Resiliência

Continuando, uma das questões cruciais quando falamos de microsserviços é a Resiliência. Mas o que é isso?
Bom, em nosso meio, pode ser entendida como um conjunto de estratégias adotadas intencionalmente para a adaptação de um sistema quando uma falha ocorre.

Em algum momento todo sistema irá falhar, e precisamos estar preparados a isso. Nessa vertente, é muito melhor que as estratégias sejam mais “consistentes” do que “perfeitas”.
Como exemplo, é melhor que respondamos a uma requisição em 500 ms sempre, não importando se recebemos 1 ou 1 milhão de requisições. Caso a resposta ultrapasse seu tempo, precisaríamos começar a barrar requisições. Em MS muitas vezes é pior um sistema lento que um fora do ar, pois essa lentidão inesperada pode causar um efeito dominó em todos os outros sistemas envolvidos.

De forma geral, podemos listar alguns mecanismos de resiliência mais conhecidos:

  • Health checks: observar os sinais vitais do sistema como garantia de sua saúde. Podemos realizar de forma ativa, a partir do próprio serviço, ou passiva, com a verificação a partir de um consumer por exemplo;
  • Rate limiting: limitar as requisições de um sistema para não afetar sua qualidade. Aqui, temos que nos basear no cliente estratégico que irá utilizar nosso sistema e em suas demandas;
  • Circuit Breaker: ter uma forma de impedir novas requisições de forma simples pode se demonstrar crucial para proteger o sistema;
  • Comunicação assíncrona: conseguimos evitar a perda de dados e falhas em cadeia caso algum sistema saia do ar, por exemplo implementando um padrão outbox com registros temporários. Exemplos de serviços de mensageria: Kafka, SQS, etc;
  • Retry: aqui podemos falar sobre backoff exponencial e com Jitter. Recomendo assistir o seguinte vídeo: https://www.youtube.com/watch?v=1MkPpKPyBps
  • Observabilidade: APM, tracing, métricas personalizadas, OpenTelemetry;
  • Autenticação: acho que é claro para todos a necessidade de autenticar as requisições de um sistema.

Inclusive, mesmo trabalhando-se com comunicação assíncrona, ainda temos que pensar nas possibilidades de nosso sistema de mensageria não funcionar. E aí? Neste caso como podemos garantir alguma resiliência?
Poderíamos pensar em trabalhar por exemplo com o Padrão Outbox, criando tabelas com registros temporários no banco. Mas, além disso, temos que nos preocupar também com garantias de entrega e recebimento, idempotência, políticas de fallback, e em documentar todos estes pontos de nosso sistema.

Acho que até aqui ficou bem claro que depender de implementações do zero a todo momento em que vamos criar um novo microsserviço acaba se tornando inviável e muito trabalhoso. Por isso, hoje em dia são muito utilizadas ferramentas como API Gateway e Service Mesh para abstrair essas implementações de resiliência e facilitarmos o processo de comunicação e entrega.

Porém, não é do dia para noite que conseguiremos cobrir todos esses pontos, é sempre um processo de amadurecimento - precisamos mapeá-los e ir atacando-os a um a partir de nossas prioridades.

Coreografia vs Orquestração

Basicamente existem duas formas de comunicação entre MS - coreografia e orquestração.
Em uma coreografia, as comunicações são mais descentralizadas, acontecendo de forma mais independente. Na orquestração, temos um “maestro”, um serviço que irá coordenar como a orquestra irá fluir.

Image description

Então vamos diferenciá-los:

Coreografia:

  • Comunicação decentralizada, baseada em eventos;
  • Serviços mais independentes entre si, mais facilmente substituíveis, tornando o sistema mais escalável.

Em contrapartida, apresentam maior complexidade de manutenção, monitoramento e solução de problemas.

Podemos listar alguns casos em que se é bom trabalhar com Coreografia:

  • Quando todo processo pode se basear no input inicial sem precisar de mais contexto (passos condicionais intermediários);
  • Quando temos um fluxo com uma direção clara e única.

Orquestração:

  • Comunicação mais centralizada, baseada em comandos;
  • Orquestrador dita a sequência de comunicações que os MS devem seguir, também definindo políticas de fallback;
  • Mais simples de monitorar e solucionar problemas, pois sabemos onde olhar quando algo da errado.

Em contrapartida, gera acoplamento entre os serviços, dificultando adicionar, remover ou substituí-los. A falha de um pode ser a falha de outros.

Podemos listar alguns casos em que se é bom trabalhar com Orquestração:

  • Quando temos passos condicionais que trigarão fluxos diferentes, como por exemplo confirmação de pagamento de cartão de crédito;
  • Quando precisamos centralizar o monitoramento do fluxo.

E novamente voltamos ao mesmo ditado: não existe bala de prata. Dependendo do caso, um ou outro pode ter seus benefícios, o importante aqui é implementar alguma estratégia de comunicação. Caso contrário, podemos gerar um Anti-Pattern conhecido, a "Estrela da Morte", quando as comunicações ficam tão descentralizadas e interdependentes que podemos perder o controle da comunicação da rede.

Image description

Em outras palavras, se sua empresa está implementando MS sem parar, e mais importante, sem um componente de mediação na arquitetura, é apenas uma questão de tempo até a "Estrela da Morte" aparecer pra você.

Sendo assim, já podemos pensar aqui em estratégias de mitigação - Resiliência - para evitar que isso aconteça. Por exemplo, a utilização de API Gateways como intermediários para comunicação entre os MS é uma maneira eficiente para conseguirmos manter um controle maior das chamadas, pois elas passarão a acontecer entre contextos, e MS de contextos diferentes não terão ideia de qual MS estará se comunicando do outro lado.
Inclusive, assim como já dito anteriormente, poderemos mais facilmente definir rate limiting, circuit breakers, autenticação, e outros mecanismos de resiliência diretamente em nosso API Gateway.

Image description

E, assim como em nossas aplicações, podemos seguir práticas de DDD e dividir nossos Gateways de acordo com os Bounded Contexts, aumentando ainda mais o controle sobre nosso sistema. Mas falaremos um pouco mais sobre isso já já.

Patterns

Categorizações finalizadas, agora podemos focar um pouco em alguns padrões comumente usados nos dias de hoje numa arquitetura de microsserviços.

API-Composition

Pense no caso: quero gerar um relatório, mas metade dos dados está em um microsserviço e metade em outro, o que fazer? Neste caso podemos utilizar criar um Service Composer para fazer chamadas a esses MS e realizar a composição e transformação dos dados de acordo com as regras de negócio.

Image description

Decompose By Business Capability

Supondo que queremos decompor um monolito em n microsserviços por x razões, como começar este processo? Bom, uma ideia inicial é buscar decompor nosso sistema por áreas de negócio (Bounded Contexts). Utilizar o DDD para visualizar nossa aplicação e separá-la em domínios, subdomínios, subcontextos, etc. Porém, ainda sim não é uma tarefa simples - existem muitas áreas cinzentas de intersecção.
Para facilitar o processo, podemos utilizar uma ferramenta do DDD: Context Mapping. Caso queria ler mais sobre, recomendo este link: https://www.infoq.com/articles/ddd-contextmapping/

Strangler Application

Ainda falando em decomposição, podemos nos utilizar do "Strangler Pattern" para começar a trabalhar com MS, basicamente seguindo duas regras:

  1. Toda nova feature será transformada em um MS
  2. Pegar pequenos pedaços do monolito e transformar em MS

Porém, esta quebra vai sendo feita aos poucos - o monolito continua existindo e vai reduzindo cada vez mais, até que se torne apenas mais um pedaço da aplicação. Além disso, temos que lembrar dos pontos de atenção mencionados no início deste artigo para realizar esta migração.

API-Gateway

Como já dito anteriormente, o API Gateway irá redirecionar as requisições aos serviços, funcionando como uma porta de entrada única e fornecendo soluções para rate limiting, transformações nas mensagens, autenticação, health checks, etc. Também podemos trabalhar com API Gateways divididos por áreas de neǵocio, melhorando ainda mais o controle de nosso sistema.

Image description

ACL - Anti-Corruption Layer

Podemos inclusive criar um novo serviço para servir como um “proxy” - por exemplo, criar uma interface de pagamentos que irá abstrair qual gateway de pagamento será chamado, de forma a não impactar diretamente o MS consumidor em caso de mudanças. Além disso, mais que um proxy, este ACL pode também encapsular regras de negócio, como por exemplo, fazendo a escolha de um gateway de pagamento de cartão de crédito de acordo com a bandeira escolhida.
O objetivo aqui é impedir a necessidade (ou intrusão) de um domínio conhecer os detalhes do outro.

Image description

BFF - Backend For Frontend

Temos que levar em consideração que, dependendo do cliente, precisaremos de retornos de API diferentes de acordo com cada demanda. Nessa linha, podemos implementar BFFs, que segregam nossos backends por tipo de cliente, retornando apenas as infos que aquele cliente em específico irá utilizar.

Image description

Podemos, alternativamente, utilizar GraphQL como uma maneira de substituir o uso de BFFs, garantindo ao client o poder de escolha dos dados a serem retornados.

Relatórios e Consolidação de Informações

Quando vamos trabalhar com microsserviços, muitas vezes queremos obter dados espalhados por diversos bancos para retornar em uma requisição, como por exemplo na geração de um relatório de extrato bancário. Nesta linha, é importante também falar sobre tabelas de projeção.

Exemplo: quero gerar o relatório X, obtendo nome, email e telefone contidos no MS 1, o saldo do MS 2, e empréstimos realizados do MS 3. Com isso, posso pensar na criação de uma tabela no banco de um outro MS 4 exatamente com esta estrutura:

Image description

Agora, posso fazer cada MS se comunicar e atualizar continuamente os registros dessa tabela quando sofrerem modificações:

  • Cada vez que o dado de um MS mudar, ele mesmo atualiza no seu banco e na tabela do banco do MS 4;
  • O próprio MS 4 escuta eventos de alteração gerados pelos outros MS em um message broker e então atualiza sua tabela.

Em ambos teremos consistência eventual, que como já dito é algo comum ao se trabalhar numa arquitetura de microsserviços.

Transactional Outbox

Assim como brevemente comentado anteriormente, podemos ver o "transactional outbox" como um padrão de resiliência. Neste pattern, persistimos temporariamente nossos dados em uma tabela outbox de forma a não perdê-los caso algum sistema saia do ar.

Exemplo: um MS 1 faz uma requisição http a um MS 2, ou mesmo posta eventos em um sistema de mensageria, como o RabbitMQ. Para evitarmos perder esses eventos caso haja algum problema em nosso MS 2 ou no message broker, guardamos os dados na tabela outbox. Após o sucesso do envio, o MS 1 deleta os dados dessa tabela.
Assim, de tempos em tempos o MS 1 ficará lendo a tabela e mandando as transações perdidas novamente para o seu destino. Além disso, é importante que estes dados não se misturem com a base principal e, para evitar isso, podemos por exemplo implementar um sdk interno que trabalhará com retries e guardará os dados na tabela em caso de falha.

Image description

É claro, porém, que para casos que necessitam de uma resposta imediata, não poderemos aplicar este padrão.

Secret Manager / Vault

Como fazemos para rotacionar diversas credenciais de diversos microsserviços e controlar tudo isso? Aqui podemos trabalhar com o Vault, que nada mais é do que uma solução da Hashicorp que armazena nossas credenciais e facilita este processo. Podemos por exemplo criar webhooks a serem chamados quando o Vault identifica as datas limites que setamos para nossas secrets.

Image description

Padronização de Logs

Em observabilidade, temos 3 vertentes básicas:

  • Logs
  • Métricas
  • Tracing

O log é o resultado de um evento. Se um erro estoura para nosso cliente, podemos ir procurando o log por nossas VMs. Porém, muitas vezes a VM do log pode até já ter sido destruída por conta de mecanismos de autoscaling por exemplo. Para evitar problemas e facilitar a observabilidade, podemos centralizar os logs por exemplo no ElasticSearch.
Além disso, temos que nos preocupar com a padronização: imagine diversos microsserviços, cada um com sua estrutura própria de logs, dificultaria muito fazer buscas né?. Nesse sentido, podemos criar um SDK que realizará uma padronização dos logs dos sistemas de nossa empresa por exemplo.

Image description

OTEL - Open Telemetry

Se recebemos um erro 500 em alguma requisição, que por sua vez envolve a comunicação de 3 microsserviços diferentes, como saber onde aconteceu? Aqui entra o conceito de tracing distribuído, que nos dá exatamente essa visibilidade.
Agora, vamos supor que fazemos o tracing utilizando o serviço do New Relic, mas depois queremos mudar para Datadog, depois para o Elastic - isso acaba gerando uma grande esforço "braçal". De forma a não ficarmos presos em nosso ‘vendor’, entra a ferramenta do OpenTelemetry, que disponibiliza um ‘collector’ para nossos serviços enviarem os dados, e consegue fazer a distribuição para o serviço especificado.

Image description

Com isso, temos maior segurança, descentralização e padronização dos dados de observabilidade que uma aplicação gera (logs, métricas e tracing).

Service Template

Por fim, mas não menos importante, o Service Template, algo que é muito utilizado e continuará sendo. Basicamente, é um modelo definido por nossa empresa com padrões de implementação para logs, outbox, passwords, comunicação com sistemas de mensageria, observabilidade, CQRS, múltiplos bancos, auditoria, jobs, etc. - em outras palavras, um “kit de desenvolvimento”.
Porém, para se ter algo assim, é muito imporante também um time de plataforma e sustenção para auxiliar na manutenção e aplicação destes padrões, ou seja, torna-se necessário uma certa maturação da empresa em si.

Em uma arquitetura de microsserviços, caso não tenhamos um service template, os devs irão demorar muito mais para criar novos serviços, pois sempre terão que configurar tudo do zero.


Bom, acredito que com todos estes tópicos em mente já consiga ter um bom ponto de partida para decidir os melhores padrões para seu sistema ou ao menos para ter uma visão geral do que estudar em seguida.

Obrigado pela leitura!

Image description

Top comments (1)

Collapse
 
diego_m_n profile image
Diego Mamede Nogueira

Ótimo artigo!!