Tech Writers

Microsserviços do “tamanho certo” – Parte I

8 minutos

Uma pergunta difícil de ser respondida quando trabalhamos com microsserviços é com relação ao “tamanho” adequado das aplicações que constituem o ecossistema.  

A princípio, apenas para citarmos alguns exemplos e evidenciar a importância do assunto, serviços de granularidade inadequada podem implicar em:  

  • Aumento do custo de manutenção e do índice de retrabalho das equipes; 
  • Prejuízo nos requisitos não funcionais como escalabilidade, elasticidade e disponibilidade; 
  • Agravamento do impacto de arquitetura descentralizada em termos de performance; 
  • Complexidade acidental no monitoramento e detecção de falhas da aplicação. 

Determinar a granularidade adequada é uma tarefa difícil. Provavelmente, não será na primeira tentativa que você obterá sucesso. 

Nesse artigo, trarei alguns insights de possíveis cenários que justificam a decomposição de uma aplicação em microsserviços menores para te ajudar nessa questão. Confira!

Granular ou modular microsserviços?

Para compreendermos melhor as justificativas que irão nos conduzir durante o processo de refinamento da aplicação, devemos esclarecer a diferença conceitual entre os termos “granularidade” e “modularidade”. 

Embora intimamente relacionados, tratam de aspectos arquiteturais distintos.  

Modularidade, em nosso contexto de discussão, trata da organização interna de uma aplicação em componentes separados de alta coesão e com baixo acoplamento. 

Idealmente, toda aplicação (principalmente monolíticas) deve ter essa preocupação, sendo desenvolvida em um formato modular flexível que facilite uma eventual decomposição. 

A ausência de modularidade conduzirá o projeto ao perigoso e quase certamente irreversível big ball of mud (ou ainda pior: big ball of distributed mud).

Figura 1- Exemplo de módulos coesos com dependência 

Granularidade, por outro lado, diz respeito ao tamanho dos nossos módulos e/ou serviços. Em uma arquitetura distribuída é muito mais comum termos problemas com granularidade do que com a modularidade.  

O ponto central dessa discussão é que uma arquitetura modular facilita muito a quebra de um serviço centralizado em microsserviços mais refinados, sendo quase um pré-requisito para uma decomposição arquitetural de menor esforço e com risco controlado. 

A velha advertência de refatorar um trecho de código problemático antes de alterar seu comportamento também pode, nas devidas proporções, nos servir de orientação. Assim, é uma escolha sábia reestruturar um serviço em um formato modular flexível antes de aplicar as diretrizes que abordaremos na sequência. 

Dica: Ferramentas simples como NetArchTest (.NET) e ArchUnit (Java) podem ser utilizadas pelo arquiteto para garantir a modularidade de uma aplicação, seguindo o conceito de fitness functions e arquiteturas evolucionárias!

Critérios de desintegração dos microsserviços

Afinal de contas, quais seriam os critérios que justificariam quebrar um serviço em aplicações menores?  

São eles: 

  • Escopo e funcionalidade; 
  • Código de alta volatilidade; 
  • Escalabilidade e throughput; 
  • Tolerância à falha; 
  • Segurança; 
  • Extensibilidade. 

A seguir, explicaremos melhor cada um desses tópicos:

Escopo e funcionalidade

Essa é a justificativa mais comum na quebra de granularidade de um serviço. Um microsserviço objetiva ter alta coesão. Desse modo, deve fazer uma única coisa e fazê-la muito bem. 

A natureza subjetiva desse critério pode induzir a decisões equivocadas de arquitetura. 

Como “responsabilidade única” acaba dependendo da avaliação e interpretação individual de cada um, é muito difícil afirmar com precisão quando essa recomendação é válida. 

Observe a imagem:

Figura 2 – Decomposição de serviço com boa coesão 

No exemplo acima, as funcionalidades estão intimamente relacionadas dentro de um mesmo contexto de negócio (notificação). Avaliando apenas do ponto de vista da coesão, é provável que não tenhamos uma boa justificativa para uma decomposição arquitetural.

Figura 3 – Serviço tratando de funcionalidades sem relação 

Agora, considere um único serviço que gerencia o perfil do usuário e é responsável por manipular uma sessão de comentários. Claramente, estamos falando de contextos de negócio distintos e seria mais fácil de aceitar uma decisão apoiada nessa justificativa. 

Para reforçar: este critério, por si só, muitas vezes não justifica a quebra de um serviço. Geralmente, ele é aplicado em conjunto com outros critérios, reforçando a tomada de decisão.  

2 – Código de alta volatilidade

A velocidade que o código fonte muda é uma ótima diretriz para fundamentar a decomposição de uma aplicação. 

Imagine um serviço de títulos financeiros, no qual o módulo de histórico tem novas implementações a cada semana, enquanto os módulos de títulos a pagar e receber são alterados a cada seis meses.

Figura 4 – Separando código de alta volatilidade 

Nessa situação, a decomposição arquitetural pode ser uma decisão sábia para reduzir o escopo de testes antes de cada liberação. 

Além disso, essa decisão também trará ganho de agilidade e manterá nosso risco de deploy controlado, garantindo que o serviço de títulos não seja mais afetado pelas frequentes mudanças na lógica do serviço de histórico. 

3 – Escalabilidade e throughput

Muito semelhante ao item anterior, o throughput de um serviço pode ser uma ótima justificativa para a quebra da aplicação. 

Diferentes níveis de demanda, em diferentes funcionalidades, podem exigir que o serviço tenha que escalar de formas distintas e independentes. 

Manter a aplicação centralizada pode impactar diretamente a capacidade e os custos da arquitetura em termos de escalabilidade e elasticidade. 

Dependendo do contexto de negócio, este critério, por si só, pode ser suficiente para justificar sua decisão.

 Figura 5 – Serviço com diferentes níveis de requisição em tpm (transactions per minute) 

4 – Tolerância à falha

O termo tolerância à falha descreve a capacidade de uma aplicação de continuar operando mesmo quando uma determinada parte desta aplicação deixa de funcionar. 

Vamos considerar o exemplo anterior. Imagine um cenário onde o serviço de histórico, por integrar com diversas aplicações de terceiros fora da nossa arquitetura, costuma falhar com certa frequência, chegando ao ponto de reiniciar todo o serviço de títulos financeiros e gerar indisponibilidade. 

Nesse caso, uma decisão compreensível seria: separar a rotina problemática em um serviço isolado. Para, assim, manter nossa aplicação funcional a despeito de eventuais falhas catastróficas no serviço de históricos.

Figura 6 – Separando uma rotina problemática para melhorar a tolerância à falha

5 – Segurança

Considere o exemplo ilustrado na figura abaixo. Nele, um serviço que trata das informações básicas de um usuário (endereço, telefone, nome etc.) precisa gerenciar dados sensíveis dos seus cartões de crédito. 

Essas informações podem ter requisitos distintos com relação ao acesso e a proteção dos mesmos. 

Quebrar o serviço, neste caso, pode auxiliar em: 

  • Restringir ainda mais o acesso ao código cujos critérios de segurança sejam mais rigorosos;
  • Evitar que o código menos restrito seja impactado com complexidade acidental de outros módulos. 

Figura 7 – Serviços com critérios de acesso e segurança distintos 

6 – Extensibilidade

Uma solução extensível tem a capacidade de ter novas funcionalidades adicionadas com facilidade conforme o contexto de negócio cresce. Essa habilidade também pode ser um forte motivador para segregar uma aplicação.  

Imagine que uma empresa tem um serviço centralizado para gerenciar formas de pagamento e deseja suportar novos métodos. 

Certamente, seria possível consolidar tudo isso em um único serviço. Porém, a cada nova inclusão, o escopo de testes se tornaria mais e mais complexo, aumentando o risco de liberação, e, com isso, o custo de novas modificações. 

Dessa forma, uma forma de mitigar esse problema, seria separar cada forma de pagamento em um serviço exclusivo. Assim, seria permitido que novos serviços pudessem subir e estender a funcionalidade atual sem impactar o código em produção ou aumentar o escopo de testes, mantendo, assim, o risco de novas implementações sob controle.

Figura 8 – Segregação de serviços para permitir extensibilidade 

Conclusão

Dificilmente um arquiteto irá acertar de primeira. Requisitos mudam. Conforme aprendemos com nossas ferramentas de telemetria e feedback, a decomposição arquitetural de uma aplicação acaba sendo um processo natural dentro de uma aplicação moderna e evolutiva. 

Felizmente, temos exemplos baseados na experimentação prática que nos servem de balizadores nessa empreitada: 

  • Garanta que seu serviço possui uma estrutura interna modular flexível; 
  • Avalie com calma os critérios que justifiquem uma decomposição arquitetural; 
  • Considere cada trade-off face às necessidades do seu contexto de negócio. 

Por fim, aos leitores que desejam saber mais sobre o tema, recomendo a leitura do livro que fundamentou este artigo: Software Architecture: The Hard Parts. Uma abordagem profunda e prática sobre este e muitos outros desafios enfrentados na arquitetura de software moderna de sistemas complexos. 

Posteriormente, na parte II deste artigo, discutiremos os critérios que podem levar um arquiteto a unificar serviços distintos em uma aplicação centralizada. Fique ligado!

Gostou de conhecer um pouco mais sobre Microsserviços? Me conte aqui nos comentários! 

Confira mais conteúdos como esse em nosso Blog!

Quer ser nosso próximo Tech Writer? Confira nossas vagas na página Carreira!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *