Bem-vindo à aula 93 do nosso curso de DevOps. Hoje vamos mergulhar em um dos temas mais críticos para qualquer profissional da área: performance e otimização de sistemas. Em um mundo onde a experiência do usuário é fundamental e os custos de infraestrutura podem escalar rapidamente, entender como extrair o máximo de desempenho de cada componente é essencial. Esta aula foi desenhada para fornecer a você as ferramentas conceituais e práticas necessárias para identificar, diagnosticar e resolver problemas de performance, desde o nível de aplicação até o de infraestrutura.

Vamos abordar quatro pilares fundamentais: gargalos, profiling em produção, caching e escalabilidade. Cada um desses tópicos é uma peça do quebra-cabeça da otimização. Ao final, você terá uma visão integrada de como pensar sobre performance de forma proativa e reativa, sabendo onde olhar primeiro e quais técnicas aplicar em cada cenário. Prepare-se para uma aula repleta de exemplos práticos e comandos que você pode usar no seu dia a dia.

Gargalos

Gargalos são pontos em um sistema onde a capacidade de processamento, armazenamento ou rede é insuficiente para atender à demanda, causando degradação de performance. Eles podem ocorrer em qualquer camada: CPU, memória, disco, rede ou até mesmo no código da aplicação. Identificar gargalos é o primeiro passo para otimizar, e a abordagem deve ser sistemática, usando métricas e observabilidade.

Um erro comum é tentar otimizar tudo de uma vez. Em vez disso, devemos focar no gargalo mais crítico, que geralmente é o que limita o throughput ou aumenta a latência. Por exemplo, se um serviço web está lento, pode ser devido a queries SQL ineficientes, falta de conexões no pool, ou até mesmo contenção de CPU. Para descobrir, precisamos medir.

Ferramentas como top, htop, vmstat e iostat são fundamentais para monitorar recursos do sistema. Em ambientes de nuvem, serviços como AWS CloudWatch, Google Cloud Monitoring e Azure Monitor oferecem métricas agregadas. Mas muitas vezes é necessário um aprofundamento com ferramentas de tracing e profiling, como veremos na próxima seção.

Vejamos um exemplo prático de uso de top para identificar um gargalo de CPU:

# Exibe processos em tempo real, ordenados por uso de CPU
top -o %CPU

# Saída simplificada:
# PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
# 1234 appuser   20   0  1.2g  300m   12m R  99.3   3.7   5:23.00 node

Nesse exemplo, um processo node está consumindo 99.3% da CPU, indicando um possível gargalo de processamento. O próximo passo seria usar um profiler para descobrir qual parte do código está causando isso.

Profiling em produção

Profiling é a técnica de analisar dinamicamente o comportamento de um programa para identificar onde ele gasta tempo ou recursos. Em produção, o profiling é desafiador porque precisamos minimizar o impacto no desempenho real. No entanto, é crucial para encontrar problemas que só aparecem sob carga real.

Existem dois tipos principais de profiling: amostragem (sampling) e instrumentação. A amostragem coleta dados em intervalos regulares, tendo baixo overhead, enquanto a instrumentação adiciona código para registrar eventos, podendo ter impacto maior. Em produção, a amostragem é geralmente preferida.

Ferramentas populares incluem perf para Linux, async-profiler para JVM, e para Node.js, o --prof flag ou o módulo v8-profiler. Para Python, py-spy é excelente porque não requer modificação no código. Vamos ver um exemplo com perf:

# Instalar perf (Debian/Ubuntu)
sudo apt-get install linux-tools-common linux-tools-generic

# Coletar amostras de um processo por 30 segundos
sudo perf record -g -p 1234 -- sleep 30

# Gerar relatório
sudo perf report

O relatório mostrará a árvore de chamadas, indicando onde a CPU está sendo gasta. Isso ajuda a identificar funções quentes que merecem otimização.

Além de CPU, profiling de memória também é vital. Ferramentas como heaptrack para C++, heapdump para Node.js, e tracemalloc para Python podem revelar vazamentos e uso excessivo de memória. Em produção, é importante ter um plano para lidar com o overhead do profiling, como fazer durante janelas de baixo tráfego ou usar ferramentas de baixo impacto.

Caching

Caching é uma técnica que armazena cópias de dados frequentemente acessados em um local de acesso rápido, reduzindo a latência e a carga em sistemas de armazenamento ou computação. Em DevOps, o caching pode ser aplicado em várias camadas: navegador, CDN, aplicação, banco de dados e até mesmo em nível de DNS.

Os benefícios são enormes: melhora a experiência do usuário, reduz custos de infraestrutura e aumenta a capacidade de resposta. No entanto, implementar caching exige cuidado com a consistência dos dados e a invalidação. Estratégias comuns incluem cache-aside, write-through e write-behind.

Vamos ver um exemplo de cache-aside em uma aplicação web usando Redis, um dos caches mais populares. O padrão é: primeiro verificar o cache, se não existir, buscar no banco e popular o cache.

# Suponha que temos uma aplicação Node.js com Redis
# Instalar cliente Redis: npm install redis

const redis = require('redis');
const client = redis.createClient();

async function getUser(id) {
  const cacheKey = `user:${id}`;
  // 1. Tentar obter do cache
  const cached = await client.get(cacheKey);
  if (cached) return JSON.parse(cached);

  // 2. Se não estiver no cache, buscar no banco
  const user = await db.findUser(id);

  // 3. Armazenar no cache com TTL de 1 hora
  await client.set(cacheKey, JSON.stringify(user), 'EX', 3600);
  return user;
}

Nesse código, usamos TTL (time-to-live) para expirar o cache automaticamente, evitando dados obsoletos. Em sistemas maiores, o Redis pode ser usado para cache de sessões, páginas HTML, resultados de consultas e muito mais.

Outra forma importante de caching é o HTTP caching, controlado por cabeçalhos como Cache-Control e ETag. Em CDNs, isso é essencial para servir conteúdo estático rapidamente. Vamos ver um exemplo de configuração em um servidor Nginx:

# Configuração de cache para arquivos estáticos
location /static/ {
    expires 30d;
    add_header Cache-Control "public, immutable";
}

Isso faz com que o navegador e CDNs armazenem os arquivos por 30 dias, reduzindo drasticamente o tráfego.

Escalabilidade

Escalabilidade é a capacidade de um sistema de lidar com cargas crescentes adicionando recursos. Existem dois tipos principais: escalabilidade vertical (aumentar recursos de uma única máquina) e horizontal (adicionar mais máquinas). Em DevOps, a escalabilidade horizontal é frequentemente preferida por ser mais flexível e resiliente.

Para escalar horizontalmente, precisamos de balanceamento de carga, replicação de dados e estratégias de consistência. Um exemplo comum é usar múltiplas instâncias de uma aplicação atrás de um load balancer, como o Nginx ou o AWS ELB. Isso distribui o tráfego e permite que novas instâncias sejam adicionadas dinamicamente.

Outro aspecto é a escalabilidade de banco de dados. Técnicas como replicação de leitura (read replicas) e sharding (particionamento de dados) são usadas para distribuir a carga. Vamos ver um exemplo de configuração de replicação no PostgreSQL:

# postgresql.conf no servidor primário
wal_level = replica
max_wal_senders = 10

# Executar no primário para criar uma réplica
SELECT pg_create_physical_replication_slot('replica1');

# No servidor réplica, configurar o standby
# standby_mode = on
# primary_conninfo = 'host=primary_host port=5432 user=replica password=secret'

Com replicação, as leituras podem ser direcionadas para as réplicas, aliviando o primário. Para sharding, ferramentas como Citus (para PostgreSQL) ou MongoDB com sharding nativo ajudam a distribuir dados por vários servidores.

Além disso, a escalabilidade deve ser pensada em conjunto com a observabilidade. Métricas como CPU, memória, latência e taxa de erros devem ser monitoradas para acionar o auto-scaling em ambientes de nuvem. Por exemplo, no Kubernetes, podemos usar o Horizontal Pod Autoscaler (HPA):

# Exemplo de HPA para uma aplicação
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: minha-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: minha-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Esse HPA aumenta o número de pods quando a utilização de CPU excede 70%, garantindo que a aplicação responda à demanda sem intervenção manual.

Boas Práticas e Observações Finais

Antes de encerrar, quero destacar algumas boas práticas: sempre meça antes de otimizar; defina SLOs (Service Level Objectives) para orientar seus esforços; automatize testes de performance como parte do CI/CD; e documente seus achados e decisões. Performance é um processo contínuo, não uma tarefa única.

Lembre-se de que otimização prematura pode ser prejudicial. Foco em gargalos reais, use dados para embasar decisões e colabore com a equipe de desenvolvimento para aplicar melhorias. Com as ferramentas e conceitos desta aula, você está pronto para enfrentar desafios de performance em qualquer ambiente.

Referências

Exercícios

  1. Explique a diferença entre gargalos de CPU e gargalos de I/O. Dê um exemplo de sintoma de cada um e como identificá-los usando ferramentas de linha de comando.
  2. ✓ Resposta: Gargalo de CPU ocorre quando o processador está saturado, por exemplo, devido a loops infinitos ou processamento intenso. Sintoma: alto uso de CPU com baixa utilização de disco/rede. Identificação: usar top e ver processo com %CPU alto. Gargalo de I/O ocorre quando o disco ou rede é o limitante, como em leituras/escritas frequentes. Sintoma: alta latência e uso de disco elevado com CPU baixa. Identificação: usar iostat para ver %util alto ou vmstat com wa alto.
  3. Escreva um comando perf para registrar o perfil de um processo por 60 segundos e gerar um relatório. Explique o que o relatório mostra.
  4. ✓ Resposta: Comando: sudo perf record -g -p PID -- sleep 60 e depois sudo perf report. O relatório mostra uma árvore de chamadas com o tempo gasto em cada função, permitindo identificar pontos quentes de CPU.
  5. Implemente um cache-aside em Python para uma função que busca dados de um banco, usando Redis. Inclua TTL.
  6. ✓ Resposta:
    import redis, json
    r = redis.Redis()
    def get_data(key):
        cache_key = f"data:{key}"
        cached = r.get(cache_key)
        if cached:
            return json.loads(cached)
        data = fetch_from_db(key)
        r.setex(cache_key, 3600, json.dumps(data))
        return data
    
  7. Qual é a diferença entre escalabilidade vertical e horizontal? Dê um exemplo de cada.
  8. ✓ Resposta: Vertical: aumentar recursos de uma única máquina (ex: adicionar mais RAM). Horizontal: adicionar mais máquinas (ex: criar novas instâncias de aplicação atrás de um load balancer).
  9. Configure um HorizontalPodAutoscaler no Kubernetes para escalar um deployment chamado "web" quando o uso de CPU ultrapassar 80%.
  10. ✓ Resposta:
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: web-hpa
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: web
      minReplicas: 1
      maxReplicas: 10
      metrics:
      - type: Resource
        resource:
          name: cpu
          target:
            type: Utilization
            averageUtilization: 80