Monolito modular ou microsserviços para um sistema de gestão que ficou lento

Escrito por

em

O cadastro de clientes cresceu, o relatório mensal já leva quarenta segundos para abrir e alguém sugeriu “migrar para microsserviços”. Antes de abrir esse debate, vale separar o que é problema de arquitetura do que é problema de consulta. A maioria dos sistemas de gestão que chega a esse ponto tem o segundo, e a solução custa uma semana, não um semestre.

O que o monolito bem feito ainda resolve

Um monolito modular com um único banco relacional entrega transação ACID entre pedido, estoque e financeiro sem nenhum esforço. Quando você parte isso em serviços, cada fronteira vira consistência eventual, filas, retentativas e idempotência. Para um ERP de médio porte, com dezenas de milhares de registros por dia e não bilhões, esse custo raramente se paga.

O gargalo costuma estar em três lugares: consultas sem índice adequado, N+1 no ORM e relatórios pesados rodando no mesmo banco que atende a operação. Os três se resolvem sem mudar a topologia.

Antes de dividir, meça

Ative o log de consultas lentas e deixe rodar uma semana. Ordene por tempo total, não por tempo individual: uma query de 80 ms chamada 200 mil vezes pesa mais do que uma de 4 segundos chamada duas vezes. Em Postgres, o EXPLAIN (ANALYZE, BUFFERS) mostra se o plano faz varredura sequencial onde deveria usar índice.

Na prática: CREATE INDEX CONCURRENTLY idx_pedidos_cliente_data ON pedidos (cliente_id, criado_em DESC); — o CONCURRENTLY evita travar escritas durante a criação, o que importa em produção.

O N+1 que o ORM esconde

É o defeito mais barato de corrigir e o que mais aparece em telas de listagem. A tela carrega 50 pedidos e, para cada um, o ORM dispara uma consulta extra para buscar o cliente, outra para os itens, outra para o status. Em desenvolvimento, com 20 registros, ninguém percebe. Em produção, a listagem faz 150 idas ao banco e a latência de rede entre aplicação e banco, mesmo de 1 ms, já soma 150 ms antes de qualquer processamento.

O diagnóstico é contar consultas por requisição, não olhar tempo. Se uma listagem executa mais de 10 consultas, desconfie. A correção é carregar as relações junto (select_related ou prefetch_related no Django, includes no Rails, joinedload no SQLAlchemy) ou montar uma consulta única com JOIN e agregação.

Na prática: Pedido.objects.select_related("cliente").prefetch_related("itens") reduz a listagem de 150 consultas para 3, sem tocar em banco, infraestrutura ou arquitetura.

Relatório pesado não deve disputar o banco da operação

Relatórios gerenciais fazem agregações sobre milhões de linhas e seguram memória e I/O. Rodando no mesmo primário que atende o caixa ou o pedido, eles derrubam a latência de todo mundo às nove da manhã de segunda, quando a diretoria abre o painel. Uma réplica de leitura resolve isso por uma fração do custo de qualquer redesenho. Duas ressalvas: a réplica tem atraso de replicação, normalmente de segundos, então relatório “em tempo real” de caixa precisa de decisão consciente; e consultas longas na réplica podem ser canceladas pelo mecanismo de conflito de replicação, o que exige ajustar max_standby_streaming_delay ou aceitar retentativas.

Para relatórios que ninguém precisa ver ao segundo, uma tabela de resumo recalculada de hora em hora, ou uma view materializada atualizada por agendamento, entrega tela instantânea. É um pouco de código e muito alívio.

Como extrair um módulo sem parar o resto

Quando os sinais acima aparecerem, o caminho que menos dá problema segue uma sequência. Defina a fronteira dentro do próprio monolito primeiro: o módulo passa a ser acessado só por uma interface interna, sem consultar suas tabelas diretamente de outros lugares. Isso por si só costuma levar um ou dois sprints e já mostra quais acoplamentos escondidos existem.

Depois, coloque uma fila entre o monolito e o módulo para o que for assíncrono (emissão, notificação, processamento). Rode os dois caminhos em paralelo por algumas semanas, comparando resultados. Só então mova o processamento para um serviço próprio, com banco próprio quando a fronteira de dados estiver limpa. Defina desde o início o contrato versionado: um campo novo opcional é compatível, remover ou renomear campo quebra consumidores.

O erro clássico é extrair o módulo com mais dependências primeiro, por ser o “mais importante”. Comece pelo mais periférico. Você aprende o custo operacional de ter um serviço (deploy, logs, alertas, tracing) num lugar onde errar é barato.

Cache só onde a leitura se repete

Cache esconde problema quando entra antes do diagnóstico. Depois de corrigir índice e N+1, olhe o que ainda é lido repetidamente com o mesmo resultado: tabela de preços, configuração por tenant, permissões do usuário. Um cache em memória da aplicação com expiração curta, de 30 a 120 segundos, já corta boa parte da carga sem introduzir infraestrutura nova. Redis entra quando há mais de uma instância da aplicação e a invalidação precisa ser coordenada. A pergunta que decide é: o que acontece se o usuário ver um dado de dois minutos atrás? Se a resposta for “nada”, cacheie. Se for “emitimos nota com preço errado”, não.

Comparativo: monolito modular x serviços separados

Critério Monolito modular Serviços separados
Transação entre domínios Nativa Saga ou consistência eventual
Deploy Um artefato Por serviço, com versionamento de contrato
Escala independente Só do conjunto Por serviço
Depuração Stack trace único Tracing distribuído obrigatório
Custo operacional Baixo Alto: observabilidade, filas, CI por serviço
Times necessários Um ou dois Vários, com dono por serviço

Quando a separação passa a fazer sentido

Há três sinais que justificam extrair um módulo. Primeiro, o módulo tem perfil de carga completamente diferente: emissão de nota fiscal em lote, processamento de imagem, envio de e-mail em massa. Segundo, equipes diferentes precisam publicar em cadências diferentes e o deploy conjunto virou fila. Terceiro, uma exigência de isolamento, como dados de pagamento sob escopo próprio de conformidade.

Nesses casos, extraia um só módulo, comece pela borda (o que menos depende do resto) e mantenha o banco compartilhado em modo somente leitura por um período, até confiar no contrato. Extrair tudo de uma vez é a forma mais confiável de ficar seis meses sem entregar funcionalidade.

Decisão

Um cenário hipotético para calibrar

Imagine um ERP de distribuidora com 25 mil pedidos por mês e relatório de comissões que demora 40 segundos. O time propõe extrair o módulo financeiro. A investigação mostra que a consulta de comissões faz uma varredura sequencial em uma tabela de itens de pedido sem índice por vendedor e data, e que o relatório roda no primário. Um índice composto e a mudança para a réplica levam o relatório a poucos segundos. O módulo financeiro continua onde estava, e a equipe passou um mês entregando funcionalidade em vez de reescrever infraestrutura. O exemplo é inventado, mas o padrão é recorrente: a causa fica na camada de dados com muito mais frequência do que na topologia.

Minha posição: trate a extração de serviço como a última opção da lista, não a primeira. A ordem que eu seguiria é medir, indexar, eliminar N+1, separar leitura de escrita, cachear o que for repetido e só então avaliar fronteiras de serviço. Cada etapa custa menos que a seguinte e, em geral, uma das primeiras já resolve. Registre o tempo das consultas críticas antes e depois de cada mudança, para que a próxima conversa sobre arquitetura use números e não impressão. Se mesmo assim a carga ou a organização do time pedirem separação, você chega à decisão com evidência e com um módulo candidato bem delimitado, o que reduz muito o risco da migração.

Se o sintoma é lentidão e o time tem menos de dez pessoas, fique no monolito modular, corrija índices e N+1, mova relatórios para uma réplica de leitura e coloque um cache onde o perfil de consulta mostrar leitura repetida. Se o sintoma é bloqueio entre times ou carga assimétrica em um módulo específico, extraia esse módulo. Microsserviços como ponto de partida para um sistema de gestão sem esses sintomas é custo antecipado por um problema que talvez nunca apareça.

Comentários

Deixe um comentário

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