Como criar microsserviços: projetando

No artigo anterior, apresentamos os aspectos fundamentais sobre microsserviços e sua arquitetura. Neste post, vamos aprofundar nas questões referentes a como criar microsserviços, desenvolvendo aplicações, com foco no projeto dos microsserviços.

Os critérios para a criação de microsserviços são flexíveis e devem ser adaptados para atender às necessidades do negócio e da evolução da aplicação propriamente dita.

Então, como podemos começar a criar um microsserviço?

Já sabemos (veja post anterior ) que o tamanho do código e o tamanho da equipe não são boas métricas para definir um microsserviço. Devemos basear a construção de cada microsserviço em termos das funcionalidades de negócio ou das funcionalidades técnicas necessárias para a aplicação, separando-as em duas categorias. No primeiro caso, os microsserviços implementam funcionalidades que refletem diretamente os objetivos do negócio. No segundo caso, os microsserviços implementam funcionalidades técnicas que podem ser compartilhadas com outros microsserviços ou utilizadas por outras aplicações como, por exemplo, interação com sistemas externos.

Vamos analisar inicialmente os microsserviços que implementam funcionalidades do negócio. Como começar a projetar tal microsserviço? Cabe lembrar que as propriedades fundamentais de alta coesão, baixo acoplamento, autonomia e independência devem ser respeitadas. Assim, o principal desafio está em encontrar os limites certos para cada microsserviço. Não podem ser limites muito estreitos, para não incorrer em alto acoplamento e, consequentemente, ser difícil realizar a implantação de forma independentemente. Também não podem ser limites muito amplos para que o microsserviço não assuma muitas responsabilidades e seja pouco coeso.

Além disso, deve-se pensar em como a aplicação na sua totalidade poderá evoluir ao longo do tempo, para identificar se a estruturação dos microsserviços será adequada durante todo o tempo de vida da aplicação.

O ponto de partida certamente é entender muito bem o negócio e seus casos de uso, para que se possa projetar uma solução adequada. É fundamental identificar as entidades e as funcionalidades do negócio para definir quais serviços a aplicação deverá prover e como esses serviços deverão se relacionar. Ao final dessa etapa, provavelmente os limites dos microsserviços já estarão definidos e poderemos ir um pouco além, verificando se esse projeto poderá evoluir juntamente com a aplicação.

Como identificar quais funcionalidades irão compor um microsserviço?

Aqui vamos considerar as duas categorias que citamos anteriormente: 1) microsserviços que atendem diretamente os objetivos do negócio e 2) microsserviços que refletem funcionalidades técnicas.

No primeiro caso, o escopo dos microsserviços diretamente ligados aos objetivos do negócio pode ser identificado usando três abordagens diferentes, como descreveremos a seguir. A primeira forma, funcionalidades do negócio, é a mais referenciada na literatura, e as outras duas, “casos de uso” e “volatilidade” são propostas por Morgan Bruce e Paulo A. Pereira, no livro “Microservices In Action”. Vamos entender como funciona cada uma dessas abordagens:

  • Funcionalidades do negócio: Nessa abordagem, o escopo do microsserviço está diretamente ligado aos objetivos do negócio. É fácil observar que essa forma de projetar microsserviços tem uma forte correlação com a abordagem de projeto orientado a domínio (Domain-Driven Design – DDD). Assim, podemos usar a noção de Bounded Contexts (Contextos Delimitados) como ponto de partida para definir o escopo dos microsserviços. Aqui vale ressaltar que em DDD, temos múltiplos contextos, cada um deles altamente coeso, com escopo bem definido, com uma visão única do mundo real que representa e com um limite externo explícito. Essas características atendem muito bem às propriedades desejáveis de microsserviços! Então, nesse enfoque, começamos definindo os contextos do domínio da aplicação, suas funcionalidades e as entidades que estão envolvidas nesse contexto. Dependendo do contexto, podemos mapeá-lo diretamente para um microsserviço. Caso ele seja muito complexo, podemos dividir o contexto em vários microsserviços independentes que colaboram entre si. Desta forma, a propriedade de alta coesão dos microsserviços estará garantida por construção.

Embora a descrição desse enfoque seja bastante simples, é importante ressaltar alguns aspectos:

  1. É necessário um bom conhecimento do negócio para que se possa identificar corretamente os contextos, seus relacionamentos e as informações de que necessitam. Fazer uma escolha equivocada com relação aos contextos implicará em um projeto não adequado dos microsserviços, comprometendo, por exemplo, o desempenho da aplicação. Caso isso venha a ocorrer, a correção dos problemas acarretados por equívocos desse tipo pode exigir a posterior refatoração dos microsserviços, ocasionando operações de alto custo como migração de dados e redefinição dos relacionamentos entre os microsserviços.
  2. Para manter os microsserviços independentes e altamente coesos, é necessário o uso de APIs para expor apenas as funcionalidades/métodos que façam sentido do ponto de vista funcional, mantendo os detalhes de implementação isolados dentro do próprio microsserviço.

Antes de finalizar esse tópico, vale um alerta:  o uso de Bounded Contexts como ponto de partida para definir o limite funcional dos microsserviços é uma abordagem extremamente válida, especialmente porque a propriedade de alta coesão, que é baseada no Princípio de Responsabilidade Única, tende a ser comprometida se a funcionalidade de um microsserviço ultrapassar as fronteiras de um contexto de negócio (embora existam circunstâncias específicas em que isso pode ser admitido). Entretanto, Bounded Contexts não são necessariamente equivalentes à funcionalidade de um microsserviço, como apresentado no artigo “Bounded Contexts não são Microsserviços”. Ressaltando novamente, podemos (e devemos) usá-los apenas como um ponto de partida para definir o escopo dos microsserviços.

  • Casos de uso: Uma outra forma de definir o escopo dos microsserviços é identificando os casos de uso da aplicação. Nessa abordagem, cada microsserviço será responsável pelas funcionalidades relacionadas a cada caso de uso da aplicação. Como citado por Bruce e Pereira isso é apropriado quando uma determinada funcionalidade não pode ser atribuída a um único domínio da aplicação ou ela interage com vários domínios. Outra situação apropriada para essa escolha é quando o caso de uso é complexo, e dividir sua implementação em vários microsserviços pode violar o Princípio da Responsabilidade Única, comprometendo a propriedade de alta coesão.
  • Volatilidade: As abordagens anteriores analisam apenas as funcionalidades atuais da aplicação, mas não consideram como a aplicação irá evoluir. A abordagem por volatilidade propõe considerar quais áreas da aplicação têm mais chance de mudar e separá-las para que não impactem as outras áreas à medida que a evolução da aplicação aconteça.

Os microsserviços que implementam funcionalidades técnicas, por sua vez, também atendem o negócio, mas de forma indireta, uma vez que são chamados pelos microsserviços de negócios. Exemplos desse tipo de microsserviços são:

  • Integração com sistemas de terceiros;
  • Aspectos técnicos ubíquos, ou seja, funcionalidades que dependem, afetam ou oferecem serviços para muitas outras partes da aplicação e possivelmente não adicionam valor ao negócio ou não estão diretamente associadas a um requisito da aplicação propriamente dita. Alguns exemplos são: logging, envio de e-mail/notificação e tratamento de exceções.

Esse tipo de microsserviço é usado normalmente para dar suporte ou simplificar outros microsserviços, especialmente os de negócio, que podem então se concentrar em suas reais responsabilidades sem precisar aumentar sua complexidade com questões técnicas. Podemos usar os microsserviços de funcionalidades técnicas quando (1) elas são utilizadas por vários microsserviços ou (2) quando elas podem mudar independentemente das funcionalidades do negócio ou mesmo (3) quando queremos reduzir a complexidade de um microsserviço de negócio.

É claro que essa otimização também tem um custo. Portanto, é necessário definir muito bem o escopo dos microsserviços de funcionalidade técnicas para sejam autônomos e independentes e para que não provoquem forte acoplamento dos microsserviços de negócio.

Evolução, Hierarquia e Colaboração entre Microsserviços

Uma vez definido os microsserviços em si, podemos pensar em sua relação com a evolução da aplicação. O que fazer quando uma nova funcionalidade surgir? Deve ser criado um novo microsserviço ou essa nova funcionalidade deve ser adicionada a um microsserviço existente?

Neste ponto, cabe também falar sobre a hierarquia dos microsserviços. Consideramos aqui duas categorias: agregadores e coordenadores. Microsserviços agregadores são aqueles que interagem com vários outros serviços para gerar um resultado. Já os coordenadores são aqueles que utilizam os serviços de nível mais baixo nível para oferecer serviços de maior nível de abstração.

Além da evolução e hierarquia, outro aspecto importante no projeto de microsserviços é a colaboração entre eles para que possam efetivamente realizar uma tarefa completa dentro da aplicação, atingindo um objetivo do negócio. Essa colaboração pode ser feita através de uma comunicação ponto-a-ponto (ou usando um service mesh, veja mais adiante), de maneira síncrona ou assíncrona. Tipicamente, a comunicação assíncrona é baseada em eventos (event-driven).

A comunicação síncrona, normalmente a primeira abordagem quando se pensa em comunicação, estabelece um modelo sequencial de execução, em que, após uma chamada a um serviço, o chamador fica bloqueado à espera do resultado antes de poder prosseguir. O principal impacto desse modelo é gerar forte acoplamento entre os microsserviços.

A comunicação assíncrona é mais flexível porque não bloqueia a execução após a chamada a um serviço, o que gera menos acoplamento entre os microsserviços. A comunicação ocorre através de eventos e, para isso, normalmente é necessário um componente independente do sistema, uma fila de mensagens (ex: Kafka ou RabbitMQ), que será responsável por receber e distribuir os eventos.

Uma boa prática é compor as duas formas de comunicação dentro da aplicação. Certamente existem funcionalidades que requerem comunicação síncrona, porque dependem do encadeamento de resultados de outras funcionalidades, mas também existem situações em que se pode seguir adiante, enquanto os resultados esperados não ficam prontos.

Uma das principais características de aplicações baseadas em microsserviços é o conceito de terminais inteligentes (Smart Endpoints) e canais de comunicação simples (Dumb Pipes), que coloca a inteligência dentro dos microsserviços e deixa os mecanismos de comunicação apenas com a função de transporte da informação. Entretanto, a comunicação não é uma camada independente nessa arquitetura, os próprios microsserviços são responsáveis por construir e enviar as mensagens. Além disso, como os microsserviços também são independentes, eles ficam responsáveis por coreografar (às vezes orquestrar) essa colaboração/comunicação. Essa é uma das principais diferenças entre a arquitetura de microsserviços e a antiga arquitetura SOA (Service Oriented Architecture), onde a orquestração de serviços normalmente é realizada por componentes externos aos próprios serviços.

Dado que a colaboração entre microsserviços é parte fundamental de uma aplicação baseada nessa abordagem, um novo componente de infraestrutura responsável por realizar e controlar a comunicação entre eles tem se estabelecido como padrão dentro da arquitetura de microsserviços: o Service Mesh. Tipicamente, o service mesh implementa uma camada distribuída para entrega de mensagens, utilizando um protocolo de comunicação leve e eficiente, e fatora algumas funções técnicas que todo microsserviço deve implementar, como mecanismos de resiliência, autenticação e confiabilidade, eliminando a necessidade de replicar essa funcionalidade em cada microsserviço. Nosso blog tem vários artigos falando mais sobre o conceito de service mesh e sua implementação, como aqui, aqui, aqui, aqui e aqui. Entretanto, a comunicação entre microsserviços proporcionada pelo service mesh é sempre síncrona, para o uso de comunicação assíncrona é necessário utilizar uma fila de mensagens.

Dado que, para a realização de operações mais complexas, muitas vezes é necessário que os microsserviços colaborem entre si, eles não estão isolados e fazem parte de uma rede: alguns microsserviços são “dependentes” de outros (downstream collaboration) e existem outros que “dependem” deles (upstream collaboration) para executar uma tarefa completa da aplicação. É importante ressaltar que, apesar dessa interdependência, cada microsserviço deve continuar sendo autônomo para realizar suas funcionalidades, respeitando o critério de responsabilidade única!

Coreografia e Orquestração de Microsserviços

A coreografia é uma técnica para compor microsserviços de forma distribuída e descentralizada, sob uma perspectiva global da aplicação, onde cada microsserviço sabe o que deve fazer e como colaborar com os demais. Não há a necessidade de um coordenador, pois a coreografia já foi estruturada e definida durante a concepção de cada microsserviço e é apenas realizada em tempo de execução.

Já no caso da orquestração, é necessário existir um coordenador (normalmente, também um microsserviço) que será responsável por invocar o microsserviço certo no momento certo, fornecendo as informações necessárias e aguardando suas respostas. E, nesse caso, esse coordenador fica totalmente dependente desses microsserviços que ele invoca, podendo chegar ao ponto de limitar-se apenas à tarefa de realizar requisições e obter sua resposta, sem implementar nenhuma outra funcionalidade específica, técnica ou de negócios.

Em uma aplicação coreografada, cada microsserviço possui suas responsabilidades e as executa em reação a algum evento. Não há necessidade de um microsserviço comandar a execução dos outros. Aplicações que usam essa solução criam microsserviços altamente desacoplados, permitindo a implantação independente desses microsserviços. Entretanto, todo esse benefício tem um custo, exigindo acrescentar um novo elemento de infraestrutura: a fila de mensagem. Adiciona-se, então, mais um componente a ser gerenciado e que pode ser também mais um ponto de falha na aplicação como um todo.

A orquestração, por sua vez, pode aumentar o acoplamento entre os microsserviços, estabelecendo uma relação de dependência entre eles que aumenta a complexidade de sua implantação. Além disso, o orquestrador pode acabar assumindo mais responsabilidades de negócio e deixar os orquestrados apenas com atividades de menor importância, podendo aumentar o risco de construir microsserviços sem autonomia.

Normalmente quando a definição de escopo dos microsserviços é feita por casos de uso, surge a orquestração. Então, principalmente ao usar essa abordagem, o ideal é encontrar um equilíbrio entre orquestração e coreografia.

API Gateway e Backend For Frontend (BFF)

Além da comunicação entre os microsserviços, a aplicação precisa interagir com o mundo exterior, ou seja, usuários, aplicativos clientes, aplicações de terceiros, etc. Para isso precisamos de mecanismos para expor ao mundo a funcionalidade oferecida pelos microsserviços. Uma forma de atender essa necessidade é através de API gateways que abstraem os detalhes da aplicação (como os microsserviços são implementados e como colaboram entre si para realizar uma determinada funcionalidade). O API gateway fica responsável por passar as solicitações do mundo externo para a aplicação e transformar ou combinar suas respostas para entregar o resultado de volta ao mundo externo.

Além de isolar os microsserviços do mundo externo, o API Gateway também concentra várias responsabilidades típicas de um sistema distribuído, como balanceamento de carga, autenticação, estabelecimento de políticas de segurança, gerenciamento de caches, estabelecimento de limites para número de chamadas externas e volume de tráfego de dados, além de registros estatísticos e registro de logs referentes às chamadas do mundo externo.

O uso de um API gateway é uma solução elegante para expor a funcionalidade dos microsserviços, mas pode se tornar complexa quando ele passa a ser o único ponto de interação com o mundo externo (por exemplo, com clientes da aplicação) para muitos microsserviços ou quando os microsserviços interagem com várias aplicações diferentes. Nesses casos, complementar essa abordagem usando pattern Backend For Frontend (BFF) pode ser bastante adequado.

O Backend For Frontend funciona como uma espécie de adaptador, fazendo uma adequação entre os serviços genéricos oferecidos pelos microsserviços e as necessidades específicas de cada cliente.

Um exemplo típico de utilização de Backend For Frontend aparece quando é necessário construir, para a mesma aplicação, dois tipos diferentes de interface de usuário: Web e mobile. Nesse caso, as duas interfaces normalmente apresentam necessidades diferentes no consumo das funcionalidades oferecidas pelos mesmos microsserviços. Tipicamente, o aplicativo mobile precisa de informações resumidas, e muitas vezes uma única tela exige informações de vários microsserviços diferentes. Ao mesmo tempo, dada a limitação de banda de comunicação típica das redes móveis e a necessidade de economizar bateria, é desejável reduzir o número de chamadas à retaguarda que o aplicativo realiza. Já a interface Web é mais flexível nesses requisitos, além de comumente apresentar informações mais detalhadas, não havendo necessidade de economizar banda de comunicação ou evitar o tráfego de maiores volumes de dados. Nessa situação, para evitar duplicar a funcionalidade dos microsserviços ou torna-los dependentes dos tipos de clientes que eles atendem, pode-se utilizar um Backend For Frontend específico para cada aplicativo, conforme a figura a seguir.

Cada Backend For Frontend poderia também ser construído como um microsserviço, beneficiando-se da infraestrutura disponível para sua própria execução, e cumprindo o papel de agregador ou orquestrador. Também é possível expor diretamente a funcionalidade oferecida por cada Backend For Frontend sem utilizar um API Gateway, mas nesse caso abre-se mão das funcionalidades adicionais oferecidas por ele.

Em certo sentido, o uso do pattern Backend For Frontend equivale a criar várias APIs diferentes sobre a funcionalidade oferecidas pelos mesmos microsserviços.

Uma outra abordagem possível seria criar uma espécie de super-API que permitisse ao consumidor definir a forma de resposta desejada por ele. Para construir essa super-API, pode-se usar uma linguagem de consulta para APIs (ex. GraphQL) que permite que os consumidores especifiquem quais informações desejam e também permite utilizar várias fontes/recursos em uma única consulta.

Confiabilidade e Reusabilidade

Conforme apresentado anteriormente, é natural existir uma rede de colaboração entre os microsserviços. Então, confiabilidade é uma característica fundamental nessa colaboração. O projeto de microsserviços deve levar em conta essa necessidade e garantir a confiabilidade provendo, por exemplo, mecanismos para recuperação de falhas e para comunicação confiável.

Sempre que se fala em componentização no mundo do software, a reusabilidade dos componentes é um dos objetivos mais importantes e, na abordagem de microsserviços não é diferente. Entretanto, uma das características dos sistemas baseados em microsserviços é a liberdade de escolha da linguagem e da tecnologia a ser utilizada para o desenvolvimento de cada um deles de forma independente. Desta forma, a reusabilidade pode parecer mais complexa, mas é possível em vários aspectos.

Quebrando o Monólito

Nesse artigo apresentamos algumas formas de definir o escopo de cada microsserviço que comporá uma aplicação, pressupondo que ela ainda não exista e está sendo definida. Entretanto, pode ser que estejamos trabalhando na transformação de uma aplicação monolítica em uma aplicação baseada em microsserviços e, para isso, podemos considerar outros aspectos para definir o escopo dos microsserviços, como por exemplo:

  • Identificar códigos replicados em vários módulos da aplicação;
  • Identificar se os módulos são fracamente acoplados ao resto dos serviços;
  • Identificar se alguma funcionalidade será fortemente utilizada (para saber o quanto o microsserviço que irá implementá-la deverá ser escalável).

Conclusão

Conforme podemos observar, o projeto de microsserviços envolve diversos aspectos importantes e muitos fatores devem ser levados em consideração para definir o escopo e a funcionalidade de cada um, bem como a forma de interação entre eles e também com o mundo externo. Para podermos abordar ordenadamente o assunto, elaboramos essa série de artigos sobre o tema, que inclui os aspectos teóricos, mas também resultados práticos advindos de nossa experiência na construção de aplicações completas para nossos clientes utilizando essa arquitetura. Fica o convite para ler a série completa.

Deixe um comentário

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

Newsletter

Insights de tecnologia para você!

Não compartilharemos seu e-mail com terceiros e também prometemos não enviar spams. Ao informar seu e-mail, você concorda com nossa Política de Privacidade.

Conteúdos relacionados

Veja nesse artigo de Edison Kalaf, sócio diretor da Opus Software, como a TI não é apenas operacional, mas um agente ...
Confira como funciona a Inteligência Artificial Geral, os impactos sociais e éticos dessa tecnologia e o que podemos ...
Veja nesse artigo como evitar drawbacks e interrupções de serviço ao utilizar Kubernetes, maximizando a eficiência.