Metodologia9 min

Performance em Containers: otimizando aplicações containerizadas

Containers adicionam camadas de abstração que impactam performance. Aprenda a configurar e otimizar aplicações em Docker e Kubernetes.

Containers revolucionaram como deployamos aplicações, mas essa abstração tem custo. Configurações inadequadas podem transformar uma aplicação performática em um gargalo. Este artigo explora como otimizar performance em ambientes containerizados.

Container não é máquina virtual. Otimizar como se fosse é o primeiro erro.

O Impacto dos Containers na Performance

Overhead real

Aplicação nativa:     100 RPS baseline
Aplicação em Docker:  95-98 RPS (2-5% overhead)
Aplicação em K8s:     90-95 RPS (5-10% overhead)

O overhead é pequeno, mas erros de configuração amplificam drasticamente:

Container mal configurado: 50-70 RPS
→ Problema não é o container, é a configuração

Fontes de overhead

  1. Rede: overlay networks, NAT, iptables
  2. Storage: copy-on-write, volumes
  3. CPU: cgroups, scheduling
  4. Memória: limites, OOM killer

Configurando Recursos Corretamente

CPU: requests vs limits

resources:
  requests:
    cpu: "500m"      # Garantido pelo scheduler
  limits:
    cpu: "1000m"     # Máximo permitido

Erros comuns:

# ❌ Limite muito baixo - throttling constante
limits:
  cpu: "100m"

# ❌ Sem limite - pode monopolizar nó
limits:
  cpu: null

# ❌ Request = Limit alto - desperdício
requests:
  cpu: "2000m"
limits:
  cpu: "2000m"

Configuração recomendada:

# ✅ Request baseado em uso médio, limit em picos
resources:
  requests:
    cpu: "250m"      # Uso médio observado
  limits:
    cpu: "1000m"     # Headroom para picos

CPU Throttling

Quando container atinge limite de CPU, sofre throttling:

Sem throttling: latência p99 = 50ms
Com throttling: latência p99 = 500ms (10x pior)

Como detectar:

# Métricas do container
cat /sys/fs/cgroup/cpu/cpu.stat
# nr_throttled: número de vezes throttled
# throttled_time: tempo total em nanosegundos

Prometheus query:

rate(container_cpu_cfs_throttled_seconds_total[5m])

Memória: o equilíbrio delicado

resources:
  requests:
    memory: "512Mi"
  limits:
    memory: "1Gi"

O problema do OOM Killer:

Memória usada > Limit → OOM Kill → Pod reinicia
→ Conexões perdidas, requisições em voo falham

Configuração segura:

# ✅ Headroom de 20-30% acima do uso normal
resources:
  requests:
    memory: "512Mi"   # Uso médio
  limits:
    memory: "768Mi"   # +50% headroom

JVM em Containers

JVMs antigas não respeitam limites de container:

# Container com 1GB de limit
# JVM antiga vê 64GB da máquina host
# Aloca heap de 16GB → OOM Kill instantâneo

Solução:

# Use JVM 11+ que respeita cgroups
FROM eclipse-temurin:17-jre

# Ou configure explicitamente
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0"

Configuração recomendada para JVM:

env:
  - name: JAVA_OPTS
    value: >-
      -XX:MaxRAMPercentage=75.0
      -XX:InitialRAMPercentage=50.0
      -XX:+UseG1GC
      -XX:+UseContainerSupport

Otimização de Rede

Service mesh overhead

Sem service mesh:  latência = 5ms
Com Istio sidecar: latência = 8-12ms (+60-140%)

Quando vale a pena:

  • Observabilidade distribuída
  • mTLS obrigatório
  • Traffic management complexo

Quando evitar:

  • Latência ultra-baixa crítica
  • Alto volume de requests internos
  • Simplicidade é prioridade

DNS lookup

Cada request pode fazer DNS lookup:

Request → DNS lookup (2-5ms) → Connection → Response

Otimização:

# Configurar dnsPolicy
spec:
  dnsPolicy: ClusterFirst
  dnsConfig:
    options:
      - name: ndots
        value: "2"    # Reduz lookups desnecessários

Connection pooling

# ❌ Nova conexão por request
# TCP handshake + TLS = 50-100ms por request

# ✅ Pool de conexões
# Reutiliza conexões estabelecidas

Configuração de pool:

// Node.js com pool de conexões
const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000
});

Otimização de Storage

Tipos de storage e performance

emptyDir (memory): ~500MB/s
emptyDir (disk):   ~100MB/s
hostPath:          ~100MB/s
PersistentVolume:  ~50-100MB/s (depende do provider)

Copy-on-Write overhead

Layers de imagem usam CoW:

# ❌ Muitos layers = muito CoW
RUN apt-get update
RUN apt-get install -y package1
RUN apt-get install -y package2

# ✅ Menos layers
RUN apt-get update && \
    apt-get install -y package1 package2 && \
    rm -rf /var/lib/apt/lists/*

Logs e performance

# ❌ Logs para stdout sem limite
# Disk I/O cresce indefinidamente

# ✅ Limitar logs do container
apiVersion: v1
kind: Pod
spec:
  containers:
  - name: app
    # Configurar log rotation no runtime

Docker daemon config:

{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  }
}

Kubernetes-Specific Optimizations

Pod Disruption Budget

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: app-pdb
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: my-app

Topology Spread

spec:
  topologySpreadConstraints:
  - maxSkew: 1
    topologyKey: topology.kubernetes.io/zone
    whenUnsatisfiable: DoNotSchedule
    labelSelector:
      matchLabels:
        app: my-app

Readiness vs Liveness

# Liveness: app está viva?
livenessProbe:
  httpGet:
    path: /health/live
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

# Readiness: app pode receber tráfego?
readinessProbe:
  httpGet:
    path: /health/ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Erro comum:

# ❌ Readiness probe muito pesado
readinessProbe:
  httpGet:
    path: /health  # Checa DB, cache, APIs externas
  periodSeconds: 1  # A cada segundo!
# = DDoS em si mesmo

# ✅ Readiness leve e frequência adequada
readinessProbe:
  httpGet:
    path: /health/ready  # Check básico
  periodSeconds: 5

Graceful Shutdown

spec:
  terminationGracePeriodSeconds: 30
  containers:
  - name: app
    lifecycle:
      preStop:
        exec:
          command: ["/bin/sh", "-c", "sleep 5"]
// Aplicação deve tratar SIGTERM
process.on('SIGTERM', async () => {
  console.log('Received SIGTERM, shutting down gracefully');
  await server.close();
  await db.close();
  process.exit(0);
});

Imagem Docker Otimizada

Multi-stage build

# Build stage
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Runtime stage
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .
USER node
CMD ["node", "server.js"]

Imagem base

# ❌ Imagem pesada
FROM ubuntu:22.04  # ~77MB

# ✅ Imagem otimizada
FROM alpine:3.18   # ~7MB

# ✅ Distroless (ainda menor, mais seguro)
FROM gcr.io/distroless/nodejs:18  # ~40MB, sem shell

Startup time

Imagem grande (500MB):   pull = 30-60s
Imagem otimizada (50MB): pull = 3-6s

Monitoramento de Performance

Métricas essenciais

# Container-level
- container_cpu_usage_seconds_total
- container_memory_usage_bytes
- container_network_receive_bytes_total
- container_fs_reads_bytes_total

# Application-level
- http_request_duration_seconds
- http_requests_total
- process_resident_memory_bytes

Dashboard mínimo

1. CPU Usage vs Request vs Limit
2. Memory Usage vs Request vs Limit
3. CPU Throttling
4. Pod Restarts
5. Network I/O
6. Disk I/O

Conclusão

Performance em containers depende de:

  1. Recursos corretos: requests e limits baseados em dados reais
  2. Evitar throttling: CPU throttling destrói latência
  3. Memória com headroom: OOM kills causam instabilidade
  4. Rede otimizada: DNS, connection pools, service mesh consciente
  5. Imagens enxutas: menos layers, menos tamanho, startup rápido

Antes de culpar o container:

  1. Verifique se há CPU throttling
  2. Confirme que não há OOM kills
  3. Analise métricas de rede
  4. Compare com baseline fora do container

Container é ferramenta, não vilão. Performance ruim em container geralmente é performance ruim amplificada.

containersdockerkubernetesotimização
Compartilhar:
Read in English

Quer entender os limites da sua plataforma?

Entre em contato para uma avaliação de performance.

Fale Conosco