Os custos dos microsserviços (2020)
(robertovitillo.com)- O backend geralmente começa como um monólito, mas quando várias equipes de funcionalidade passam a mexer na mesma base de código, o acoplamento aumenta e a velocidade de mudança pode cair
- Microsserviços são uma forma de dividir serviços por função de negócio para obter deploy independente e autonomia de equipes pequenas
- Se os serviços ficarem pequenos demais, a carga operacional cresce mais do que os benefícios, e o nome “micro” pode até atrapalhar o julgamento
- Uma adoção total traz uma ampla gama de custos: padronização, provisionamento de recursos, proteção para chamadas remotas, CI/CD, operação e até consistência eventual
- Em geral, é mais seguro aprender bem os limites dentro de um monólito e separar aos poucos quando as dores do crescimento ficarem maiores; uma abordagem microservices-first faz sentido quando já há experiência e uma plataforma preparada
Limitações que aparecem quando o monólito cresce
- O backend de uma aplicação JavaScript moderna de página única normalmente começa oferecendo uma API HTTP RESTful e usando um banco de dados relacional como um único serviço web stateless
- Esse serviço é composto por componentes ou bibliotecas que implementam diferentes funções de negócio
- Quanto mais equipes de funcionalidade participam da mesma base de código, maior tende a ser o acoplamento entre componentes e mais fácil fica haver sobreposição entre áreas de trabalho das equipes
- A produtividade cai, e fica mais difícil prever o impacto das mudanças
- Quando a base de código fica complexa o suficiente, ninguém mais entende tudo por completo, então implementar novas funcionalidades e corrigir bugs passa a levar muito tempo
- Mesmo que o backend esteja dividido em bibliotecas por equipe, mudar uma biblioteca no fim ainda leva ao redeploy do serviço
- Se entrar um bug como vazamento de memória, o serviço inteiro pode ser afetado
- Se um build com problema for revertido, a velocidade de todas as equipes é impactada, não só da equipe que introduziu o bug
A autonomia trazida pelos limites entre serviços
- Uma forma de reduzir as dores de crescimento de um backend monolítico é dividir o backend em um conjunto de serviços com deploy independente
- Os serviços se comunicam por APIs, e APIs criam limites mais difíceis de violar do que os limites entre componentes dentro do mesmo processo
- Essa arquitetura é chamada de arquitetura de microsserviços, mas a expressão
micropode induzir ao erro- Os serviços não precisam necessariamente ser pequenos
- Serviços com funcionalidade demais limitada podem gerar mais carga operacional do que benefícios
- Um nome mais apropriado poderia ser service-oriented architecture, mas esse nome também carrega bagagem antiga
Velocidade gerada por equipes pequenas e serviços independentes
- Ao dividir o backend em serviços por função de negócio, cada serviço pode ser desenvolvido e operado por uma pequena equipe única
- Equipes pequenas são mais eficazes porque o custo de comunicação aumenta ao quadrado conforme o tamanho da equipe
- Quando cada equipe define seu próprio cronograma de releases e controla sua base de código, a coordenação entre equipes diminui e a tomada de decisão fica mais rápida
- Se a base de código do serviço for pequena, ela é mais fácil de entender pelos desenvolvedores e o tempo de adaptação de novos contratados também diminui
- Bases de código menores ajudam a reduzir lentidão em IDEs, o que também melhora a produtividade do desenvolvedor
- Graças a limites fortes entre serviços, o desenvolvedor só precisa entender uma pequena parte do backend para alterá-la, e não o sistema inteiro
- Cada serviço pode escalar de forma independente e escolher a stack tecnológica mais adequada às próprias necessidades
- Quem consome a API não precisa se preocupar com como a funcionalidade foi implementada internamente
- Fica mais fácil experimentar e avaliar novas tecnologias sem afetar outras áreas do sistema
- Cada microsserviço pode ter seu próprio modelo de dados e seu próprio armazenamento de dados
- O desenvolvedor pode alterar o schema sem impactar outros serviços
Os custos reais adicionados pelos microsserviços
- Microsserviços adicionam muito mais partes móveis ao sistema como um todo, e esse custo não é grátis
- O custo de adotar microsserviços por completo só vale a pena quando pode ser amortizado de forma distribuída entre dezenas de equipes de desenvolvimento
-
Experiência de desenvolvimento
- É possível usar linguagens, bibliotecas e armazenamentos de dados diferentes em cada serviço, mas isso pode dificultar a manutenção da aplicação
- Se as stacks de software forem totalmente diferentes, fica mais difícil para um desenvolvedor migrar de uma equipe para outra
- Para oferecer funcionalidades comuns necessárias a todos os serviços, como logging, é preciso manter bibliotecas para cada linguagem adotada
- É necessário algum nível de padronização
- É possível incentivar de forma leve o uso de certas tecnologias oferecendo uma boa experiência de desenvolvimento para equipes que sigam a linguagem e o portfólio tecnológico recomendados
-
Provisionamento de recursos
- Para suportar muitos serviços independentes, é preciso conseguir criar com facilidade servidores, armazenamentos de dados e outros recursos genéricos
- Não se deve deixar cada equipe inventar seu próprio jeito de criar recursos
- Os recursos provisionados também precisam ser reconfigurados, e lidar com isso exige bastante automação
-
Comunicação
- Chamadas remotas têm custo alto e introduzem novas formas de o sistema falhar
- Para se proteger contra falhas, são necessários mecanismos defensivos como timeouts, retries e circuit breakers
- Para reduzir a perda de desempenho da comunicação em rede, também é preciso usar processamento assíncrono e batching
- Esses fatores aumentam a complexidade do sistema
- Monólitos também podem sofrer problemas parecidos por causa do acesso de clientes remotos e do uso de APIs de terceiros, mas em escala menor
-
Integração contínua, entrega contínua e deploy
- Integração contínua garante que alterações de código sejam mescladas na branch principal depois que builds e testes automáticos forem executados
- Depois que a alteração é mesclada, ela deve ser automaticamente publicada e implantada em um ambiente parecido com produção
- Nesse ambiente, devem ser executados testes de integração e end-to-end para verificar se o microsserviço não quebra outros serviços dos quais depende
- Testar um microsserviço individual não é mais difícil do que testar um monólito, mas o teste de integração de todos os microsserviços é muito mais difícil
- Quando serviços individuais interagem, podem surgir comportamentos muito sutis e inesperados
-
Operação
- Diferentemente de um monólito, alocar uma equipe de operações separada para cada equipe responsável por um serviço custa muito mais caro
- Normalmente, a equipe que desenvolve o serviço também assume o on-call dele
- Em cada sprint, a equipe precisa decidir o que priorizar entre trabalho de desenvolvimento e carga operacional, e isso gera atrito
- Depurar falhas do sistema também fica mais difícil
- Não dá para subir a aplicação inteira na máquina local e depurá-la passo a passo
- Como há mais partes móveis, também há mais formas de falha
- Logging e monitoramento de qualidade tornam-se importantes em todos os níveis
-
Consistência eventual
- Quando a aplicação é dividida em vários serviços, o modelo de dados deixa de existir apenas em um único armazenamento de dados
- Atualizar atomicamente registros em armazenamentos de dados diferentes e garantir consistência forte é lento, caro e difícil de implementar
- Essa arquitetura normalmente precisa aceitar consistência eventual
Quando separar
- Dividir uma aplicação em serviços aumenta bastante a complexidade do sistema como um todo
- Em geral, é melhor começar com um monólito e só quebrá-lo quando houver motivo suficiente para isso
- Como é difícil definir corretamente os limites entre serviços, costuma ser mais fácil mover esses limites dentro do monólito até encontrar o ponto adequado
- Quando o monólito estiver maduro o suficiente e as dores do crescimento começarem a ficar grandes, será possível extrair microsserviços um a um
- Uma abordagem microservices-first só é adequada quando já existe experiência prévia, e quando a plataforma necessária já foi construída ou o tempo para construí-la foi levado em conta
1 comentários
Opiniões do Hacker News
Concordo em geral com o texto, mas, vendo pelo outro lado, há casos em que vale a pena arcar com o custo: você pode isolar problemas de compatibilidade com dependências de terceiros antigas em um serviço separado e evitar uma refatoração dolorosa ou uma reimplementação própria; impedir que restrições de deploy de um sistema crítico de alta disponibilidade bloqueiem o deploy de outros sistemas; e isolar restrições de design que grandes clientes não conseguem mudar
Não quer dizer que seja grátis; quer dizer que pode ser um custo que vale pagar para não transformar um monólito simples em um monte de lixo, não deixá-lo balançando com deploys arriscados, ou não ficar preso a um parceiro de negócios com uma stack técnica pior
Microsserviços não são a abordagem de fazer com que os componentes internos sejam, por padrão, serviços em vez de bibliotecas ou classes?
Se um serviço de inferência de machine learning for em Python, a UI com renderização server-side em Rails e um serviço com muito I/O e concorrência em Go, a carga extra de separar esses três em serviços distintos pode ser justificada
Eu até gosto de microsserviços, mas otimização prematura e escala prematura podem ser tão ruins quanto a dívida técnica que surge quando depois é preciso otimizar em uma direção completamente diferente
O momento de extrair algo para um serviço separado é quando surge um problema que não pode ser resolvido de forma administrável sem fazer isso
Aumentar a complexidade arquitetural para forçar limites não pode ser a solução para falta de disciplina organizacional, mas empresas de tecnologia de médio porte tratam isso assim o tempo todo. Se limites de domínio fracos estão causando problemas, extrair serviços provavelmente não vai resolver bem
Citação: https://www.oreilly.com/library/view/software-architects-han...
CI/CD que bloqueia PRs até os testes passarem também é uma solução técnica cara para resolver um problema de disciplina organizacional
Todo mundo aceita como válido que a resolução de nomes de domínio seja feita por um serviço externo, e quase ninguém integra um resolver DNS recursivo e cache ao monólito. Mas essa separação de responsabilidades, mesmo sendo antiga, não costuma ser aceita como exemplo
O problema parece ser que, quanto mais “bom” é o funcionário, mais vontade ele tem de demonstrar valor, o que leva a overengineering e, no fim, vira um inferno
Microsserviços
grug se pergunta por que cérebros grandes pegam o problema mais difícil, o de decompor corretamente um sistema, e ainda colocam chamadas de rede no meio
para grug isso parece muito confuso
https://grugbrain.dev/#grug-on-microservices
“Queremos modernizar a abordagem de FOO_TABLE por SCALE_REASONS e migrar de MySQL para DynamoDB, mas 32 das 59 equipes acessam FOO_TABLE diretamente ou chamam diretamente métodos private da nossa classe. Como as prioridades são diferentes, essas equipes não conseguem migrar para usar FOO_SERVICE nem mudar suas consultas para usar tabelas particionadas. Agora escalar FOO_TABLE vira um trabalho de vários trimestres, e precisamos permitir que as equipes atualizem devagar. Talvez daqui a 1 ou 2 anos possamos aposentar a abordagem antiga que está pegando fogo agora. Até lá, aproveite o on-call”
Com microsserviços, a equipe percebe que a tabela não escala, mas os dados são fornecidos por uma API. No sprint seguinte, planeja e executa a migração, e os usuários da API sentem que ficou muito mais rápido
Se você abrir o REPL do Ruby moderno, já obtém, em termos de autocomplete e verificação de sintaxe, boa parte do que esperaria de uma IDE para linguagem tipada estaticamente
Isolamento não deve ser visto como algo trivial; é um dos recursos mais poderosos que se pode colocar em software. Pode melhorar desempenho, criar limites de falha e fornecer limites de segurança
Isso é o mesmo conceito fundamental do modelo de atores. Em vez de dois componentes compartilharem e modificarem a memória um do outro, dois sistemas isolados, como atores ou microsserviços, comunicam-se apenas por meio de um protocolo definido
Acho que as pessoas entendem modularidade de forma errada. Modularidade é importante, mas passei a pensar que existe outro princípio arquitetural importante: o princípio do vórtice único
Em um sistema de software, um vórtice é um loop no fluxo de dados. Se você envia dados para algum lugar e recebe de volta o resultado processado, ou se é afetado por esses dados de alguma forma, existe um vórtice. Uma variável mutável é um exemplo de vórtice bem pequeno
O princípio do vórtice único significa que, idealmente, um sistema de software deveria ter apenas um vórtice; em outras palavras, todos os componentes deveriam saber em que direção o seu vórtice gira
Quando se juntam módulos que formam dois vórtices, a composição é fácil se as direções dos vórtices combinam; se forem opostas, fica complicado porque é preciso decidir qual será a direção do novo vórtice. Por isso, o melhor é que os vórtices de todos os módulos tenham a mesma direção e formem um único vórtice
Esse princípio generaliza ideias como o padrão Flux, CQRS, event sourcing e imutabilidade
Ou algo inesperado pode acontecer em A e acabar bloqueando C. Idealmente, um pai deveria chamar apenas seus filhos e não depender deles; se isso falha, a arquitetura também falhou
Ao olhar para um diagrama de arquitetura, metade da batalha é entender o que é assíncrono/síncrono, push/pull, e a direção e a ordem do fluxo
Barramentos de serviços corporativos e filas de mensagens parecem soluções para criar vórtices onde as duas direções não se encaixam
Há um trecho que diz que o problema é “o número de bibliotecas que precisam ser mantidas para fornecer funcionalidades comuns necessárias a todos os serviços, como logging, uma para cada linguagem adotada”
Esse é o maior motivo pelo qual abandonamos microserviços. Com as ferramentas de 2023, especialmente em ambientes puramente cloud native ou FaaS de escala infinita, gastar largura de banda mental com essa preocupação é um completo desperdício. Isso vale ainda mais quando a base de clientes é formada por bancos e instituições financeiras, que examinam duramente cada dependência de terceiros
Hoje operamos com uma única distribuição binária monolítica em .NET, com cerca de 250 MB comprimida com gzip, e não há nenhum sinal de rachadura. Se você está preocupado, de forma minuciosa, com uma distribuição SaaS de 10 a 100 MB dizendo coisas como “meu exe não cabe mais no L2”, pode ficar tranquilo. A jornada do software monolítico ainda nem começou
Se chegar o momento de reescrever uma dessas pilhas de lixo, não seria muito mais fácil ter tudo em um único lugar, com cada commit globalmente consistente?
Microserviços não significam terra sem lei, nem que cada equipe possa agir sem considerar a organização como um todo. Pode e deve haver regras para manter algum grau de consistência entre equipes
O melhor código é aquele que é fácil jogar fora e reescrever. Na prática, código antigo passa por mais mãos, piora mais e, acima de tudo, ninguém quer mantê-lo
Quem prefere microserviços diz que é mais fácil alterar ou reescrever porque há um contrato claro sobre como o serviço deve funcionar e porque a base de código daquele serviço é muito menor
O lado do monólito diz que é mais fácil alterar ou reescrever porque tudo é um grande novelo, então, para mudar um fio, basta saber onde esse fio se cruza com os outros
Não sei quem está certo. É provável que o monólito seja melhor para veteranos que passaram anos olhando a base de código no microscópio, e microserviços sejam melhores para os demais
Do ponto de vista de gerenciamento de releases, a migração de um monólito para microserviços muitas vezes acontece pelos motivos errados
O único motivo realmente válido para a mudança parece ser um problema de escalabilidade, como um gargalo de desempenho. O resto é apenas transferir a complexidade do desenvolvimento de software para a manutenção do sistema
Claro que os desenvolvedores vão gostar, porque diminui bastante a necessidade de se coordenar com outras equipes. Mas deixar claro quando e como uma funcionalidade é implementada, devidamente testada em todos os microserviços, ativada em produção e gerenciada de forma centralizada fica muito mais difícil se a comunicação entre equipes de desenvolvimento não for madura o suficiente. E essa imaturidade muitas vezes é justamente o motivo real para dividir o monólito em primeiro lugar
Por outro lado, lidar com vários microserviços com poucos desenvolvedores e um contexto bem definido é sofrido
A afirmação de que “o resto é transferir a complexidade do desenvolvimento de software para a manutenção do sistema” faz sentido se o software estiver em desenvolvimento ativo. Desenvolvimento é caro. O custo de manter sistemas distribuídos para uma equipe grande pode ser menor do que o custo de desenvolver um monólito enorme; no fim, depende da situação
Onde está o texto “O custo dos monólitos”? Não vejo isso aqui. É só porque todo mundo está implementando microsserviços de forma ignorante que só esse problema aparece
Se todo mundo estivesse implementando monólitos de forma ignorante, haveria uma quantidade absurda de textos do tipo “monólitos são ruins”
As pessoas não entendem que os dois sistemas acabam gerando a mesma quantidade de problemas. É a diferença entre fazer um elefante trabalhar e fazer 1000 ratos trabalharem. Ambos terão problemas; os problemas só serão diferentes
Escolher um dos dois não faz os problemas desaparecerem. Qualquer lado vai ter problemas, e você precisa escolher um e fornecer soluções. Se a arquitetura for o maior obstáculo de uma empresa, eu gostaria de trabalhar lá
Se um monólito estiver bem decomposto, em que ele difere de microsserviços colocados no mesmo lugar? Provavelmente só na interface. Chamadas de função viram RPC, e você aceita um pouco de overhead para obter benefícios como tratar componentes separadamente, por exemplo para aplicar patches
Em que microsserviços distribuídos diferem de microsserviços no mesmo local? O deploy é mais complexo, mas você consegue posicionar os processos de forma inteligente em hardware mais adequado. Os modos de falha aumentam, mas você consegue maior tolerância a falhas
Não há resposta universal aqui. Se você precisa dos benefícios, pague o custo. Vejo esses debates de microsserviços versus monólitos como resultado de aplicar mal um padrão, da falta de ferramentas que facilitariam a vida ou, na maioria das vezes, de continuar sem refatorar e redesenhar um software de 10 anos até ele ficar doloroso de trabalhar, independentemente da arquitetura inicial
Dizer que “chamadas de função viram RPC” parece simples, mas no momento em que isso acontece o app cliente precisa lidar com todos estes caminhos de execução: servidor DNS inacessível, falha ao resolver hostname, host inacessível, conexão recusada, autenticação recusada, certificado TLS não confiável, limite de taxa excedido, 301 movido permanentemente, atraso na preparação do resultado e timeout
Mesmo em uma aplicação bem decomposta, para lidar com isso de forma robusta provavelmente será necessário redesenhá-la para permitir filas assíncronas, limitação da taxa de requisições, cache de resultados, retries com backoff e logging, credenciais de cliente configuráveis, trust stores configuráveis, URLs de recursos configuráveis e registro de falhas
Em chamadas de função locais, também serão necessárias funções RPC e parâmetros adicionais para fornecer dados que antes estavam no contexto. Além disso, a UI do monólito talvez precise comunicar ao usuário latências e falhas de rede que antes eram impossíveis
Refatorar até o monólito mais bem feito para microsserviços exige um esforço considerável
É seguro tentar novamente essa chamada RPC? E backoff exponencial? O load balancer da API está funcionando corretamente? Então também é preciso monitoramento. Como fazer tracing entre microsserviços? Agora passa a ser necessário algo como OpenTelemetry
Quanto mais difícil é depurar com breakpoints em uma arquitetura de microsserviços? Como reverter uma transação de banco de dados entre dois microsserviços? Uma simples chamada de função de repente fica muito mais complexa
Toda vez que defendem esse conceito, deixam de fora do cálculo a fragmentação criada pela exigência de que os serviços sejam muito pequenos. Mais concretamente: se você tem um monólito e um microsserviço, isso não são microsserviços. A palavra microsserviços implica que tudo está dividido em pequenos serviços e que não há monólito
A maior parte da argumentação pró-microsserviços desmorona no momento em que se percebe que eles precisam necessariamente ser “micro”. E, se você abandona essa exigência, não sobra nenhuma proposta nova ou profunda
Do ponto de vista operacional, dizer que “o deploy é mais complexo” é um eufemismo enorme. Cada pacote é algo que precisa ser gerenciado separadamente, com características e problemas próprios
Assim como desenvolvedores front-end acham que não dá para criar uma “aplicação web moderna” sem frameworks ou bibliotecas de front-end como React/Vue/Svelte, hoje parece circular a ideia de que um “monólito” é como executar uma massa de piche gigantesca e assustadora em uma única instância, que “não escala”; isso não faz sentido
Outra coisa que se nota é que a quantidade total de código fica muito maior, e a maioria dos serviços acaba sendo 20% código de negócio/domínio e 80% tratamento de envio e recebimento de mensagens de rede. Por mais que você esconda, isso continua lá, e de algum modo é preciso lidar com a rede
Assim como a loucura do front-end, esse culto aos microsserviços só vai acabar quando a economia quebrar e não houver mais dinheiro para sustentar essas torres de Babel da perdição
Dito isso, microsserviços também têm seu lugar. Eles fazem sentido dentro de algumas empresas que obtêm mais valor do que o custo
Há benefícios técnicos, como escalabilidade independente por serviço, mas o maior benefício é estabelecer fronteiras claras de propriedade e evitar acoplamento excessivo desnecessário
Se uma equipe pequena está criando um único produto, é provável que o problema organizacional que microsserviços resolvem ainda não exista, então talvez eles não sejam necessários. Nesse caso, microsserviços são otimização prematura: você está pagando um custo para resolver um problema que ainda não existe
Indo além, a complexidade de front-end está toda na cabeça: https://bower.sh/front-end-complexity
O texto parece partir do pressuposto de que a organização de desenvolvimento está completamente quebrada e fora de controle. Coisas como não decidir o que fazer durante o sprint, introduzir às escondidas bibliotecas de terceiros e linguagens desconhecidas, e implantar silenciosamente em produção mudanças incompatíveis
Se os desenvolvedores não forem idiotas, microsserviços também parecem ser mais fáceis
Tenho certeza de que microsserviços seriam mais fáceis se você trabalhasse em uma equipe formada por desenvolvedores inteligentes e competentes. Claro que, nesse caso, tudo ficaria mais fácil