Arquitetura de software
Microsserviços: fronteiras de serviço, deploy independente e o custo real
Intermediário12 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
Microsserviços trocam complexidade interna por complexidade distribuída, e a troca só compensa quando a organização precisa entregar de forma independente. Este guia define o que o estilo realmente exige, mostra como encontrar fronteiras que resistam ao contato com funcionalidades reais, compara os estilos de comunicação e apresenta um retrato realista da conta operacional e do caminho de saída de um monólito.
O que você vai aprender
- Enunciar as propriedades que um sistema precisa ter antes de poder ser chamado honestamente de microsserviços
- Encontrar fronteiras de serviço a partir de capacidades de negócio, e não de camadas técnicas
- Escolher entre comunicação síncrona e orientada a eventos para uma determinada interação
- Estimar o investimento operacional que uma plataforma de microsserviços exige antes de se comprometer com ela
- Planejar uma extração incremental a partir de um monólito com a abordagem strangler fig
Microsserviços costumam ser apresentados como uma forma de construir software, mas são mais bem entendidos como uma forma de organizar a entrega. A descrição técnica (serviços pequenos, cada um em seu próprio processo, comunicando-se pela rede) é a parte fácil, e descreve muitos sistemas que não tiram nenhum proveito disso. O que faz o estilo valer a pena é uma propriedade que não tem nada a ver com tamanho: qualquer serviço pode ser alterado, testado, entregue e revertido sem coordenação com os times donos dos outros.
Essa propriedade é cara. Ela exige pipelines automatizados por serviço, tolerância a falhas parciais em todo chamador, observabilidade que atravesse processos e um desenho de dados em que dois serviços nunca mexem nas mesmas linhas. Times que adotam o vocabulário sem pagar pela propriedade acabam com um monólito distribuído: todos os saltos de rede dos microsserviços, toda a coordenação de entregas de um monólito e uma experiência de depuração pior do que a de ambos. Este guia trata de como conquistar essa propriedade com honestidade, e dos casos em que um único artefato de deploy bem modularizado é a melhor resposta de engenharia.
O que o estilo realmente exige
Cinco compromissos separam microsserviços de "várias aplicações que conversam entre si".
Uma unidade de deploy por serviço, entregue no seu próprio ritmo. Se publicar uma mudança no serviço de preços exige uma entrega sincronizada do serviço de catálogo, os dois são uma única unidade de mudança fantasiada de dois contêineres. A capacidade de entregar de forma independente é o teste decisivo, e é ela que você deve medir.
Dados privados. Um serviço é dono do seu estado persistente e é o único processo que o lê ou escreve. Os outros serviços só enxergam esse estado pela interface do serviço ou pelos dados que ele publica deliberadamente. É a regra mais violada e aquela cuja violação é mais difícil de desfazer.
Um contrato que sobrevive à sua implementação. Os consumidores dependem de uma interface publicada (uma API HTTP, um schema de mensagem), não de uma biblioteca compartilhada de classes de entidade. Evolução retrocompatível é um requisito de design, não uma aspiração.
Isolamento de falhas. Toda chamada remota pode ser lenta, falhar ou devolver algo inesperado. Os chamadores definem timeouts, limitam a concorrência e degradam de forma elegante. Sem isso, um sistema de doze serviços com 99,9% de disponibilidade cada um é menos confiável do que o monólito que ele substituiu.
Um time dono do serviço de ponta a ponta. As mesmas pessoas projetam, constroem, fazem o deploy e dão suporte ao serviço. A lei de Conway faz disso menos uma preferência do que uma previsão: uma organização cujos times são divididos por camada técnica vai produzir serviços divididos por camada técnica, diga o diagrama de arquitetura o que disser.
Encontrando fronteiras que se sustentam
Fronteiras ruins são a falha característica dos projetos de microsserviços, e quase sempre são traçadas por linhas técnicas: um "serviço de banco de dados", um "serviço de validação", um "serviço de orquestração". Cada um deles participa de todos os casos de uso, então toda funcionalidade mexe em todos os serviços e nada pode ser entregue sozinho.
Fronteiras que se sustentam são traçadas em torno de capacidades de negócio: coisas que o negócio faz e que têm nome, dono e ciclo de vida próprios. Pedidos, precificação, estoque, faturamento, rastreamento de entregas, identidade do cliente. O vocabulário e a técnica de modelagem vêm do domain-driven design, em que um bounded context é exatamente a unidade que um serviço deve implementar.
Três testes práticos ajudam a avaliar uma fronteira candidata:
| Teste | Pergunta | Como é uma resposta ruim |
|---|---|---|
| Coesão da mudança | Quando chega uma funcionalidade típica, quantos serviços mudam? | Quase todos, toda vez |
| Posse dos dados | Este serviço consegue responder às suas consultas principais a partir do próprio armazenamento? | Precisa de um join entre dois outros serviços |
| Vocabulário | Os termos usados dentro dele têm um significado único e consistente? | "Pedido" significa quatro coisas diferentes em um único serviço |
O teste de coesão da mudança é o mais revelador, porque é possível medi-lo depois. Pegue as últimas vinte funcionalidades concluídas e conte quantos serviços cada uma tocou. Numa distribuição saudável, a maioria das funcionalidades cai em um ou dois serviços. Se a mediana for cinco, as fronteiras estão erradas, e adicionar mais serviços vai piorar a situação.
Fronteiras também têm uma dimensão temporal. A divisão certa para um produto com 30.000 usuários raramente é a certa com 3 milhões, e serviços às vezes são fundidos. Trate uma fronteira como uma hipótese com prazo de validade, e não como uma decisão permanente, e mantenha baixo o custo de fundir dois serviços conservando interfaces estreitas.
Dados por serviço, e o que fazer com as consultas
Dar a cada serviço seu próprio schema é simples até o primeiro relatório precisar de dados de quatro deles. Todas as respostas disponíveis são trade-offs, e escolher mal aqui é o que empurra os times de volta para um banco de dados compartilhado.
Composição de APIs. Um chamador (muitas vezes o gateway ou um agregador dedicado) consulta vários serviços e faz o join em memória. É simples, correto e funciona bem para um punhado de registros. Degrada muito quando é preciso filtrar ou ordenar entre serviços, porque não dá para paginar um join que você mesmo calcula.
Modelos de leitura replicados. Cada serviço publica eventos quando seus dados mudam; os serviços interessados mantêm uma cópia local e desnormalizada, moldada para as suas consultas. As leituras ficam rápidas e locais; o custo é a consistência eventual e o código que mantém a projeção. É a forma geral do padrão CQRS descrito no guia de padrões de design de microsserviços.
Um armazenamento analítico separado. Relatórios e business intelligence não deveriam rodar contra os serviços operacionais. Faça o streaming das mudanças para um data warehouse ou data lake e deixe os analistas fazerem todos os joins que quiserem por lá.
A regra que mantém isso administrável: um serviço pode publicar seus dados, mas nunca deixa outro serviço acessar diretamente o seu armazenamento. Uma réplica de leitura exposta a outros times é um banco compartilhado com etapas extras: o schema vira público e congela.
Estilos de comunicação
Duas famílias, cada uma adequada a interações diferentes.
Requisição/resposta síncrona (HTTP ou gRPC) é a escolha certa quando o chamador não consegue continuar sem a resposta: verificar se um cliente existe antes de criar um pedido, validar um token. É simples de entender e fácil de rastrear. O perigo é o acoplamento temporal (a disponibilidade do chamador passa a ser o produto da disponibilidade de todos os serviços da cadeia) e uma latência que se acumula salto a salto. Mantenha as cadeias rasas: dois níveis de profundidade são administráveis; cinco são um stack trace distribuído esperando para dar timeout.
Eventos assíncronos publicam um fato e deixam as partes interessadas reagirem. "OrderPlaced" é emitido uma única vez; estoque, faturamento e notificações fazem cada um a sua parte no próprio ritmo. Quem publica não sabe nem se importa com quem está ouvindo, então adicionar um consumidor não exige nenhuma mudança upstream, e um consumidor fora do ar atrasa o trabalho em vez de fazê-lo falhar. Os custos são a consistência eventual, uma depuração de ponta a ponta mais difícil e a necessidade de os consumidores lidarem com entregas duplicadas. O funcionamento de brokers, ordenação e garantias de entrega é tratado em padrões e garantias de mensageria.
Uma heurística útil: use chamadas síncronas para consultas e eventos assíncronos para mudanças de estado que se propagam. Comandos que precisam ser confirmados para um usuário que está esperando continuam síncronos; tudo aquilo por que o usuário não precisa esperar vira um evento.
// Synchronous: the order cannot be priced without this answer.
CustomerTier tier = customerClient.tierFor(customerId); // timeout: 300 ms, fallback: STANDARD
// Asynchronous: nobody waits for stock, invoicing or email.
events.publish(new OrderPlaced(order.id(), order.lines(), order.total(), Instant.now()));
Seja qual for o estilo, o versionamento importa mais do que a escolha do protocolo. Acrescente campos, nunca mude o significado dos existentes; tolere campos desconhecidos no lado do consumidor; e mantenha o formato antigo de uma mensagem legível pelo menos pelo tempo que o consumidor mais lento leva para se atualizar.
A conta operacional
O esforço de engenharia que uma plataforma de microsserviços exige é subestimado com frequência, porque não aparece no diagrama de arquitetura. Antes de se comprometer, coloque tudo no orçamento.
- Um pipeline por serviço. Build, testes, varredura de segurança, deploy em cada ambiente, rollback. Multiplique pelo número de serviços; se criar um serviço novo custa uma semana de encanamento de pipeline, os times vão evitar criar serviços e enfiar funcionalidades nos que já existem. É por isso que pipelines de entrega contínua são um pré-requisito, e não um projeto para depois.
- Tracing distribuído e logs correlacionados. Sem um trace ID propagado por todos os saltos, diagnosticar "o checkout está lento" significa ler doze arquivos de log na mão.
- Métricas e alertas centralizados por serviço, com objetivos de nível de serviço (SLOs) que descrevam o comportamento visível para o usuário, e não o uso de CPU.
- Plataforma de execução. Orquestração de contêineres, service discovery, gestão de segredos, rotação de certificados, políticas de rede.
- Testes de contrato. Testes de contrato orientados ao consumidor pegam a mudança incompatível antes que ela chegue a um ambiente onde diagnosticá-la custa um dia.
- Plantão e runbooks por time, porque o time que entrega é o time que é acionado.
Uma verificação rápida de sanidade: se a organização não consegue fazer o deploy do monólito atual em produção sob demanda, em menos de uma hora, com testes automatizados em que confia, ela não está pronta para operar quinze serviços. Microsserviços amplificam a maturidade de um processo de entrega; eles não a fornecem.
Quando o monólito é a resposta certa
Um único artefato de deploy é a escolha certa com mais frequência do que as palestras de conferência sugerem. Ele é o certo quando o time é pequeno o bastante para caber em uma única daily, quando o domínio ainda não é compreendido o bastante para traçar fronteiras estáveis, quando o tráfego do produto cresce de maneira uniforme e não em pontos quentes, e quando a consistência transacional em todo o modelo é um requisito real, e não um hábito.
A versão que vale a pena buscar é o monólito modular: um processo, um deploy, mas módulos internos com fronteiras impostas. Cada módulo tem sua própria árvore de pacotes, seu próprio schema (ou pelo menos suas próprias tabelas, sem chaves estrangeiras entre módulos), uma interface pública de poucos tipos e uma verificação em tempo de build que quebra o build quando um módulo mexe nas entranhas de outro. O sistema de módulos do Java, regras do ArchUnit ou simplesmente módulos de build separados funcionam.
| Aspecto | Monólito modular | Microsserviços |
|---|---|---|
| Deploy | Um pipeline, um artefato | Um pipeline por serviço |
| Refatoração entre fronteiras | Com ajuda do compilador, minutos | Entregas coordenadas, semanas |
| Consistência | Transações locais | Sagas e consistência eventual |
| Escala | A aplicação inteira de uma vez | Por serviço |
| Raio de impacto de uma falha | O processo inteiro | Um serviço, se estiver isolado |
| Autonomia dos times | Trem de releases compartilhado | Independente |
| Depuração | Um único stack trace | Trace distribuído |
O monólito modular também é o ponto de partida honesto para um sistema que você pretende dividir mais tarde, porque os módulos mostram onde ficam as costuras. Dividir um monólito bem modularizado é um trabalho mecânico; dividir um monólito emaranhado é arqueologia.
Migrando com o strangler fig
Reescritas de sistemas grandes fracassam por motivos bem conhecidos: o negócio continua mudando o sistema antigo enquanto o novo é construído, e a virada concentra um risco enorme em um único momento. A abordagem strangler fig evita as duas coisas extraindo uma capacidade por vez, roteando o tráfego para a nova implementação e deixando o sistema antigo encolher até poder ser removido.
Coloque uma camada de roteamento na frente
Posicione um gateway ou reverse proxy entre os clientes e o monólito, para que o tráfego possa ser redirecionado por caminho sem mudar os clientes. Veja gestão de APIs e gateways.Escolha uma capacidade que valha a pena extrair
Escolha uma com fronteira clara, dor real (muda com frequência ou escala de forma diferente) e pouco emaranhamento de dados. Não comece pela mais difícil nem pela mais central.Separe os dados primeiro
Identifique as tabelas que pertencem à capacidade, remova do monólito os joins e as chaves estrangeiras entre schemas e substitua-os por chamadas ou eventos publicados. Costuma ser a etapa mais longa, e a que determina o sucesso.Construa o serviço e rode os dois
Implemente o novo serviço e rode-o ao lado do código antigo (tráfego sombra, ou escrita nos dois com comparação) até que o comportamento seja o mesmo.Mude o tráfego e depois apague
Mude a rota, acompanhe as métricas, mantenha o rollback disponível por uma semana e depois apague o código antigo. É pulando essa exclusão que as organizações acabam mantendo duas implementações para sempre.
Acompanhe dois números durante todo o processo: a fatia do tráfego atendida pelos serviços extraídos e o número de funcionalidades que ainda exigem uma mudança no monólito. Se o segundo não estiver caindo, a extração está produzindo serviços sem produzir autonomia.
Erros comuns
Dividir por camada técnica. Um "serviço de UI", um "serviço de negócio" e um "serviço de dados" recriam as camadas de uma única aplicação pela rede, garantindo que toda funcionalidade toque os três.
Começar com serviços demais. Times que iniciam um produto novo com vinte serviços estão chutando fronteiras antes de entender o domínio. Comece com dois ou três serviços de granularidade grossa, ou com um monólito modular, e divida quando uma pressão específica exigir.
Bibliotecas compartilhadas com lógica de domínio. Um JAR comum com a entidade Order dentro amarra todos os serviços a um mesmo ciclo de entrega. Compartilhe utilitários se for inevitável; nunca compartilhe o modelo.
Cadeias síncronas para tudo. Cinco serviços no caminho de uma requisição multiplicam a latência e dividem a disponibilidade. Leve o trabalho para eventos sempre que o usuário não precisar esperar.
Nenhuma disciplina de versionamento. Renomear um campo JSON quebra consumidores que você nunca conheceu. Apenas mudanças aditivas, com janelas de depreciação explícitas.
Ignorar a conta da plataforma. Optar por microsserviços sem financiar pipelines, tracing e plantão produz serviços que ninguém consegue operar e que todos têm medo de colocar em produção.
Principais conclusões
- A propriedade que define os microsserviços é o deploy independente; se os serviços precisam ser entregues juntos, a distribuição não traz nada e custa muito.
- Trace fronteiras em torno de capacidades de negócio e valide-as contando quantos serviços uma funcionalidade típica toca.
- Cada serviço é dono dos seus dados de forma privada; consultas entre serviços são resolvidas por composição, modelos de leitura replicados ou um armazenamento analítico separado, nunca por um schema compartilhado.
- Use chamadas síncronas quando o chamador precisa de uma resposta e eventos para mudanças de estado que se propagam, e trate o versionamento de interfaces como uma preocupação de design de primeira classe.
- O investimento operacional (um pipeline por serviço, tracing, métricas, testes de contrato, plantão) é o custo real e precisa ser financiado antes da primeira divisão.
- Um monólito modular entrega a maior parte do benefício de design sem a complexidade distribuída, e é o melhor ponto de partida para uma futura extração com strangler fig.
Fontes e leituras complementares
Perguntas frequentes
- Qual deve ser o tamanho de um microsserviço?
- Tamanho é a medida errada. Um serviço deve ser dono de uma capacidade de negócio por completo, incluindo seus dados, e pequeno o bastante para que o time responsável consiga reescrevê-lo em poucas semanas, se precisar. Contar linhas de código ou impor um número máximo de arquivos produz serviços que não conseguem responder a uma pergunta sem chamar quatro vizinhos, o que é muito pior do que um serviço que simplesmente é grande.
- Vários microsserviços podem compartilhar o mesmo banco de dados?
- Tecnicamente podem, e o resultado é um monólito distribuído. Qualquer mudança de schema vira uma entrega coordenada, a query lenta de um serviço deixa outro sem recursos e ninguém consegue mais saber quem escreve o quê. Se dois serviços realmente precisam da mesma tabela, isso é um forte indício de que eles são um serviço só. Compartilhe dados por meio de interfaces publicadas ou de modelos de leitura replicados.
- Microsserviços deixam um sistema mais rápido?
- Quase nunca no nível de uma requisição individual. Transformar uma chamada de método dentro do processo em uma chamada de rede acrescenta serialização, transporte e tratamento de falhas, normalmente um milissegundo ou mais a cada vez. O que os microsserviços trazem é a escala independente dos componentes, de modo que os três endpoints que mais consomem CPU possam receber mais capacidade sem pagar pela aplicação inteira, e velocidade de entrega para times que não ficam mais na fila uns dos outros.
- Um monólito modular é um passo para trás?
- Muitas vezes é o destino certo. Um único artefato de deploy com fronteiras de módulo aplicadas com rigor, schemas separados por módulo e nenhum SQL entre módulos entrega a maior parte do benefício de design sem nenhum dos custos da distribuição. Ele também deixa a opção em aberto: módulos com fronteiras limpas podem ser extraídos mais tarde, quando uma pressão específica (escala independente, cadência de entrega própria, tecnologia diferente) justificar.