De 1.2s para 180ms: como refatorar microsserviços para suportar 4x mais requisições
Reduzir latência não é apenas uma questão de tornar uma API mais rápida. Em arquiteturas distribuídas, melhorar o tempo de resposta pode reduzir consumo de CPU, diminuir conexões simultâneas, aliviar o banco de dados, reduzir filas internas, aumentar a capacidade de processamento por Pod e, consequentemente, diminuir custos de infraestrutura.
Neste artigo, vamos analisar um cenário de referência em que uma API baseada em microsserviços apresentava aproximadamente 1,2 segundo de latência média e, após uma série de refatorações, passou a responder próximo de 180 ms, suportando aproximadamente quatro vezes mais requisições com uma infraestrutura semelhante.
Os números são utilizados como estudo de caso técnico. Eles não representam um resultado automático ou universal: o ganho real depende da arquitetura, linguagem, banco de dados, padrão de tráfego e gargalos existentes.
A ideia central é demonstrar uma evolução completa:
Problema → Profiling → Refatoração → Banco → Cache → Concorrência → Kubernetes → Observabilidade → Produção
1. O problema: a API escalava infraestrutura, mas não escalava performance
Imagine uma plataforma SaaS executando centenas de milhares de operações por hora.
A arquitetura principal utiliza:
- Kubernetes;
- Amazon EKS;
- APIs em Node.js;
- PostgreSQL;
- Redis;
- RabbitMQ;
- Prometheus;
- Grafana;
- Loki;
- OpenTelemetry;
- KEDA e HPA para escalabilidade.
Uma API chamada orders-api recebe requisições para consulta e criação de pedidos.
Em horário normal:
500 req/sDurante picos:
1.500-2.000 req/sA aplicação inicialmente apresentava:
p50: 650 ms
p95: 1.200 ms
p99: 2.800 msAo aumentar o tráfego, a solução adotada pelo time era adicionar Pods.
6 pods
↓
10 pods
↓
15 pods
↓
20 podsMesmo assim, a latência permanecia alta.
O problema estava na suposição de que:
mais réplicas sempre significam mais capacidade.
Isso não é necessariamente verdade.
Se todos os Pods continuarem disputando o mesmo banco de dados, executando consultas ineficientes e mantendo operações bloqueantes, aumentar as réplicas pode simplesmente deslocar o gargalo.
2. O que significa refatorar microsserviços para performance
Refatorar um microsserviço para performance significa modificar sua implementação e, eventualmente, sua arquitetura interna sem necessariamente alterar seu comportamento funcional.
O objetivo não é apenas produzir código mais elegante.
O objetivo pode envolver:
- reduzir tempo de CPU;
- reduzir chamadas externas;
- eliminar consultas redundantes;
- diminuir quantidade de dados transferidos;
- reduzir round trips de rede;
- paralelizar operações independentes;
- remover processamento síncrono;
- melhorar gerenciamento de conexões;
- utilizar cache de maneira adequada;
- reduzir contenção em banco;
- otimizar serialização;
- evitar alocações desnecessárias;
- aumentar throughput.
Um dos conceitos mais importantes nesse processo é distinguir latência de throughput.
Latência
Tempo necessário para processar uma operação.
Exemplo:
request → 180 ms → responseThroughput
Quantidade de operações que o sistema consegue processar em determinado intervalo.
Exemplo:
2.000 requests por segundoUma API pode ter baixa latência e baixo throughput.
Da mesma forma, pode processar muitas operações por segundo, mas apresentar latências individuais elevadas.
O objetivo de uma arquitetura eficiente normalmente é equilibrar os dois.
3. Principais conceitos envolvidos
3.1 Latência p50, p95 e p99
Utilizar apenas a média pode esconder problemas.
Imagine:
90 requests → 100 ms
10 requests → 4 segundosA média pode parecer aceitável, mas 10% dos usuários terão uma experiência ruim.
Por isso utilizamos percentis.
p50
50% das requisições terminaram abaixo daquele valor.
p95
95% das requisições terminaram abaixo daquele valor.
p99
99% das requisições terminaram abaixo daquele valor.
Em produção, p95 e p99 frequentemente são mais úteis que a média.
4. Antes de refatorar: encontre o gargalo
Performance não deve ser otimizada com base em suposições.
O primeiro passo deve ser instrumentar a aplicação.
Podemos dividir aproximadamente uma requisição de 1,2 segundo:
API Gateway / Ingress 15 ms
Middleware 20 ms
Auth 45 ms
PostgreSQL 620 ms
Serviço externo 280 ms
Processamento 75 ms
Serialização 20 ms
Rede 25 ms
-----------------------------------
Total aproximado 1.100 msEssa análise muda completamente a estratégia.
Otimizar serialização de JSON de 20 ms para 10 ms dificilmente resolverá o problema se o banco estiver consumindo mais de 600 ms.
A primeira regra de otimização é:
otimize o componente que realmente está consumindo tempo.
5. Arquitetura inicial
O fluxo original poderia ser representado como:
Cliente
↓
Load Balancer
↓
Ingress Controller
↓
orders-api
↓
users-api
↓
products-api
↓
PostgreSQL
↓
API externa
↓
RespostaO problema dessa arquitetura é a quantidade de dependências síncronas.
Para responder uma única requisição, orders-api precisava esperar vários serviços.
Algo próximo de:
Request
↓
orders-api
↓
users-api ────────── 120 ms
↓
products-api ─────── 180 ms
↓
PostgreSQL ───────── 500 ms
↓
API externa ──────── 250 ms
↓
ResponseMesmo pequenas latências individuais se acumulam.
Esse efeito é chamado frequentemente de latency amplification.
6. Primeira refatoração: eliminar chamadas sequenciais
Considere o código:
const user = await getUser(userId);
const products = await getProducts(productIds);
const inventory = await getInventory(productIds);Se cada operação levar:
getUser 100 ms
getProducts 150 ms
getInventory 180 msO tempo aproximado será:
100 + 150 + 180 = 430 msPorém, se essas operações forem independentes, podem executar simultaneamente.
const [user, products, inventory] = await Promise.all([
getUser(userId),
getProducts(productIds),
getInventory(productIds),
]);Agora o tempo tende a ficar próximo da operação mais lenta:
~180 msem vez de:
~430 msIsso não significa que tudo deve ser paralelizado indiscriminadamente.
Se dezenas de consultas forem abertas simultaneamente, podemos apenas transferir o problema para PostgreSQL, Redis ou APIs externas.
Concorrência deve ser controlada.
7. Segunda refatoração: reduzir consultas ao banco
Um dos maiores problemas encontrados em microsserviços é o padrão N+1 queries.
Imagine:
const orders = await db.query(
`
SELECT *
FROM orders
WHERE customer_id = $1
`,
[customerId],
);
for (const order of orders.rows) {
order.items = await db.query(
`
SELECT *
FROM order_items
WHERE order_id = $1
`,
[order.id],
);
}Se o usuário possui 100 pedidos:
1 query para pedidos
+
100 queries para itens
=
101 queriesEm produção isso pode ser extremamente caro.
Uma alternativa é realizar uma consulta única:
SELECT
o.id,
o.created_at,
o.status,
oi.product_id,
oi.quantity,
oi.price
FROM orders o
LEFT JOIN order_items oi
ON oi.order_id = o.id
WHERE o.customer_id = $1
ORDER BY o.created_at DESC;Outra possibilidade é realizar duas consultas controladas:
SELECT *
FROM orders
WHERE customer_id = $1;e:
SELECT *
FROM order_items
WHERE order_id = ANY($1);Isso reduz:
101 queriespara:
2 queries8. Índices: poucos milissegundos podem virar segundos
Considere:
SELECT id, status, created_at
FROM orders
WHERE company_id = $1
AND status = $2
ORDER BY created_at DESC
LIMIT 50;Sem índice adequado, PostgreSQL pode executar:
Seq Scansobre milhões de registros.
Um índice possível seria:
CREATE INDEX CONCURRENTLY idx_orders_company_status_created
ON orders (company_id, status, created_at DESC);Em tabelas grandes, CONCURRENTLY reduz o impacto sobre operações de escrita durante a criação do índice.
Antes de adicionar índices, utilize:
EXPLAIN ANALYZE
SELECT id, status, created_at
FROM orders
WHERE company_id = 100
AND status = 'pending'
ORDER BY created_at DESC
LIMIT 50;O objetivo é entender o plano de execução real.
Adicionar índices indiscriminadamente também tem custo:
- ocupa armazenamento;
- aumenta trabalho em
INSERT; - aumenta trabalho em
UPDATE; - aumenta operações de manutenção;
- aumenta consumo de cache do banco.
9. Terceira refatoração: retornar apenas os dados necessários
Outro antipadrão comum é:
SELECT *
FROM orders;Tal consulta pode recuperar:
- campos de auditoria;
- JSONs grandes;
- metadados;
- descrições;
- dados que não serão utilizados.
Prefira:
SELECT
id,
customer_id,
status,
total,
created_at
FROM orders
WHERE company_id = $1
LIMIT 50;Menos dados significam:
menos leitura de disco
→ menos memória
→ menos tráfego de rede
→ menos serialização
→ menor tempo de resposta10. Quarta refatoração: cache corretamente utilizado
Nem toda consulta precisa atingir PostgreSQL.
Imagine um endpoint:
GET /products/123Os dados de produto mudam poucas vezes por dia, mas são consultados milhares de vezes por minuto.
Redis pode ser utilizado:
Request
↓
Redis
├── HIT → resposta
│
└── MISS
↓
PostgreSQL
↓
Redis SET
↓
respostaExemplo em Node.js:
async function getProduct(productId) {
const key = `product:${productId}`;
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
const result = await db.query(
`
SELECT id, name, price, status
FROM products
WHERE id = $1
`,
[productId],
);
if (!result.rows[0]) {
return null;
}
await redis.set(key, JSON.stringify(result.rows[0]), { EX: 300 });
return result.rows[0];
}Nesse exemplo, o cache expira em:
300 segundosou cinco minutos.
O TTL deve ser definido considerando:
- frequência de atualização;
- tolerância a dados temporariamente antigos;
- volume;
- memória;
- estratégia de invalidação.
Cache não é uma solução automática.
Um cache mal implementado pode criar:
- dados inconsistentes;
- cache stampede;
- uso excessivo de memória;
- hot keys;
- complexidade operacional.
11. Quinta refatoração: remover processamento pesado da requisição
Considere uma API que executa:
receber pedido
↓
salvar pedido
↓
gerar PDF
↓
enviar e-mail
↓
atualizar ERP
↓
registrar analytics
↓
responder usuárioO usuário fica esperando operações que não precisam fazer parte da resposta.
Podemos modificar para:
Cliente
↓
orders-api
↓
PostgreSQL
↓
RabbitMQ
↓
HTTP 201Enquanto isso:
RabbitMQ
├── pdf-worker
├── email-worker
├── erp-worker
└── analytics-workerAgora o caminho crítico da requisição é muito menor.
O endpoint pode cair de:
900 mspara algo próximo de:
150-250 msdependendo da operação.
12. Exemplo de produção
Imagine o seguinte ambiente:
Internet
↓
AWS ALB
↓
Ingress Controller
↓
EKS
↓
orders-api
├── Redis
├── PostgreSQL
└── RabbitMQ
├── notification-worker
├── billing-worker
└── integration-workerO serviço possui inicialmente:
10 Pods
requests:
CPU: 500m
Memory: 512Mi
limits:
CPU: 1000m
Memory: 1GiCada Pod processava aproximadamente:
100 req/santes que p95 ultrapassasse 1 segundo.
Capacidade aproximada:
10 × 100 req/s
=
1.000 req/sApós as otimizações:
N+1 removido
cache implementado
queries indexadas
operações paralelizadas
trabalho assíncrono movido para RabbitMQ
connection pooling ajustado
payload reduzidoCada Pod passa a processar aproximadamente:
400 req/sMantendo 10 Pods:
10 × 400
=
4.000 req/sPor isso, uma redução de latência pode resultar em aumento significativo de throughput sem necessariamente quadruplicar a infraestrutura.
13. Kubernetes: configuração da aplicação
Um Deployment de produção poderia utilizar:
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: production
spec:
replicas: 4
revisionHistoryLimit: 3
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: orders-api
template:
metadata:
labels:
app: orders-api
spec:
terminationGracePeriodSeconds: 30
containers:
- name: orders-api
image: registry.example.com/orders-api:2.4.0
ports:
- name: http
containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: PORT
value: "3000"
- name: DB_HOST
valueFrom:
secretKeyRef:
name: orders-api-database
key: host
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: orders-api-database
key: password
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
readinessProbe:
httpGet:
path: /health/ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: http
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3O que essa configuração faz
O Deployment mantém quatro instâncias da API e permite atualização progressiva sem indisponibilidade planejada.
O trecho:
maxUnavailable: 0
maxSurge: 1permite criar um Pod adicional antes de remover um Pod antigo.
As probes têm objetivos distintos.
Readiness
readinessProbe:indica se o Pod está pronto para receber tráfego.
Liveness
livenessProbe:indica se o processo continua saudável.
Os valores de recursos:
requests:
cpu: 500m
memory: 512Midevem ser definidos com base em métricas reais.
Não devem ser copiados para produção sem profiling.
14. Autoscaling com HPA
Depois de melhorar a aplicação, podemos utilizar Horizontal Pod Autoscaler.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: orders-api
namespace: production
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: orders-api
minReplicas: 4
maxReplicas: 30
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65Nesse caso, o HPA tenta manter aproximadamente:
65% de CPUe pode escalar entre:
4 e 30 PodsO stabilizationWindowSeconds de scale-down ajuda a impedir redução agressiva imediatamente após um pico.
Em produção, CPU nem sempre é a melhor métrica.
Para APIs, também podem ser utilizadas métricas como:
requests por segundo
requests concorrentes
latência
fila pendente15. Escalabilidade de workers com KEDA
Para processamento assíncrono, escalar por CPU pode ser pouco eficiente.
Um worker pode estar consumindo pouca CPU enquanto milhares de mensagens aguardam processamento.
Uma estratégia melhor é usar a profundidade da fila.
Exemplo com KEDA e RabbitMQ:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: orders-worker
namespace: production
spec:
scaleTargetRef:
name: orders-worker
pollingInterval: 15
cooldownPeriod: 120
minReplicaCount: 2
maxReplicaCount: 40
triggers:
- type: rabbitmq
metadata:
protocol: amqp
queueName: orders
mode: QueueLength
value: "50"
hostFromEnv: RABBITMQ_CONNECTIONNesse exemplo, o KEDA observa a fila orders.
À medida que o backlog cresce, novos workers podem ser adicionados.
O valor:
value: "50"deve ser calibrado conforme:
- duração média de processamento;
- capacidade do consumidor;
- limite do banco;
- capacidade do RabbitMQ;
- tempo aceitável de fila.
Escalar consumers sem considerar o downstream pode sobrecarregar PostgreSQL ou APIs externas.
16. Connection pooling
A abertura de conexões com banco possui custo.
Criar uma conexão PostgreSQL para cada requisição é uma estratégia ruim.
Utilize connection pool.
Exemplo:
const { Pool } = require("pg");
const pool = new Pool({
host: process.env.DB_HOST,
port: 5432,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20,
min: 2,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 3000,
});Mas existe um detalhe importante.
Imagine:
30 Pods
×
20 conexões
=
600 conexõesSe PostgreSQL suporta confortavelmente 300 conexões para aquele workload, o pool está superdimensionado.
Por isso:
pool por aplicação
×
quantidade máxima de Podsdeve fazer parte do capacity planning.
Em ambientes grandes, ferramentas como PgBouncer também podem ser utilizadas.
17. Timeout é obrigatório
Chamadas sem timeout podem prender recursos indefinidamente.
Exemplo:
const response = await fetch(url, {
signal: AbortSignal.timeout(2000),
});Aqui a chamada externa possui timeout de dois segundos.
Isso evita que uma dependência lenta consuma:
- conexão;
- memória;
- event loop;
- worker;
- slot de processamento;
por tempo indefinido.
18. Retries: necessários, mas perigosos
Retry pode aumentar resiliência, mas também transformar uma falha pequena em uma avalanche.
Imagine:
10.000 requestsCada request tenta novamente três vezes:
10.000 × 3
=
30.000 tentativas adicionaisSe o banco já está sobrecarregado, isso pode piorar drasticamente o incidente.
Prefira:
timeout
+
retry limitado
+
exponential backoff
+
jitter
+
circuit breakerExemplo conceitual:
100 ms
↓
200 ms
↓
400 ms
↓
falhaEvite retries infinitos.
19. Circuit breaker
Se uma dependência está apresentando falhas repetidas, continuar enviando requisições pode ser contraproducente.
Um circuit breaker funciona aproximadamente assim:
CLOSED
↓
falhas ultrapassam limite
↓
OPEN
↓
requisições bloqueadas temporariamente
↓
HALF-OPEN
↓
teste
↓
CLOSED ou OPENEssa abordagem protege:
- o próprio microsserviço;
- a dependência;
- conexões;
- threads;
- event loop;
- banco;
- recursos de rede.
20. Observabilidade: descubra onde os 1,2 segundos estão sendo gastos
Sem observabilidade, performance tuning vira tentativa e erro.
Uma estratégia madura utiliza três pilares.
Métricas
Prometheus pode coletar:
http_requests_total
http_request_duration_seconds
process_cpu_seconds_total
process_resident_memory_bytes
database_query_duration_seconds
redis_operations_total
rabbitmq_queue_messagesUma métrica especialmente importante:
histogram_quantile(
0.95,
sum(
rate(
http_request_duration_seconds_bucket[5m]
)
) by (le)
)Essa consulta calcula aproximadamente o p95 de latência usando métricas histogram do Prometheus.
21. Logs
Logs devem fornecer contexto suficiente sem expor informações sensíveis.
Exemplo:
{
"level": "info",
"service": "orders-api",
"method": "GET",
"route": "/orders/:id",
"status": 200,
"duration_ms": 174,
"trace_id": "184f4bc...",
"request_id": "req-78a..."
}O Loki pode centralizar esses dados.
Evite registrar:
password
token
JWT completo
cartão
credenciais
PII desnecessária22. Distributed tracing
Em microsserviços, tracing distribuído é uma das ferramentas mais úteis.
Com OpenTelemetry podemos visualizar:
GET /orders/123 184 ms
│
├── authentication 12 ms
│
├── Redis GET 3 ms
│
├── PostgreSQL SELECT 41 ms
│
├── inventory-service 82 ms
│ └── PostgreSQL SELECT 38 ms
│
└── serialization 6 msAgora é possível responder objetivamente:
Onde estão os 184 ms?
Sem tracing, a API pode parecer lenta quando, na verdade, o problema está em outro microsserviço.
23. Alertas
Alertas não devem observar apenas disponibilidade.
Um serviço pode estar tecnicamente UP, mas praticamente inutilizável.
Considere alertas para:
p95 latency > 500 ms
p99 latency > 1 s
HTTP 5xx > 2%
DB pool saturation > 80%
RabbitMQ backlog > limite
Redis memory > limite
CPU throttling
OOMKilled
Pod restart rateEsses sinais normalmente são mais úteis que simplesmente:
pod running = true24. Segurança
Performance não deve ser obtida removendo controles de segurança.
Secrets
Evite:
env:
- name: DB_PASSWORD
value: super-secret-passwordPrefira Kubernetes Secrets integrados a soluções como:
- AWS Secrets Manager;
- External Secrets Operator;
- HashiCorp Vault.
ServiceAccount
Cada aplicação deve utilizar identidade própria.
Evite compartilhar credenciais administrativas.
Em AWS EKS, workloads podem utilizar mecanismos de identidade associados aos Pods para acessar serviços AWS seguindo o princípio do menor privilégio.
NetworkPolicy
Quando suportado pelo CNI utilizado, restrinja comunicação entre workloads.
Exemplo:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: orders-api
namespace: production
spec:
podSelector:
matchLabels:
app: orders-api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress
ports:
- protocol: TCP
port: 3000
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: database
ports:
- protocol: TCP
port: 5432Essa política é apenas um exemplo estrutural.
Em produção, é necessário considerar também:
- DNS;
- Redis;
- RabbitMQ;
- APIs externas;
- observabilidade;
- chamadas entre namespaces.
Uma NetworkPolicy incompleta pode interromper o serviço.
25. FinOps: performance também reduz custo
Performance e FinOps estão diretamente relacionados.
Considere:
Antes:
20 Pods
1 vCPU
1 GiB
Depois:
8 Pods
1 vCPU
1 GiBSe a mesma demanda puder ser processada com menos Pods, será possível reduzir:
- quantidade de nodes;
- CPU provisionada;
- memória provisionada;
- tráfego interno;
- conexões de banco;
- custos de compute.
Por outro lado, requests inadequados podem impedir consolidação.
Exemplo:
requests:
cpu: "2"
memory: 4Gise a aplicação utiliza:
CPU média: 300m
Memória média: 700MiO scheduler reserva muito mais capacidade do que o necessário.
Isso gera desperdício.
Requests devem considerar:
- uso histórico;
- p95;
- p99;
- comportamento de pico;
- overhead;
- segurança operacional.
Não basta utilizar a média.
26. CPU limit e throttling
Considere:
resources:
requests:
cpu: 500m
limits:
cpu: 500mSe a aplicação precisar utilizar temporariamente mais CPU, poderá sofrer throttling.
Isso pode gerar:
mais latência
↓
mais requisições simultâneas
↓
mais filas internas
↓
mais latênciaÉ importante acompanhar métricas como:
container_cpu_cfs_throttled_seconds_totalA estratégia de CPU limits depende do contexto e das políticas da plataforma.
Não existe um único valor correto para todos os workloads.
27. Spot e On-Demand
Depois de melhorar a aplicação, a infraestrutura também pode ser otimizada.
Workloads tolerantes a interrupções podem utilizar Spot.
Exemplo:
Workers assíncronos
→ Spot
APIs críticas
→ combinação Spot + On-Demand
Banco
→ capacidade estávelPara utilizar Spot corretamente, considere:
- múltiplos tipos de instância;
- múltiplas zonas;
- PodDisruptionBudget;
- graceful shutdown;
- retry;
- filas;
- distribuição de Pods;
- fallback para On-Demand.
Performance não deve depender de apenas um tipo específico de máquina.
28. Desenvolvimento x produção
Desenvolvimento
Pode ser aceitável utilizar:
1 réplica
cache local
logs DEBUG
sem autoscaling
PostgreSQL compartilhado
timeouts simplificadosEssas condições facilitam desenvolvimento e testes.
Produção
Normalmente será necessário avaliar:
múltiplas réplicas
PodDisruptionBudget
autoscaling
health checks
timeouts
circuit breaker
connection pool
cache distribuído
observabilidade
tracing
NetworkPolicy
Secret Manager
backup
multi-AZ
graceful shutdown
capacity planningUm comportamento que funciona com cinco requests por segundo localmente pode falhar com milhares de requisições por segundo em produção.
29. Load testing
Antes de afirmar que o serviço suporta quatro vezes mais tráfego, é necessário testar.
Uma ferramenta possível é k6.
Exemplo:
import http from "k6/http";
import { check } from "k6";
export const options = {
stages: [
{ duration: "2m", target: 200 },
{ duration: "5m", target: 500 },
{ duration: "5m", target: 1000 },
{ duration: "5m", target: 2000 },
{ duration: "2m", target: 0 },
],
thresholds: {
http_req_failed: ["rate<0.01"],
http_req_duration: ["p(95)<500"],
},
};
export default function () {
const response = http.get("https://api.example.com/orders");
check(response, {
"status is 200": (r) => r.status === 200,
});
}Esse teste aumenta progressivamente a quantidade de Virtual Users.
Porém, performance testing deve representar o padrão real de uso.
Não adianta testar apenas:
GET /healthe concluir que o sistema suporta 20 mil requests por segundo.
É necessário reproduzir:
- autenticação;
- leitura;
- escrita;
- cache hit;
- cache miss;
- queries;
- mensageria;
- payloads reais.
30. Comparando antes e depois
Um possível resultado:
| Métrica | Antes | Depois |
|---|---|---|
| p50 | 650 ms | 95 ms |
| p95 | 1.200 ms | 180 ms |
| p99 | 2.800 ms | 420 ms |
| Throughput | 1.000 req/s | 4.000 req/s |
| Queries/request | 27 | 4 |
| Cache hit | 0% | 82% |
| CPU por Pod | 75% | 58% |
| Pods necessários | 20 | 8-12 |
Esses números devem ser medidos no ambiente específico.
O ponto principal não é exatamente chegar a 180 ms.
O objetivo é entender por que o serviço estava lento e remover trabalho desnecessário do caminho crítico.
31. Troubleshooting
Quando a latência começar a aumentar, analise por camadas.
Verificar Pods
kubectl get pods -n production -o wideObserve:
STATUS
RESTARTS
NODE
READYVerificar recursos
kubectl top pods -n productionDepois:
kubectl top nodesCPU alta pode indicar saturação.
Memória crescente continuamente pode indicar leak.
Verificar descrição do Pod
kubectl describe pod <pod> -n productionProcure:
OOMKilled
Evicted
Unhealthy
FailedScheduling
Probe failedVer logs
kubectl logs <pod> -n production --since=15mCaso exista mais de um container:
kubectl logs <pod> \
-n production \
-c orders-api \
--since=15mVer eventos
kubectl get events \
-n production \
--sort-by=.metadata.creationTimestampVer HPA
kubectl get hpa -n productionE:
kubectl describe hpa orders-api -n production32. Diagnóstico por sintomas
CPU alta e latência alta
Possíveis causas:
serialização pesada
loops
criptografia
compressão
processamento síncrono
throttlingInvestigue profiling de CPU.
CPU baixa e latência alta
Pode indicar espera em I/O:
PostgreSQL
Redis
RabbitMQ
API externa
DNS
redeTracing distribuído ajuda muito nesse cenário.
Muitas conexões PostgreSQL
Possíveis causas:
pool excessivo
connection leak
muitos Pods
queries lentas
transações longasVerifique:
SELECT
state,
COUNT(*)
FROM pg_stat_activity
GROUP BY state;Banco consumindo CPU alta
Verifique queries mais pesadas por meio das ferramentas de observabilidade do banco e, quando disponível, pg_stat_statements.
Não tente corrigir imediatamente adicionando CPU.
Primeiro descubra quais queries estão consumindo recursos.
33. Boas práticas
Uma estratégia de performance madura deve seguir uma sequência semelhante:
- medir antes de otimizar;
- acompanhar p50, p95 e p99;
- utilizar distributed tracing;
- reduzir chamadas síncronas;
- eliminar N+1;
- criar índices baseados em consultas reais;
- limitar payloads;
- utilizar cache apenas onde houver benefício claro;
- mover operações não críticas para processamento assíncrono;
- configurar connection pools conscientemente;
- implementar timeouts;
- aplicar retry com backoff e jitter;
- limitar concorrência;
- utilizar autoscaling baseado em métricas coerentes;
- realizar load tests;
- definir SLOs;
- observar custo por requisição;
- validar performance após cada mudança.
34. O que evitar
Aumentar Pods sem investigar
latência alta
↓
adiciona Pods
↓
mais conexões
↓
mais carga no banco
↓
latência ainda maiorHorizontal scaling não resolve automaticamente gargalos downstream.
Criar índices para todas as colunas
Índices possuem custo.
Utilize planos de execução e comportamento real das queries.
Cachear tudo
Cache aumenta complexidade e pode introduzir inconsistência.
Retry infinito
Pode causar retry storm.
Remover CPU limits apenas para mascarar problemas
Recursos de Kubernetes devem ser definidos conscientemente, não utilizados para esconder uma implementação ineficiente.
Executar operações lentas dentro da requisição sem necessidade
Envio de e-mail, geração de relatórios, sincronizações e analytics geralmente podem ser candidatos ao processamento assíncrono.
Fazer benchmark em ambiente irreal
Um benchmark executado sem banco real, sem autenticação, sem cache misses e sem dependências não representa produção.
35. Quando utilizar essa abordagem
Esse tipo de refatoração é especialmente indicado quando existem:
APIs de alto volume
SaaS multi-tenant
marketplaces
e-commerce
plataformas financeiras
telemetria
streaming de eventos
integrações corporativas
processamento em Kubernetes
arquiteturas de microsserviçosTambém faz sentido quando:
- a infraestrutura cresce mais rápido que o tráfego;
- adicionar Pods deixou de resolver;
- p95 ou p99 estão elevados;
- PostgreSQL começa a saturar;
- muitos serviços dependem uns dos outros sincronamente;
- o custo por requisição está alto.
36. Quando não utilizar
Nem toda aplicação precisa de Redis, RabbitMQ, circuit breaker, Kubernetes e dezenas de microsserviços.
Se uma aplicação recebe:
5 req/se uma consulta de 300 ms não afeta nenhum requisito do negócio, introduzir:
Redis
RabbitMQ
Kafka
service mesh
KEDA
tracing distribuídopode aumentar significativamente a complexidade sem benefício proporcional.
Às vezes a melhor arquitetura ainda é:
Aplicação
↓
PostgreSQLArquitetura deve ser proporcional ao problema.
37. Resumo prático
Sair de uma latência próxima de 1,2 segundo para aproximadamente 180 ms não depende de uma configuração específica do Kubernetes.
É normalmente resultado da combinação de diversas melhorias:
Profiling
↓
Queries melhores
↓
Índices corretos
↓
Menos round trips
↓
Operações concorrentes
↓
Cache
↓
Processamento assíncrono
↓
Connection pooling
↓
Timeouts
↓
Observabilidade
↓
AutoscalingO erro mais comum é começar pela infraestrutura.
Adicionar Pods antes de entender a aplicação pode apenas aumentar o custo.
Uma abordagem mais eficiente começa perguntando:
onde estamos gastando os 1.200 ms?Depois da instrumentação, podemos descobrir algo como:
500 ms PostgreSQL
250 ms API externa
180 ms chamadas sequenciais
100 ms processamento
70 ms outrosAgora existe um plano de otimização baseado em dados.
Se, depois da refatoração, uma requisição permanece ativa por apenas 180 ms em vez de 1,2 segundo, recursos permanecem ocupados por menos tempo.
Isso pode permitir que o mesmo Pod processe muito mais requisições.
Conclusão
Performance de microsserviços não deve ser tratada exclusivamente como um problema de infraestrutura.
Kubernetes consegue adicionar Pods. Karpenter pode adicionar nodes. HPA consegue escalar Deployments. KEDA pode aumentar workers.
Nenhuma dessas ferramentas, entretanto, corrige automaticamente uma query que faz full scan em milhões de registros, uma API que realiza 100 consultas para montar uma resposta ou um microsserviço que espera sequencialmente cinco dependências externas.
Uma estratégia moderna de DevOps, SRE e Platform Engineering precisa analisar o sistema completo:
Código
↓
Runtime
↓
Banco
↓
Cache
↓
Mensageria
↓
Rede
↓
Kubernetes
↓
CloudPerformance deve ser tratada como característica arquitetural.
Primeiro, medimos.
Depois, identificamos o caminho crítico.
Em seguida, removemos processamento desnecessário, diminuímos round trips, otimizamos consultas, paralelizamos aquilo que pode ser paralelizado, movemos operações secundárias para filas e aplicamos cache onde realmente existe benefício.
Somente depois dimensionamos autoscaling e infraestrutura.
O resultado mais importante de uma refatoração desse tipo não é simplesmente transformar 1,2 segundo em 180 ms.
É transformar uma arquitetura que dependia de aumentar infraestrutura para sobreviver aos picos em uma plataforma capaz de utilizar melhor cada CPU, cada conexão, cada Pod e cada node disponível.
Esse é o ponto em que otimização de aplicação, SRE, Kubernetes, arquitetura e FinOps deixam de ser disciplinas isoladas e passam a trabalhar sobre o mesmo objetivo: aumentar a capacidade do sistema sem aumentar proporcionalmente sua complexidade e seu custo.
Artigos relacionados
Quer aplicar isso no seu ambiente?
Agende uma conversa de 30 minutos sem compromisso. Eu olho o seu cenário e digo por onde começar.
Agendar uma conversaReceba os novos artigos por e-mail
Um e-mail quando sair artigo novo sobre infraestrutura, backend e IA. Sem spam, cancele quando quiser.