Voltar para o blog

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/s

Durante picos:

1.500-2.000 req/s

A aplicação inicialmente apresentava:

p50: 650 ms
p95: 1.200 ms
p99: 2.800 ms

Ao aumentar o tráfego, a solução adotada pelo time era adicionar Pods.

6 pods
↓
10 pods
↓
15 pods
↓
20 pods

Mesmo 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 → response

Throughput

Quantidade de operações que o sistema consegue processar em determinado intervalo.

Exemplo:

2.000 requests por segundo

Uma 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 segundos

A 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 ms

Essa 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
   ↓
Resposta

O 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
   ↓
Response

Mesmo 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 ms

O tempo aproximado será:

100 + 150 + 180 = 430 ms

Poré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 ms

em vez de:

~430 ms

Isso 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 queries

Em 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 queries

para:

2 queries

8. Í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 Scan

sobre 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 resposta

10. Quarta refatoração: cache corretamente utilizado

Nem toda consulta precisa atingir PostgreSQL.

Imagine um endpoint:

GET /products/123

Os 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
        ↓
    resposta

Exemplo 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 segundos

ou 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ário

O usuário fica esperando operações que não precisam fazer parte da resposta.

Podemos modificar para:

Cliente
   ↓
orders-api
   ↓
PostgreSQL
   ↓
RabbitMQ
   ↓
HTTP 201

Enquanto isso:

RabbitMQ
   ├── pdf-worker
   ├── email-worker
   ├── erp-worker
   └── analytics-worker

Agora o caminho crítico da requisição é muito menor.

O endpoint pode cair de:

900 ms

para algo próximo de:

150-250 ms

dependendo 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-worker

O serviço possui inicialmente:

10 Pods

requests:
CPU:    500m
Memory: 512Mi

limits:
CPU:    1000m
Memory: 1Gi

Cada Pod processava aproximadamente:

100 req/s

antes que p95 ultrapassasse 1 segundo.

Capacidade aproximada:

10 × 100 req/s
=
1.000 req/s

Após as otimizações:

N+1 removido
cache implementado
queries indexadas
operações paralelizadas
trabalho assíncrono movido para RabbitMQ
connection pooling ajustado
payload reduzido

Cada Pod passa a processar aproximadamente:

400 req/s

Mantendo 10 Pods:

10 × 400
=
4.000 req/s

Por 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: 3

O 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: 1

permite 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: 512Mi

devem 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: 65

Nesse caso, o HPA tenta manter aproximadamente:

65% de CPU

e pode escalar entre:

4 e 30 Pods

O 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 pendente

15. 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_CONNECTION

Nesse 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ões

Se PostgreSQL suporta confortavelmente 300 conexões para aquele workload, o pool está superdimensionado.

Por isso:

pool por aplicação
×
quantidade máxima de Pods

deve 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 requests

Cada request tenta novamente três vezes:

10.000 × 3
=
30.000 tentativas adicionais

Se o banco já está sobrecarregado, isso pode piorar drasticamente o incidente.

Prefira:

timeout
+
retry limitado
+
exponential backoff
+
jitter
+
circuit breaker

Exemplo conceitual:

100 ms
↓
200 ms
↓
400 ms
↓
falha

Evite 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 OPEN

Essa 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_messages

Uma 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ária

22. 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 ms

Agora é 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 rate

Esses sinais normalmente são mais úteis que simplesmente:

pod running = true

24. Segurança

Performance não deve ser obtida removendo controles de segurança.

Secrets

Evite:

env:
  - name: DB_PASSWORD
    value: super-secret-password

Prefira 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: 5432

Essa 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 GiB

Se 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: 4Gi

se a aplicação utiliza:

CPU média: 300m
Memória média: 700Mi

O 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: 500m

Se 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_total

A 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ável

Para 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 simplificados

Essas 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 planning

Um 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 /health

e 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étricaAntesDepois
p50650 ms95 ms
p951.200 ms180 ms
p992.800 ms420 ms
Throughput1.000 req/s4.000 req/s
Queries/request274
Cache hit0%82%
CPU por Pod75%58%
Pods necessários208-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 wide

Observe:

STATUS
RESTARTS
NODE
READY

Verificar recursos

kubectl top pods -n production

Depois:

kubectl top nodes

CPU alta pode indicar saturação.

Memória crescente continuamente pode indicar leak.


Verificar descrição do Pod

kubectl describe pod <pod> -n production

Procure:

OOMKilled
Evicted
Unhealthy
FailedScheduling
Probe failed

Ver logs

kubectl logs <pod> -n production --since=15m

Caso exista mais de um container:

kubectl logs <pod> \
  -n production \
  -c orders-api \
  --since=15m

Ver eventos

kubectl get events \
  -n production \
  --sort-by=.metadata.creationTimestamp

Ver HPA

kubectl get hpa -n production

E:

kubectl describe hpa orders-api -n production

32. Diagnóstico por sintomas

CPU alta e latência alta

Possíveis causas:

serialização pesada
loops
criptografia
compressão
processamento síncrono
throttling

Investigue profiling de CPU.


CPU baixa e latência alta

Pode indicar espera em I/O:

PostgreSQL
Redis
RabbitMQ
API externa
DNS
rede

Tracing distribuído ajuda muito nesse cenário.


Muitas conexões PostgreSQL

Possíveis causas:

pool excessivo
connection leak
muitos Pods
queries lentas
transações longas

Verifique:

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:

  1. medir antes de otimizar;
  2. acompanhar p50, p95 e p99;
  3. utilizar distributed tracing;
  4. reduzir chamadas síncronas;
  5. eliminar N+1;
  6. criar índices baseados em consultas reais;
  7. limitar payloads;
  8. utilizar cache apenas onde houver benefício claro;
  9. mover operações não críticas para processamento assíncrono;
  10. configurar connection pools conscientemente;
  11. implementar timeouts;
  12. aplicar retry com backoff e jitter;
  13. limitar concorrência;
  14. utilizar autoscaling baseado em métricas coerentes;
  15. realizar load tests;
  16. definir SLOs;
  17. observar custo por requisição;
  18. 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 maior

Horizontal 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ços

També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/s

e uma consulta de 300 ms não afeta nenhum requisito do negócio, introduzir:

Redis
RabbitMQ
Kafka
service mesh
KEDA
tracing distribuído

pode aumentar significativamente a complexidade sem benefício proporcional.

Às vezes a melhor arquitetura ainda é:

Aplicação
↓
PostgreSQL

Arquitetura 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
↓
Autoscaling

O 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 outros

Agora 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
↓
Cloud

Performance 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.

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 conversa

Receba os novos artigos por e-mail

Um e-mail quando sair artigo novo sobre infraestrutura, backend e IA. Sem spam, cancele quando quiser.