Distribuindo Monólitos: SOA, Macro, Mini, Micro e Nano Serviços

Marcelo M. Gonçalves
Sicredi Tech
Published in
15 min readNov 18, 2022

Introdução e Contexto

A popularidade das soluções em nuvem, conteinerização de aplicações e ferramentas como Kubernetes, permitiram com que o fenômeno dos micro serviços prosperasse. Ao mesmo tempo, micro serviços não devem ser a abordagem padrão e nem a única. Monólitos, ou monólitos modulares não são inimigos a serem destruídos e representam estágios no processo de transição aos sistemas distribuídos. O entendimento sobre o domínio de negócio, ao qual estamos inseridos durante a jornada para as extremidades do espectro evolutivo, pode ser exercitado e facilitará na escolha adequada do tipo de arquitetura a ser adotada, seja macro, mini, micro ou nano serviços.

Há cerca de 20 anos atrás, Martin Fowler nos alertava sobre o que ele chamou de “a primeira lei dos objetos distribuídos”: Don’t distribute your objects. Ele estava referindo-se a complexidade adicionada automaticamente ao adotarmos sistemas distribuídos. A experiência nos mostra que todas as abordagens de arquitetura possuem benefícios e desafios, micro serviços não são exceção e não devem ser sua escolha default, na medida em que resolvem problemas ao custo de adicionar novos, ainda mais complexos.

Micro serviços não devem ser sua escolha padrão. É necessário que você realmente pense, cuidadosamente, se essa abordagem é a ideal para você.” — Sam Newman

A trajetória entre monólitos em direção aos nano serviços baseia-se na granularidade adotada em cada um dos trade-offs existentes durante o percurso. Quanto menor sua unidade implantação mais a filosofia Unix fará sentido: “Do one thing, and do it well”. Da mesma forma, a tecnologia não deve ser o pilar central nos direcionamentos de adoção de novos paradigmas de arquitetura. A abordagem escolhida precisa estar alinhada principalmente aos interesses do negócio e preferencialmente os times de infra/devops devem apoiar e dar suporte a estas escolhas.

Atingir um nível de maturidade aceitável na adoção da arquitetura de mini, micro ou nano serviços trata-se de algo desafiador e complexo. Portanto, como passo intermediário no processo, deveríamos considerar os macro serviços, conhecidos como monólitos modulares.

A complexidade acidental naturalmente pertence aos sistemas distribuídos

Independente da abordagem escolhida, não devemos esquecer do principal: devemos focar em agregar o máximo de valor ao negócio buscando eficiência nas operações através da escolha das ferramentas certas para o trabalho.

Durante o processo de descoberta e distribuição de um monólito, uma abordagem incremental pode ser adotada. Precisamos respeitar os limites e dosagens necessárias para a abordagem escolhida, pois quanto menor um serviço, maior será a sobrecarga de gerenciamento tanto na comunicação quanto na manutenção necessárias para torná-los úteis.

Para evitarmos uma Arquitetura Macarrônica / Big Ball of Mud precisamos manter disciplina, respeitando os limites contextuais e transacionais de cada componente ou funcionalidade sendo desenvolvida. Nesse sentido, respeitar os architectural drivers sempre será um bom ponto de partida.

Service-Oriented Architecture (SOA)

A existência do SOA introduziu a separação de funcionalidades únicas, abstraídas até então pelos monólitos. Em conjunto com Web Services, através do protocolo SOAP e do estilo REST, foram endereçados muitos dos desafios encontrados em aplicações monolíticas da época. A proposição do SOA era quebrar funcionalidades de negócio complexas em componentes, possibilitando modularizar e revolucionar as práticas de desenvolvimento de software até então prevalecentes.

A abstração das complexidades de infraestrutura e redes (service mesh) das soluções SOA dentro de um servidor de aplicação era oferecida de maneira padrão.

Atualmente, os conceitos e abordagem descritos no SOA encontram-se compreendidos e estabelecidos e devemos isso aos micro serviços. Micro serviços tratam-se do SOA batizado de forma diferente. A pergunta correta: Seria mesmo necessário um novo nome para os conceitos SOA?! Talvez não, mas a indústria precisa vender nomes que remetem ao sucesso, mesmo que seja ao custo de não compreender, de maneira sólida, seus conceitos e raízes. No fim das contas, talvez seja melhor ignorar e renomear o SOA do que tentar explicar seus princípios.

Durante a fase de entendimento do contexto do problema a ser resolvido, qualquer padrão ou estilo pertencerá a um refinamento evolutivo. Adquirindo maturidade e independente do seu propósito ou estratégia técnica, o valor agregado ao negócio será o requisito principal.

SOA desempenhou um papel essencial durante sua trajetória, influenciando positivamente todos os seus sucessores. Se observarmos no espectro evolutivo, e na linha do tempo, encontraremos sua posição entre os monólitos e os macro serviços. Graças aos seus conceitos revolucionários e elegantes aqui estamos hoje. Assim, qualquer estilo ou padrão de arquitetura que de alguma forma assemelha-se ao SOA ou algum de seus princípios estruturais, estará mais próximo de trilhar um caminho em direção ao sucesso.

Parte da essência do SOA revive nos sistemas distribuídos atuais, consequentemente dentro dos PODs e Containers espalhados mundo a fora.

Monólitos — Aplicações Monolíticas

Monólitos tradicionais apresentam tanto vantagens quanto desvantagens. A centralização da comunicação, infraestrutura, redes e controle transacional conferem a este tipo de arquitetura a redução da complexidade se comparados aos sistemas distribuídos. Ao mesmo tempo, principalmente por conta de sua rigidez, acabamos perdendo agilidade no fluxo de entrega e integração contínua.

Identificamos uma aplicação monolítica quando todos os componentes necessários para a execução de uma determinada tarefa encontram-se dentro da mesma unidade de deploy, estando contidos dentro das fronteiras da aplicação. Consequentemente, menor complexidade, forte acoplamento entre os as camadas e serviços, favorecimento das ACID transactions e transações distribuídas JTA + XA com 2PC (two-phase commit) lhe conferem vantagens competitivas nesses campos de desempenho de uma aplicação.

Aplicações podem, da mesma forma, serem consideradas monolíticas quando os serviços de domínio compartilham uma única instância de banco de dados ou servidor de aplicação, realizam chamadas diretas a outros endpoints (upstream/downstream services), ou encontram-se implantados e rodam dentro de um mesmo runtime (JVM, BEAM VM).

Diante deste cenário, estaremos inclinados a buscar abordagens mais distribuídas como os mini ou micro serviços. A verdade é que nem sempre sistemas distribuídos são adequados para todos os casos de uso, ao passo em que os desafios que eles trazem muitas vezes superam os benefícios concedidos. Alguns destes desafios, como complexidade de manutenção, difícil escalabilidade vertical/horizontal e refatoração, nos obrigam a retroceder para estarmos em condições de avaliarmos outras opções disponíveis, a fim de encontrar um meio termo entre as duas extremidades.

O termo monólito refere-se a unidade de deployment. Porém tornou-se, de forma equivocada, sinônimo de aplicações legadas.

Com o objetivo de encontrarmos a granularidade mais adequada para um serviço, processos adaptativos/evolutivos podem ser adotados e as provas de conceito (POCs) podem auxiliar neste procedimento. Como passo inicial, podem ser criados monólitos, pela facilidade de gerenciamento de recursos, evoluindo o entendimento do domínio de negócio durante o processo de descoberta, culminando na adoção dos sistemas distribuídos.

Em aplicações monolíticas não perdemos a possibilidade de beneficiar-nos da atomicidade das transações nas operações executadas, tais como commit e rollback.

Na época em que os monólitos prosperaram, esta era a única forma de construirmos software, sendo aplicações autocontidas e transacionalmente independentes. Esta abordagem, geralmente estruturada em três camadas, perdurou por muitos anos na indústria, podendo ser encontrada até hoje nos quatro cantos do mundo. Assim, devemos muito aos monólitos por desempenharem, de forma bem sucedida, seu papel durante o tempo em que foi necessária sua existência.

Macro Serviços — Monólitos Modulares

Monólitos modulares referem-se a uma abordagem para design de software, concebidos na década de 70, onde decompomos um monólito em módulos reutilizáveis. O modelo foi disseminado por Simon Brown e diferente de um monólito tradicional, o monólito modular sugere a extração de componentes explícitos, contendo interfaces de comunicação bem definidas e seguindo os princípios da modularização que são usados para distribuirmos.

“Se você não consegue construir um monólito bem estruturado, o que o faz pensar que micro serviços são a resposta?” — Simon Brown

David Parnas escreveu, em 1971, um paper que, para nós, é a melhor referência sobre o que é um design modular e como construí-lo. Neste trabalho, Parnas define que um sistema modular é aquele que é dividido em um número relativamente independente de módulos, com interfaces de comunicação bem definidas. Enquanto os mini, micro e nano serviços obrigatoriamente exigem unidades individuais de deploy e abordagens cada vez mais granulares, monólitos, sejam modulares ou não, referem-se a uma unidade única de deployment.

Monólitos modulares permitem um estágio intermediário entre as extremidades, quebrando o sistema em capacidades contendo features e funcionalidades. As capacidades de um sistema podem abstrair e extender diversos domínios de negócio. Práticas de modularização melhoram o processo, tornam o controle de dependências mais gerenciável e melhora a interoperabilidade nos componentes modulares.

Modular Monoliths - Simon Brown

A simples extração e decomposição em módulos de um monólito naturalmente já resolve uma série de problemas. Além disso, diferentes times podem responsabilizar-se e trabalharem em módulos distintos efetuando a comunicação através de interfaces expostas.

Estrategicamente, empresas com inviabilidade na transição para componentes mais distribuídos, seja por tempo, dinheiro ou complexidade, acabam aderindo a um grau intermediário referente ao estilo de arquitetura selecionado, beneficiando-se, mas não em sua na totalidade tanto da arquitetura com mini ou micro serviços quanto dos monólitos tradicionais.

Tanto nos monólitos modulares, quanto nos mini ou micro serviços, módulos comunicam-se uns com os outros via APIs (HTTP) ou mensagens/eventos. Preferencialmente, apresentando fraco acoplamento e comunicação assíncrona. Além disso, os módulos compartilham o mesmo esquema de banco de dados, podendo rodar dentro da mesma VM ou VMs distintas.

Projetar um sistema modular não é uma tarefa fácil, desta forma, o modelo definido por Simon Brown, denominado C4 propõem-se a ajudar-nos na etapa de documentação de arquitetura em diferentes níveis de abstração durante a jornada aos monólitos modulares. Precisamos pensar em como os módulos podem ser separados; como estabilizar a comunicação entre eles; e como lidar com o acoplamento, tornando esse trabalho bastante desafiador. Abraçar a modularidade reflete-se como impacto positivo nos times, adicionando facilidades, exigindo menor tempo no desenvolvimento de novos produtos e funcionalidades.

Mini Serviços

Definimos um mini serviço como uma coleção de serviços pertencentes ao mesmo domínio de negócio/dados, apropriando-se e agrupando múltiplas funcionalidades expostas através de APIs. Os mini serviços são menos restritivos, em relação aos micro serviços, ao passo em que possibilitam o compartilhamento dos recursos de infraestrutura e armazenamento, sugerindo que o estilo de comunicação padrão seja via APIs.

Em outras palavras, a pureza arquitetural dos micro serviços são deixadas um pouco de lado em nome do valor agregado ao negócio e da redução da complexidade imposta por eles. Juntamente com grupos de serviços mais granulares as vantagens incluem, melhoria de performance, na medida em que o número de serviços e suas interconexões são reduzidas e consequentemente facilitando o desenvolvimento e favorecendo a entrega contínua.

Devemos sempre considerar as especificidades de cada projeto: “Best tool for the job”.

Ao quebrarmos monólitos diretamente em miniserviços, adquirimos vantagens tanto verticais quanto horizontais, a considerar atributos como estilo de comunicação, complexidade transacionais, consistência eventual, tracing e logging distribuído, camada de service mesh, entre outras. Os desafios encontrados, devido ao grande número de dependências associadas a um único serviço, contribui para o aumento de complexidade nas áreas de controle eficiente de erros e descoberta de bugs presentes no sistema.

Aplicações baseadas em mini serviços são escaláveis e robustas, encaixando-se bem em determinados projetos.

Em uma arquitetura genuinamente de micro serviços, nenhum serviço deve ter ciência ou consciência do mundo ao seu redor, comunicando-se exclusivamente através de eventos e de forma assíncrona. Na visão purista dos micro serviços, esta regra será facilmente violada no caso da comunicação ocorrer de forma síncrona utilizando protocolos como HTTP.

Devemos considerar o momento certo para abraçarmos os mini services, e quando devemos deixar de lado atributos como escopo e tamanho adequados. Os mini serviços são uma forma de tornar os micro serviços um pouco mais simples, fáceis e práticos. Na medida em que os micro serviços nos restringem exigindo que cada serviço deverá isolar-se com seus dados, os mini serviços propõem compartilhá-los.

“Don’t confuse architectural perfection with business value.” — Ross Garrett

Essencialmente, podemos identificar serviços candidatos a mini serviços nos casos em que: Múltiplas aplicações ou serviços compartilham o mesmo banco de dados ou schema; A comunicação entre os serviços ocorre de forma síncrona utilizando APIs, seja REST via HTTP, GRPC via TCP; Não abraçam uma arquitetura orientada a eventos com comunicação assíncrona; Compartilham infraestrutura para unidades de implantação (deployment).

Conceitualmente, a nível elementar, mini serviços divergem dos micro serviços permitindo um comportamento menos dogmático e rigoroso. Neste sentido, os mini serviços ganham força apresentando uma abordagem mais pragmática e menos restritiva, porém como em qualquer arquitetura adotada, mais ou menos distribuída, tanto benefícios quanto limitações estarão presentes.

Ao definirmos abordagens de arquitetura a serem seguidas, estamos em busca do máximo de ganho com o preço mínimo a ser pago em termos de complexidade. Tanto os mini serviços quanto micro serviços propõe-se a dividir as funcionalidades em pedaços menores mantendo-os em seus contextos delimitados específicos aos respectivos domínios de negócio. O caminho correto a ser seguido dependerá dos papéis e pessoas envolvidas no processo, resolvendo da forma assertiva possível os trade-offs.

Micro Serviços

Micro serviços são sistemas distribuídos. Em poucas palavras, representam um estilo arquitetural onde as features e funcionalidades são decompostas em pedaços menores, independentes e agnósticos à stacks de tecnologia. Estes pequenos componentes, conhecidos como serviços, abraçam a consistência eventual, normalmente são implantados em containers e comunicam-se de forma assíncrona através de eventos.

Por design ou por acidente, micro serviços favorecem práticas e operações DevOps, como integração e entrega contínua e implantação.

Sistemas distribuídos, naturalmente são acompanhados de certas limitações: Aumento da complexidade; dificuldade de manutenção e testes de integração; camada de segurança; compartilhamento de dados e código; dificuldade de construção de dashboards e relatórios; entre outros. Neste contexto precisamos exercitar o conceito de microservices ownership.

Inicialmente, os micro serviços foram concebidos para lidarmos com os desafios de quebrarmos as complexidades dos monólitos. Evidentemente, conceitos e funcionalidades como Cloud Native, execução em containers, CI/CD, e DevOps integram o conjunto de vantagens competitivas dos micro serviços. Características as quais remodelaram a forma de como construímos e disponibilizamos nossas aplicações.

Por melhor que seja o estilo de arquitetura adotado, soluções do tipo one-size-fits-all” simplesmente não existem.

Algumas decisões devem ser tomadas quanto a computação distribuída: complexidades na camada de service mesh; estilo e natureza de comunicação; deployments; operações e tracing/logging distribuído; gerenciamento de estado dos componentes. Especificamente, em relação aos micro serviços, as restrições apresentadas seriam: Impossibilidade de comunicação síncrona, prevenindo timeouts em cascata; comunicação precisa ser assíncrona, sem bloquear o fluxo do processo de execução; abraçar a consistência eventual e utilizando etapas de compensação em caso de falhas se necessário.

Em sistemas distribuídos de maneira geral, precisamos assumir a presença dos erros, criando aplicações resilientes e tolerantes a falhas. Caso contrário estaremos sujeitos às falácias da computação distribuída.

A adoção dos micro serviços entregam vantagens do ponto de vista do SDLC (Software Development Life Cycle). Através do nível de granularidade correto e do Single Responsibility Principle (SRP), os benefícios incluem: melhora na escalabilidade e resiliência; tolerância e isolamento das falhas; maior flexibilidade no gerenciamento da stack de tecnologia adotada em cada unidade de deployment.

Ao acharmos estar fazendo a coisa certa e não termos direção para onde estamos indo, ao conduzir a evolução de arquitetura dos nossos projetos vale um ponto de atenção sobre o pior dos monólitos: o monólito distribuído.

Ao abraçarmos os micro serviços estaremos pagando um preço alto pela agilidade e flexibilidade, ao custo de adicionarmos complexidade ao processo. O aumento da complexidade impacta diretamente na queda da produtividade. Neste cenário, a presença de padrões de arquitetura e de persistência como CQRS e Event-sourcing (ES) respectivamente, em conjunto com EDA e Domain-driven Design (DDD) mostram-se como alternativas visando facilitar uma implementação bem sucedida da arquitetura de micro serviços.

Nano Serviços

Quando a granularidade encontrada nos micro serviços ainda não é o bastante, podemos introduzir o conceito de nano serviços. Não existe definição exata do tamanho que um micro serviço deveria possuir, o mesmo se aplica aos nano serviços e dependerá do processo de descoberta e definição visando encontrar a granularidade correta das funcionalidades sendo construídas.

Por definição, os nano serviços propõem-se a fazer uma única coisa, nascer e morrer no contexto do problema onde foram concebidos. São desenhados para serem descobertos e descobrirem outros serviços semelhantes, significa que se seu escopo é reduzido, auto contidos e reutilizáveis. Possuem toda a infraestrutura relevante para sua execução, podendo ser útil em tarefas de automação dos procedimentos de deployment.

Se nos referirmos a granularidade proposta pelos sistemas distribuídos, independente da complexidade agregada, micro serviços representam uma evolução em referência aos monólitos bem como os nano em relação aos micro serviços.

Os benefícios dos nano serviços não são somente técnicos e estendem-se além dos micro serviços. Independentemente, cada nano funcionalidade pode seguir seu próprio ciclo de vida, provendo um nível extra de flexibilidade se comparados aos mini ou micro serviços, na medida em que podem ser combinados nos pipelines de execução, oferecendo possibilidade de composição de fluxos mais complexos.

Precisamos de cuidado ao distribuirmos demais nossas aplicações, serviços e componentes, sob o risco de resultar em diversos pequenos problemas distribuídos. Sistemas distribuídos são complexos de construir, e mais ainda de mantê-los. Ao estruturarmos nossos componentes com uma granularidade fina demais podemos facilmente encontrar duplicações de problemas já resolvidos dentro do mesmo domínio de negócio, aumentando a redundância e discrepância entre as soluções existentes.

Exemplos de nano serviços: AWS Lambda, Azure Functions ou Google Cloud Functions rodando em Serverless.

Tanto os nano quanto micro serviços entregam benefícios, adicionando novos problemas naturalmente. Do ponto de vista de custos de manutenção, quanto mais blocos ou serviços criarmos, maior será a infraestrutura e redes com os recursos necessários para suportá-los. Idealmente, um nano serviço deve ser enxuto, em algumas situações sendo necessária a criação de uma quantidade maior do que a esperada para determinada funcionalidade, gerando custos de extras de execução que seriam inexistentes utilizando micro serviços.

Para não sacrificar a usabilidade precisaremos agrupar diversos serviços para abstrair determinada funcionalidade, controlar a comunicação, consistência dos dados e controle de resiliência e falhas, podendo tornar-se desafiador se a abordagem de arquitetura escolhida não for a adequada. No fim das contas, quanto menor for a escala em que estivermos atuando, mais difícil será a percepção do que acontece aos arredores e no mundo externo ao contexto do nosso nano serviço.

Considerações Finais e Recomendações

A recomendação do tipo de arquitetura adequada dependerá do tipo de projeto, viabilidade de recursos, e experiência dos times em questão. Os caminhos e orientações serão distintas tanto quando optamos pela modernização de aplicações legadas quanto pela criação inteiramente do zero de um grupo de serviços ou serviços individuais.

Em projetos de modernização, iniciamos a distribuição de aplicações monolíticas em pedaços menores. Esta reconstrução pode ocorrer por diversas razões: Migração para nuvem; débito técnico; melhorias nos processos de segurança; controle de riscos; agregar maior valor ao negócio; entre outros.

Em alguns casos, embora o custo para a construção de algo totalmente do zero seja alto, podem ser justificados quando apresentados os benefícios dos sistemas distribuídos. Os ganhos de agilidade em processos de CI/CD quando adotados componentes granulares, mini, micro ou nano serão evidentes. Ao optarmos pela modernização dos componentes existentes, aumentaremos os riscos e custos relacionados à complexidade e tráfego de dados.

Como um passo intermediário, modularizar nossas aplicações monolíticas facilitam a transição entre os tipos de abordagens disponíveis, até encontrarmos a que melhor se encaixa na necessidade de cada projeto. Assim, temos a possibilidade de alcançar parte das vantagens oferecidas pelos micro serviços sem arcamos com os custos de sua adoção. Contudo, monólitos modulares possuem suas deficiências, se comparados aos micro serviços, principalmente nas áreas de testes contínuos, integração e entrega contínua, versionamento e implantação.

A granularidade adequada para nossos serviços está relacionada a complexidade acidental e consistência transacional

De maneira geral e tecnicamente a depender da disposição e distribuição de funcionalidades em repositórios existentes, sejam distribuídos ou centralizados, seremos direcionados à adoção de mini ou micro serviços. De qualquer forma, as métricas para a decisão final do tipo de arquitetura a ser adotada nunca devem ser simplórias ou superficiais e devem refletir a realidade de cada empresa e projeto específicos.

O aumento da complexidade de um projeto pode ser um direcionador importante referente ao estilo e natureza de comunicação adotado. Caso seja necessário comunicação síncrona utilizando APIs (HTTP/REST) durante o processo, mini serviços são a escolha correta. A comunicação assíncrona, através de eventos (Pub/Sub) deverá ficar por conta da adoção dos micro serviços.

Identificar o nível de granularidade correto e os limites contextuais para cada serviço trata-se de algo complexo, devendo ser realizado com cuidado. Caso esta calibragem esteja inadequada pagaremos o preço pelo equívoco.

Micro serviços são uma ótima abordagem, porém o preço pago poderá ser alto a depender dos objetivos de negócio. Do ponto de vista de eficiência de custos, mini serviços superam os micro serviços. Neste momento, os macro serviços poderão oferecer um meio termo. Uma estratégia seria migrar monólitos tradicionais para modulares, e a partir disso, evoluir sob demanda para serviços mais granulares.

Os trade-offs dependem da necessidade do negócio e da realidade dos requisitos de cada projeto. As coisas precisam fazer sentido e para isso precisamos considerar a importância do entendimento do problema sendo resolvido antes de optarmos por cada uma das abordagens descritas no decorrer deste artigo. Em todos os casos, precisamos estar cientes dos preços a serem pagos ao longo da jornada. Ninguém será capaz de prever o futuro de um projeto, a depender de diversos fatores, mas certamente compreender os princípios do problema sendo resolvido, tendo as ferramentas certas em nossas mãos, já será metade do caminho em direção ao sucesso.

--

--