Quando se fala em independência de um microsserviço, o que de fato está sendo dito?
A independência tem função de viabilizar com que roadmaps de um microsserviço, ou microsserviços de uma time/equipe, tenham capacidade de evoluir sem atrito, sem afetar ou interferir nos microsserviços dependentes ou equipes relacionadas.
Se somarmos microsserviços a micro-frontends teremos a capacidade de criar novas funcionalidades inteiras, de ponta-a-ponta, sem que necessariamente seja necessário realizar implantações de outros microsserviços.
É disso que microsserviço trata, da capacidade de entregar features, releases, evoluções de roadmap de forma independente.
E claro, micro-frontend é uma arquitetura que casa como uma luva com esses interesses, embora não seja obrigatória.
Nesse contexto, a nova release poderia ser entregue da UI até todas as suas demandas internas, sem depender de outros roadmaps, implementações e implantações.
Spotify tem exemplos que nos ajudam a entender essa jornada (saiba mais)
Ao pensarmos em microsserviços precisamos pensar em diversos níveis de independência.
Independência de Implantação
Significa conseguir implantar novas releases, sem impactar microsserviços dependentes. Implica em gerenciar bem os contratos dos serviços e lidar com algum nível de omissão de dados entre microsserviços.
De um lado é necessário minimizar a demanda por tarefas que permeiem vários microsserviços, para que somente em momentos raros, seja necessário coordenar roadmaps que extrapolem um único microsserviço.
Independência de Roadmap
Ao conseguir tornar os roadmaps independentes, conseguimos segmentar os times, em verticais para que cada vertical lide com um conjunto limitado e afim de microsserviços.
Para isso é importante considerar que modelagem e design sejam consistentes e coesos para reduzir o nível de dependência e acoplamento, e mover para níveis elevados (API’s) ao invés de níveis baixos, como bancos de dados.
Uma separação clara entre eventos e comandos ajudam a reduzir o acoplamento fantasma e dão visibilidade à quem depende do quê, e qual serviço depende um do outro.
Independência de Bancos de Dados
Ao compartilharmos bancos de dados, criamos um acoplamento em um nível baixo, inviabilizando qualquer mudança por potencialmente gerar impacto em outros serviços.
A relação de propriedade e exclusividade de um serviço com suas estruturas de dados, é fundamental para a evolução de um microsserviço independente dos demais.
Para termos independência real, precisamos mover o acoplamento de níveis baixos, como no banco de dados, para camadas mais elevadas como API’s HTTP.
Do ponto de vista de arquitetura, é possível lidar com assincronismo, cache, e uma série de patterns que aumentar a resiliência e tolerar indisponibilidades.
Mas também habilita o próprio microsserviço a realizar suas próprias evoluções na medida que sejam necessárias.
Isso não apenas inclui adição de campos, mas mudanças de tipos, revisão de formatos, mas permite também projetos de fragmentação, reestruturação de cardinalidade, e evoluções naturais demandadas pelo dia-a-dia de evolução do negócio e do crescente volume de acesso e quantidade de dados.
Em contrapartida, tendo um acoplamento em baixo nível, como o compartilhamento do banco e tabelas, gera-se a impossibilidade de tomar essas decisões sem gerar impacto nos demais serviços, e é aqui que o problema do roadmap é gerado.
Quando compartilhamos banco, nos vemos em uma situação chata e complicada, e a tendência, é evitar o conflito de interesses de ambos os roadmaps, adotando soluções pautadas no que dá para ser feito, em vez de fazer o que precisa ser feito, visando minimizar os impactos dessa relação de dependência.
A consequência natural é temos cada vez mais gambiarras em vez de soluções.
E ao evitarmos as mudanças que precisam ser feitas, produzimos soluções de contorno.
Mas ao mover o acoplamento para as API’s, não estamos mantendo o acoplamento?
Para discutir esse assunto, precisamos voltar à lei de Conway.
Qualquer organização que projeta um software inevitavelmente produz um design cuja a estrutura é uma cópia da estrutura de comunicação de uma empresa.
Conway’s law – Melvin Conway
Então devemos considerar que o design também carrega as relações de dependência dessa estrutura de comunicação.
Em termos práticos, dependências do mundo real se materializam nas dependências entre microsserviços.
Entretanto, essa relação de dependência, que antes residia em uma camada de baixo nível, como o banco de dados, agora é movida para uma camada superior, como API’s, de tal forma que mudanças internas tenham oportunidade de não afetar serviços dependentes.
Ao compartilhar tabelas de um bancos de dados, permitimos um acoplamento de baixo nível que nocivo para microsserviços. Nesse contexto, a relação de dependência, ocorre em um nível no qual existe pouca ou quase nenhuma capacidade de abstração disponível.
Com o compartilhamento de tabelas, qualquer tipo de evolução nas estruturas de dados é potencialmente nociva para serviços dependentes, inclusive as mais simples.
Movimentos de migração para estruturas mistas, adoção de CQRS, e mudanças simples de estrutura de dados passam a ser inviabilizados pelo medo de gerar impacto nos demais serviços dependentes dessas estruturas.
A adoção de estruturas de otimização, como redis, não estão disponíveis com esse tipo de acoplamento.
A capacidade de adotar uma base NoSQL de suporte deixa de existir e todas as possibilidades agora estão presentes necessariamente em SGDB’s.
Uma simples decisão de normalização ou desnormalização são inviáveis sem o desconforto do impacto nos serviços dependentes.
PS: Não use esses argumentos como espantalho,
meras mudanças de cardinalidade,
criação de colunas obrigatórias,
ou remoção de colunas
são suficientes para gerar quebras em microsserviços dependentes de objetos de banco.
Ao escolhermos compartilhar tabelas, ou manter esse compartilhamento, faremos com que todas as grandes as decisões de modelagem precisem considerar todos que dependem dessas estruturas, portanto, perderemos a capacidade de evoluir essa base na direção certa. Quanto maior e mais extenso for esse acoplamento, menos conseguiremos evoluir essa base.
Quando movemos esse acoplamento para as API’s, temos total capacidade de manter e respeitar contratos antigos, mesmo que mudemos completamente nossa estrutura de dados interna, até entre tecnologias.
É importante ressaltar que não é objetivo realizar mudanças arquiteturais ou quebra em sql e nosql, ou ainda evoluir as estruturas de dados, algumas podem se manter estáveis por muito tempo.
Entretanto, reagimos às demandas de negócio e demandas do workload e elas podem demandar todo tipo de mudança e não podemos bloquear as principais.
Um simples uso eficiente de cache é capaz de economizar milhares de dólares em alguns cenários.
Em nenhum momento estamos falando de aspectos de infraestrutura, como compartilhar servidores de bancos de dados.
A discussão gira em torno do compartilhamento de objetos, como tabelas, views etc.
Se assegurarmos a independência, ainda no desenvolvimento, com instâncias isoladas ou com usuários com permissões restritas, conseguimos criar condições para que qualquer topologia possa ser adotada em produção, viabilizando, inclusive, o compartilhamento de servidores de bancos de dados.
O compartilhamento de servidor de banco pode ser uma questão de economia, quando workload é pequeno, entretanto isolar o database e criar uma instância de banco exclusiva para um microsserviço não deveria impactar outros microsserviços.
Esse não é o argumento central da independência de banco de dados, o argumento central visa a independência na evolução de objetos de banco, mas é mais uma forma de entender e avaliar se as decisões que estão sendo tomadas são coerentes com os objetivos arquiteturais.
O fato de 2 microsserviços terem suas bases no mesmo servidor não dá o direito para nenhum dos 2 acesse os dados do outro.
A fusão de vários microsserviços em um único servidor de banco, tem de ser uma decisão de OPS e ninguém deveria ser impactado por isso.
Unir ou separar, é uma decisão relativa ao workload, custo, e gestão do ambiente de produção e não deveria gerar impacto para mais ninguém além do microsserviço que é dono daqueles objetos.
Independência de Equipes/Times
Susan Fowler argumenta que sim no livro Microsserviços prontos para a produção: Construindo sistemas padronizados em uma organização de engenharia de software e chama esse pensamento de Lei de Conway Reversa.
A arquitetura de microsserviços consiste em um grande número de pequenos microsserviços independentes e isolados. A Lei de Conway Reversa exige que a estrutura organizacional de toda empresa que usa arquitetura de microsserviços seja composta de um grande número de equipes muito pequenas, isoladas e independentes. A estrutura de equipes resultante inevitavelmente leva aos fenômenos de segregação e dispersão, problemas que pioram sempre que o ecossistema de microsserviços se torna mais sofisticado, mais complexo, mais simultâneo e mais eficiente.
A Lei de Conway Reversa também significa que os desenvolvedores serão, em muitos aspectos, como microsserviços: eles serão capazes de fazer uma tarefa e (espera-se) fazer essa tarefa muito bem, mas eles estarão isolados (em termos de responsabilidade, conhecimento do domínio e experiência) do resto do ecossistema. Quando considerados juntos, todos os desenvolvedores trabalhando coletivamente dentro de um ecossistema de microsserviços saberão tudo que há para saber sobre ele, mas individualmente eles serão extremamente especializados, conhecendo apenas as partes do ecossistema pelas quais eles são responsáveis.
Fowler, Susan J.. Microsserviços prontos para a produção: Construindo sistemas padronizados em uma organização de engenharia de software (Portuguese Edition) (pp. 55-56).
Não precisamos pensar em exatamente um time por microsserviço, mas podemos pensar em times lidando com microsserviços afins.
Não sei você, mas me recordo de meu primeiro contato com a ideia de independência real, do microsserviço, e o frio na barriga, por entender que estava diante de algo muito, mas muito maior do que eu realmente concebia até então.
Me recordo que entender esse nível de isolamento e desacoplamento me fazia pensar que seria impossível adotar microsserviços em qualquer lugar.
Principalmente, seria impossível ser uma decisão do projeto ou do time, era algo grande que dependia de executivos, e diretores em geral, gente que olharia para custos, para contratação e head count.
Independência de Versionamento
Olhando para o aspecto de gerência de configuração, vemos que controlamos a versão de um repositório GIT por meio de suas TAG’s.
As Tags são atribuídas ao repositório, selecionando um commit como marco desse projeto, independendo da branch podendo estar em várias branches.
Assim, um repositório GIT é a menor unidade da qual controlamos versões.
Dessa forma deveria ser natural pensarmos que cada microsserviço deveria ter, necessariamente, seu próprio e exclusivo repositório GIT, afinal controlamos suas versões de forma independente, implantamos de forma independente, temos seus bancos independentes, e times independentes.
Essa é a ideia central de multirepo:
Além de reutilizar código por meio de bibliotecas, de que outra maneira poderemos fazer alterações em mais de um repositório? Vamos analisar outro exemplo. Na Figura 7.8, eu gostaria de modificar a API exposta pelo serviço Estoque, e preciso atualizar também o serviço Expedição para que ele possa fazer uso dessa alteração.
Se os códigos tanto de Estoque como de Expedição estivessem no mesmo repositório, eu poderia fazer o commit do código
de uma só vez.
Em vez disso, terei de separar as alterações em dois commits – um para Estoque e outro para Expedição. Figura 7.8 –
Alterações que cruzam fronteiras entre repositórios exigem vários commits. Ter essas alterações separadas poderia causar problemas se um commit falhar, porém o outro funcionar – talvez eu tivesse de fazer duas alterações para efetuar o rollback da mudança, por exemplo, e isso seria complicado se outras pessoas tiverem feito check-ins nesse ínterim. A verdade é que, nessa situação específica, é provável que eu queira, de certo modo, fazer os commits em fases, de qualquer maneira.
Eu iria querer garantir que o commit para alterar o serviço Estoque funcionasse antes de modificar qualquer código de cliente no serviço Expedição – se a nova funcionalidade não estiver presente na API, não haverá motivos para ter um código de cliente que faça uso dela.
Já conversei com várias pessoas que acham que a falta de implantações atômicas nesse caso é um problema relevante.
Sem dúvida, posso compreender a complexidade resultante dessa situação, mas acho que, na maioria dos casos, o cenário aponta para um problema subjacente mais importante.
Se você faz continuamente alterações envolvendo vários microsserviços, talvez as fronteiras de seus serviços não estejam no lugar certo, e isso poderia implicar um excesso de acoplamento entre os seus serviços.
Conforme já discutimos, estamos tentando otimizar a nossa arquitetura – e as fronteiras de nossos microsserviços – de modo que seja mais provável que as alterações sejam aplicadas dentro das fronteiras de um microsserviço.
Alterações que cruzem fronteiras deveriam ser a exceção, e não a norma.
Com efeito, eu argumentaria que a dificuldade de trabalhar com vários repositórios poderia servir para ajudar a definir melhor as fronteiras dos microsserviços, pois obrigará você a pensar com mais cuidado no local em que essas fronteiras estão, e na natureza das interações entre elas.
Se você faz constantemente alterações que envolvam vários microsserviços, é provável que as fronteiras de seus microsserviços estejam no lugar errado. Talvez valha a pena considerar a fusão de alguns microsserviços caso você perceba que isso esteja acontecendo. Há também o incômodo de ter de extrair códigos de vários repositórios e enviá-los para vários repositórios como parte de seu fluxo de trabalho usual.
Com base em minha experiência, posso dizer que isso pode ser simplificado seja pela utilização de um IDE que aceite vários repositórios (é uma tarefa com a qual todos os IDEs que utilizei nos últimos cinco anos são capaz de lidar), seja escrevendo scripts wrapper simples para deixar o trabalho na linha de comando mais fácil. Quando usar esse padrão Utilizar a abordagem de um repositório por microsserviço funciona bem tanto para equipes pequenas como para equipes grandes, mas, se você se vir fazendo várias alterações que cruzem as fronteiras dos microsserviços, talvez não seja a solução adequada para você, e o padrão monorepo que discutiremos a seguir talvez seja mais apropriado
– embora fazer várias alterações que cruzem as fronteiras dos microsserviços possa ser considerada uma advertência de que algo não está certo, conforme discutimos antes. Esse padrão também pode deixar a reutilização de código mais complexa quando comparado com a abordagem com monorepo, pois você dependerá de códigos contidos em artefatos com versões específicas.
Newman, Sam. Criando Microsserviços – 2ª Edição: Projetando sistemas com componentes menores e mais especializados (Portuguese Edition) (p. 361).
Newman não descarta monorepo, e o lista também como um padrão.
Com uma abordagem monorepo, o código de vários microsserviços (ou outros tipos de projetos) é armazenado no mesmo repositório de códigos-fontes. Já vi situações em que um monorepo era utilizado somente por uma equipe para gerenciar o controle de versões de todos os seus serviços, embora o conceito tenha se popularizado com algumas empresas de tecnologia muito grandes, nas quais várias equipes e centenas, se não milhares, de desenvolvedores podem trabalhar no mesmo repositório de códigos-fontes.
Ao ter todo o código-fonte no mesmo repositório, permitimos que alterações no código-fonte possam ser feitas entre diferentes projetos de forma atômica, além de possibilitar uma reutilização de códigos mais específicos de um projeto para outro.
A Google provavelmente é o exemplo mais conhecido de uma empresa que utiliza a abordagem de monorepo, embora esteja longe de ser a única. Embora haja outras vantagens nessa abordagem, por exemplo, mais visibilidade no código de outras pessoas, a capacidade de reutilizar código facilmente e de fazer alterações que causem impacto em vários projetos diferentes frequentemente é citada como o principal motivo para a adoção desse padrão.
Se considerarmos o exemplo que acabamos de discutir, no qual queremos fazer uma alteração no microsserviço Estoque para que ele exponha um novo comportamento, e atualizar o serviço Expedição para que utilize essa nova funcionalidade exposta, essas alterações poderiam ser feitas com um único commit, conforme vemos na Figura 7.9. Figura 7.9 – Utilizando um único commit para fazer alterações em dois microsserviços usando um monorepo. É claro que, como ocorre no padrão multirepo discutido antes, ainda precisaremos lidar com o lado da implantação nesse caso. Provavelmente teríamos de considerar cuidadosamente a ordem das implantações se quisermos evitar uma implantação sincronizada.
Newman, Sam. Criando Microsserviços – 2ª Edição: Projetando sistemas com componentes menores e mais especializados (Portuguese Edition) (p. 361).
O problema é que gerência de configuração é um assunto que foi se perdendo com o tempo, e parece ter morrido junto com o RUP.
Então, 20 anos se passaram e já não parece tão absurdo gerenciar múltiplos assuntos no mesmo repositório GIT, algo impossível de se imaginar quando levarmos em consideração a gerência de configuração, em especial gestão de versões e releases.
Quando nos deparamos com o cenário de que versionamos apenas o repositório, e não uma branch, como fazíamos no SVN, somos (ou deveríamos ser) empurra na direção dos multirepos.
Isso porque com monorepo, o GIT não é suficiente para que seja feito o gerenciamento adequado do repositório.
Precisamos de ferramentas de gestão, e aqui estão algumas listas e curadorias sobre essas ferramentas:
- monorepo.tools
- github.com/korfuri/awesome-monorepo
- github.com/shopsys/monorepo-tools
- itnext.io/the-3-best-monorepo-tools-for-2023-290bd4be8f0b
Trabalhar com múltiplos repositórios não é novo, uso desde ~2016, mas só começamos a ver suporte aqui no Microsoft NOV/2022 (Multi-repository Support Released!)
Primeiro com o Visual Studio no final de 2022, agora com o ASPIRE em 2024.
Acredito que submodules do GIT seja novo para a maioria dos devs .NET exatamente pela demora em adicionar o suporte à essa feature do git. E por isso é natural que o dev .NET sinta-se desconfortável, já que é a primeira vez que está sendo introduzido a esse assunto.
O eShop-On-Containers da Microsoft, feito com um único repositório é um desses exemplos, entretanto, não seria muito saudável trabalhar aquele projeto com um único repo, e por isso o eShop Cloud Native, foi desde sua concepção pensado em trazer a realidade dos múltiplos repositórios, mesmo antes do suporte do Visual Studio, afinal, usar a interface do visual studio para interagir como git pode ser útil, mas não pode ser limitadora.
Conclusão:
A independência é o ponto central da arquitetura de microsserviços, e muitas vezes erramos ao pensar que podemos ignorar algumas delas.
É prematuro falar que a adoção de monorepo no google seria um erro, entretanto, não está alinhado com as fundações da gerência de configuração. Ferramentas poderiam determinar equilibrar o esforço de gestão, como o docker, permitindo identificar claramente cenários onde não seria necessário rebuildar uma imagem. E consequentemente não seria absurdo determinar a partir daí se um serviço precisa ou não ser reimplantado. Mas ainda estamos falando de algo que não é natural, não é o docker quem deve determinar uma versão de uma aplicação, essa é a questão fundamental aqui sobre monorepo.
De qualquer forma toda essa independência traz sim a capacidade de destravar roadmaps, mas traz consigo um elemento importante: complexidade.
Em TODOS os cenários vamos ver demanda por observabilidade e se somarmos tudo, teremos um custo muito maior com microsserviços do que com monolitos.
Microsserviço é sim mais caro, entretanto para as empresas que têm seu roadmap travado por conta da arquitetura monolítica, é mais caro ter o roadmap travado do que pagar essa conta.
Se você levou à sério esse post talvez você esteja se questionando:
– Se o que acabei de ler for verdade, talvez microsserviço seja muito maior e talvez não seja para mim.
Bem, é isso que vou falar no próximo post da série.
0 comentários