Pular para conteúdo

Visão geral

No ano passado, a maior venda do BoxOffice caiu às 10:02. O time tinha feito teste de carga: 200 usuários virtuais contra o staging, p95 de 180 ms, "tudo verde". No post-mortem, encontraram cinco problemas nesse teste. O staging tinha um décimo dos dados da produção, então toda consulta acertava um cache quente. Os usuários virtuais esperavam cada resposta antes de enviar a próxima requisição, então, quando o sistema ficou lento, o teste educadamente ficou lento junto e nunca viu a fila que os fãs reais formaram. O teste durou dez minutos e nunca chegou ao ponto em que o pool de conexões do banco se esgotava. Ninguém tinha escrito o que significava "bom o suficiente" antes de rodá-lo. E o relatório mostrava médias.

Engenharia de desempenho é a disciplina que evita esse post-mortem. Não é "rodar uma ferramenta e olhar um gráfico": é um método — declarar o objetivo, modelar a carga de trabalho, gerar carga que se comporte como usuários reais, medir o que os usuários sentem, encontrar o único recurso que limita o sistema, corrigi-lo e medir de novo. Esta aula ensina o método e as ferramentas, usando a venda relâmpago do BoxOffice como caso de teste.

A história até aqui

Capítulo 15 da história da disciplina. Cada capítulo acrescentou uma caixa ao BoxOffice porque um número pediu: uma borda e réplicas sem estado (aula 4, aula 5), um banco que vende cada assento uma vez só (aula 6), um cache (aula 7), uma sala de espera (aula 8), SLOs e disjuntores (aula 9, aula 10), Kubernetes (aula 11) — e, desde a aula 14, as mudanças chegam mais rápido do que alguém consegue reverificá-las à mão. Cada caixa foi medida sozinha, no seu próprio lab. Esta aula responde: o projeto inteiro aguenta a próxima venda — 2 M de fãs às 10:00, p99 abaixo de 300 ms —, como provar isso, e como manter a prova a cada mudança?

Objetivos de aprendizagem

Ao final desta aula você deverá ser capaz de:

  • Definir latência, tempo de serviço, vazão, concorrência, utilização e saturação, e relacioná-los com a Lei de Little.
  • Ler uma curva vazão–latência, localizar o joelho e o ponto de saturação e explicar por que a latência explode quando a utilização se aproxima de 100 %.
  • Aplicar uma metodologia de desempenho — caracterização da carga, o método USE, métricas RED — para encontrar o gargalo de um sistema em vez de adivinhar.
  • Projetar e escrever testes smoke, load, stress, spike, soak e breakpoint em k6, escolhendo entre os modelos de carga aberto (taxa de chegada) e fechado (usuários virtuais), com thresholds que codificam os SLOs.
  • Detectar a omissão coordenada e outros erros de medição (dados irreais, caches frios, gerador de carga saturado) que fazem um teste mentir.
  • Diagnosticar um gargalo com profiling (flame graphs do async-profiler e do py-spy), ferramentas de banco de dados (EXPLAIN ANALYZE, pg_stat_statements) e dimensionamento de pools.
  • Planejar capacidade com a Lei de Little e folga, e impor orçamentos de desempenho na CI.

Roteiro

  1. Esta visão geral — o vocabulário, a curva vazão–latência e seu joelho, e o ciclo de engenharia de desempenho.
  2. Metodologia — anti-métodos, caracterização da carga, os métodos USE e RED, ambientes de teste, dados de teste e aquecimento.
  3. Testes de carga — os seis tipos de teste em k6, modelos aberto vs fechado, thresholds como SLOs, omissão coordenada, caos sob carga e os perfis do playground.
  4. Encontrando gargalos — leitura dos resultados, profiling e flame graphs, gargalos de banco de dados, dimensionamento de pools.
  5. Planejamento de capacidade — Lei de Little e folga, atraso do autoscaling, um modelo de capacidade para a venda e orçamentos de desempenho na CI.
  6. Quiz — teste seu entendimento.

Slides

Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba


O vocabulário de desempenho

Palavras precisas evitam a maioria das discussões sobre desempenho. A aula de Estimativas as apresentou; aqui elas viram alvos de medição.

Termo Definição Exemplo no BoxOffice
Tempo de serviço (S) Tempo que um recurso passa de fato trabalhando em uma requisição 4 ms de CPU e 6 ms de banco para fazer uma reserva de assento
Tempo de espera (fila) Tempo que uma requisição passa esperando um recurso ocupado Esperar uma conexão livre no pool
Latência / tempo de resposta (R) O que o cliente mede: espera + serviço + rede POST /holds levou 230 ms de ponta a ponta
Vazão (X) Requisições concluídas por unidade de tempo 1 800 reservas por segundo
Concorrência (N) Requisições no sistema no mesmo instante (em serviço ou esperando) 120 reservas em andamento
Utilização (U) Fração do tempo em que um recurso está ocupado CPU do banco ocupada 82 % do tempo
Saturação Trabalho que o recurso ainda não consegue atender — o tamanho da fila 40 requisições esperando uma conexão
Percentil (p50, p99, p99.9) O valor abaixo do qual fica aquela fração das observações p99 = 300 ms: 1 reserva em 100 é mais lenta que 300 ms
Taxa de erros Fração de requisições com falha — incluindo timeouts 0,3 % das reservas retornaram 503

Três relações valem para qualquer sistema estável e merecem ser memorizadas:

  • Lei de Little: \(N = X \times R\) — a concorrência é igual à vazão vezes o tempo de resposta1. A 1 800 reservas/s e 0,12 s cada, há em média 216 reservas em andamento: é de quantas threads, conexões ou slots assíncronos o caminho precisa.
  • Lei da utilização: \(U = X \times S\) — um banco que gasta 0,4 ms de CPU por consulta (por núcleo) a 2 000 consultas/s em um núcleo está 80 % ocupado.
  • Tempo de resposta = tempo de serviço + tempo de espera: otimizar código encurta o tempo de serviço; adicionar capacidade ou reduzir a carga encurta a espera. Perto da saturação, a espera domina.

Médias mentem

Uma média de 120 ms é compatível com "todos esperam 120 ms" e com "99 % esperam 20 ms e 1 % espera 10 s". Usuários — e SLOs — vivem na cauda. Reporte percentis a partir de histogramas (HDR ou histogramas nativos), nunca a média de percentis de máquinas ou intervalos diferentes: percentis não podem ser somados e divididos. Veja Percentis e latência de cauda.


A curva vazão–latência e seu joelho

Todo sistema, de uma thread a um data center, mostra a mesma forma quando você aumenta a carga. O gráfico da esquerda modela um pod de inventory com 8 threads de trabalho e 20 ms de tempo médio de serviço — capacidade de 400 requisições por segundo — como uma fila M/M/8. O da direita usa a Lei Universal de Escalabilidade (USL, Universal Scalability Law), que adiciona contenção (σ, esperar por recursos compartilhados) e coerência (κ, o custo de manter o estado compartilhado consistente) à escala linear ideal2:

\[ X(N) = \frac{\lambda N}{1 + \sigma (N-1) + \kappa N (N-1)} \]
2026-11-01T21:55:07.315771 image/svg+xml Matplotlib v3.11.2, https://matplotlib.org/ 0 50 100 150 200 250 300 350 400 offered load (req/s); capacity = 400 0 50 100 150 200 250 300 350 400 response time (ms) the knee: > 75 % busy Latency vs load (M/M/8, 20 ms) mean p99 0 20 40 60 80 100 120 concurrent requests N 0 100 200 300 400 500 600 700 throughput (req/s) peak at N ≈ 40 then retrograde Throughput vs concurrency (USL) USL throughput linear (ideal)

Leia os dois gráficos juntos:

  1. A região plana. Com carga baixa, a latência é só o tempo de serviço. Dobrar a carga quase não a muda. A maioria dos sistemas passa a maior parte da vida aqui, e é por isso que parecem rápidos.
  2. O joelho. Acima de cerca de 70–80 % de utilização, as requisições começam a encontrar todos os servidores ocupados e entram na fila. A latência — e o p99 primeiro — se curva para cima. O joelho é a capacidade útil do sistema: a maior vazão em que o SLO de latência ainda é cumprido.
  3. Saturação. Quando a carga oferecida se aproxima da capacidade, as filas crescem sem limite; a latência fica vertical e os timeouts viram erros. A vazão para de crescer.
  4. Escala retrógrada. Com custos de contenção e coerência, adicionar mais concorrência depois do pico reduz a vazão — threads brigam por locks, caches se invalidam, conexões expiram e são refeitas. Um sistema empurrado além do pico pode vender menos ingressos por segundo do que um mantido logo abaixo dele, que é o argumento central para controle de admissão e salas de espera.

BoxOffice: o joelho é o número com que você planeja

Se um pod de inventory mantém o p99 abaixo de 300 ms até 275 req/s e colapsa em 360, o número de capacidade para o planejamento é 275, não 360. Para uma venda que espera 4 500 requisições de reserva por segundo no pico, isso significa ⌈4 500 / 275⌉ = 17 pods antes da folga — 22 a 75 % do joelho, 33 para sobreviver à perda de uma de três zonas — e um banco que aguente as conexões de 33 pods. A página de Planejamento de capacidade constrói esse modelo de ponta a ponta.

No código do BoxOffice: o joelho do catalog, medido (lab 2)

O Hands-on 2 — Medir primeiro encontra o joelho exatamente como esta seção descreve: o sweep.sh roda o load.js (modelo aberto) uma vez por taxa de chegada, um minuto cada, depois de um aquecimento de 30 s que não é registrado, e espera 10 s entre os passos para as filas esvaziarem. Na execução de exemplo do lab (catalog limitado a 1 CPU, pool de 10 conexões com o banco), o p99 é 38,6 ms a 300 req/s, 223,7 ms a 400 e 2.056 ms a 500 — onde o k6 também descartou 410 iterações. O joelho fica entre 300 e 400 req/s, e o recurso que saturou foi o pool de conexões, não a CPU.

scripts/sweep.sh (one load test per arrival rate)
#!/usr/bin/env bash
# Lab 2: run load.js once per arrival rate and save one k6 summary per run.
#   bash scripts/sweep.sh 25 50 100 150 200 300 400
# Then:  uv run scripts/summarize.py results/lab2/rate-*.json
set -euo pipefail
OUT=${OUT:-results/lab2}
DURATION=${DURATION:-1m}
mkdir -p "$OUT"
echo ">>> warm-up: 30 s at RATE=$1 (not recorded)"
k6 run -q -e RATE="$1" -e DURATION=30s load/load.js > /dev/null || true
for rate in "$@"; do
  echo ">>> RATE=$rate req/s for $DURATION"
  # || true: a run that crosses a threshold still produces a summary; keep sweeping
  k6 run -q -e RATE="$rate" -e DURATION="$DURATION" \
     --summary-export "$OUT/rate-$rate.json" load/load.js > /dev/null || true
  sleep 10   # let queues drain before the next step
done

Experimente: este cenário do playground é o sistema mais simples possível — cliente → servidor de aplicação → banco SQL — rodando o perfil breakpoint, que aumenta a taxa de chegada passo a passo até algo quebrar. Pressione Run e observe três números juntos: vazão, latência p99 e a utilização de cada componente. Anote a taxa em que o p99 começa a se curvar (o joelho) e qual componente chega a 100 % primeiro (o gargalo). Depois dê mais capacidade a esse componente e rode de novo: o joelho se move, e qual componente vira o novo gargalo?

Abra o playground completo para adicionar um cache ou réplicas de leitura e ver como a curva muda.


O ciclo de engenharia de desempenho

Trabalho de desempenho é um ciclo científico, não um evento único antes do lançamento3:

flowchart LR
    slo["1. Objetivos<br/>SLOs por endpoint,<br/>pico esperado"] --> model["2. Modelo de carga<br/>mix, padrão de chegada,<br/>dados"]
    model --> test["3. Teste<br/>cenário k6 em um<br/>ambiente parecido com produção"]
    test --> analyze["4. Análise<br/>curva, USE, RED,<br/>perfis"]
    analyze --> fix["5. Mude UMA coisa<br/>(código, config,<br/>capacidade)"]
    fix --> test
    analyze --> ship["6. Publique + orçamento na CI<br/>monitore em produção"]
    ship --> slo
  • Objetivos primeiro. "Rápido" não é um requisito. "POST /holds: p99 < 300 ms e erros < 0,1 % a 4 500 req/s" é. Sem isso, nenhum teste pode passar ou falhar.
  • Mude uma coisa de cada vez e rode o mesmo teste de novo — senão você não consegue atribuir a melhoria.
  • A produção fecha o ciclo. O tráfego real é o melhor modelo de carga: métricas e traces de produção alimentam o próximo teste, e o orçamento de desempenho na CI impede que regressões voltem.
Nível Pergunta Ferramentas
Micro-benchmark Esta função/estrutura de dados é mais rápida? JMH (Java), pytest-benchmark
Teste de componente Quantas req/s um pod de inventory sustenta dentro do SLO? k6 contra um pod, com profiler acoplado
Teste de sistema O caminho de compra inteiro aguenta o pico da venda? Cenários k6 contra um ambiente parecido com produção4
Produção Está aguentando agora? De onde vem a cauda? Dashboards RED, traces, profiling contínuo, alertas de SLO

Na entrevista

Quando um entrevistador perguntar "como você saberia que este design aguenta o pico?", não diga "faríamos um teste de carga". Diga o que você testaria e como leria: "Um teste k6 de modelo aberto na taxa de chegada de pico esperada, com o mix realista de endpoints, thresholds de p99 e taxa de erros por endpoint, longo o suficiente para atingir o regime permanente com caches aquecidos; depois um teste breakpoint para achar o joelho e calcular a folga; o método USE no banco e no pool de conexões para descobrir o que satura primeiro."

Boas práticas

  • Mantenha os testes de carga no repositório do serviço, versionados com o código — um script que se afasta da API mede um endpoint que já não existe. No BoxOffice: a pasta load/ do repositório, do lab 2 ao lab 11.
  • Registre cada execução com suas condições — commit, configuração (tamanho do pool, limite de CPU), snapshot dos dados e máquina ao lado dos números; um p99 sem as condições não pode ser comparado com o próximo. No BoxOffice: results/lab2.md e as pastas da varredura do lab 2.
  • Publique a capacidade como um número por réplica, com o seu SLO — "catalog: 300 req/s com p99 < 300 ms em 1 CPU" é o número de que saem o planejamento de capacidade, o autoscaling e a taxa de admissão da sala de espera. No BoxOffice: o joelho medido no lab 2.
  • Leia o cliente e o servidor no mesmo eixo de tempo — o k6 diz que o p99 quebrou, as métricas RED e USE dos serviços dizem onde; marque cada execução e coloque os dois no mesmo Grafana. No BoxOffice: a pilha otel-lgtm do lab 8.
  • Barre cada mudança com um orçamento curto; rode a campanha completa antes de cada grande evento — um teste de 1 minuto em cada pull request pega o índice que falta; só os testes longos de load, spike e breakpoint provam a venda. No BoxOffice: o job performance budget do lab 11.

Armadilhas comuns

  • Sem objetivo: um teste sem critério de aprovação produz gráficos, não decisões.
  • Testar o sistema errado: dados minúsculos, caches quentes, tamanho de instância diferente, sem TLS, sem gateway.
  • Testes de modelo fechado para tráfego de mundo aberto: o gerador de carga desacelera junto com o sistema e esconde a fila (omissão coordenada).
  • Médias e execuções únicas: reporte percentis a partir de histogramas; repita as execuções e olhe a variância.
  • Otimizar sem gargalo: otimizar um componente a 20 % de utilização não muda nada.
  • Um gerador de carga saturado: se a máquina do k6 está a 100 % de CPU, você mediu o k6.

O mapa de conceitos de desempenho

A engenharia de desempenho não acrescenta caixas novas à paleta: ela mede as que você já tem. O mapa liga cada componente da paleta (verde) aos conceitos usados para carregá-lo, observá-lo e dimensioná-lo (azul):

flowchart LR
    client["client · mobile<br/>(k6 generates them)"]:::comp --> edge["apigw · lb"]:::comp
    edge --> app["app"]:::comp
    app --> sql["sql"]:::comp
    app --> cache["cache"]:::comp
    queue["queue<br/>(waiting room)"]:::comp --> app
    obs["metrics · tracing"]:::comp -.- app
    client -.- c1["open vs closed model,<br/>6 test types, thresholds"]:::tech
    app -.- c2["Little's Law, the knee,<br/>USL"]:::tech
    sql -.- c3["EXPLAIN ANALYZE, hot rows,<br/>pool sizing"]:::tech
    cache -.- c4["warm-up, cold cache,<br/>hit ratio"]:::tech
    obs -.- c5["USE, RED,<br/>flame graphs"]:::tech
    queue -.- c6["headroom, autoscaling lag,<br/>admission control"]:::tech
    classDef comp fill:#1e9e5a22,stroke:#1e9e5a
    classDef tech fill:#2f6fed22,stroke:#2f6fed
Componente da paleta Pergunta que responde Conceitos Ensinado em Cenários
client · mobile Como a carga real chega? modelo de carga, aberto (taxa de chegada) vs fechado (usuários virtuais), think time, omissão coordenada Metodologia · Testes de carga flash-sale, monolith
app Quanto uma réplica aguenta dentro do SLO? tempo de serviço, Lei de Little, lei da utilização, o joelho, USL, profiling esta visão geral · Encontrando gargalos monolith (perfil breakpoint), scale-out
sql Qual consulta ou lock limita o caminho? pg_stat_statements, EXPLAIN ANALYZE, linhas quentes, SKIP LOCKED, dimensionamento do pool de conexões Encontrando gargalos flash-sale
cache O teste está medindo um cache quente que a produção não terá? aquecimento, teste com cache frio, taxa de acerto, stampede Metodologia read-heavy-cache
metrics · tracing Qual recurso satura primeiro? USE, RED e sinais de ouro, flame graphs, profiling contínuo Metodologia · Encontrando gargalos observability-stack
queue · serverless Quantas réplicas, e quanto admitimos? folga, atraso do autoscaling, cold starts, pré-escalonamento, taxa de admissão da sala de espera Planejamento de capacidade flash-sale, serverless-burst
thirdparty O que acontece quando uma dependência fica lenta sob carga? carga e caos juntos, Toxiproxy, timeouts, circuit breakers Testes de carga cascading-failure
(o pipeline) A próxima mudança vai regredir? orçamentos de desempenho na CI, thresholds como SLOs Planejamento de capacidade —

Pontos-chave

  • Tempo de resposta = tempo de serviço + espera; perto da saturação, a espera domina. \(N = X \times R\) e \(U = X \times S\) conectam os números.
  • Todo sistema tem um joelho: planeje com a vazão em que o SLO de latência ainda vale, não com o máximo.
  • Depois do pico, mais concorrência pode significar menos vazão (USL) — controle de admissão é melhor que sobrecarga.
  • Engenharia de desempenho é um ciclo: objetivos → modelo de carga → teste → análise → mudar uma coisa → testar de novo → orçamento na CI.

O que quebra em seguida

A engenharia de desempenho prova este projeto para esta carga: um sistema de ingressos, uma venda, uma mistura de cerca de 80 % de leituras e 20 % de reservas e pedidos, um joelho medido nos próprios pods do BoxOffice. O método — modelo de carga, testes de modelo aberto, o joelho, a folga, um orçamento na CI — vale para qualquer sistema; os números e as caixas, não. Um feed de notícias espalha cada post para milhões de seguidores, um chat mantém milhões de conexões abertas por horas, um app de corridas grava a posição de cada motorista a cada poucos segundos: números diferentes pedem blocos diferentes. Reconhecer quais blocos dos quinze capítulos anteriores um sistema novo precisa, e qual número pede cada um, é a aula 16 — Estudos de caso.



  1. LITTLE, J. D. C. A Proof for the Queuing Formula: L = λW. Operations Research, v. 9, n. 3, p. 383–387, 1961. ↩

  2. GUNTHER, N. J. Guerrilla Capacity Planning. Springer, 2007. ↩

  3. GREGG, B. Systems Performance: Enterprise and the Cloud. 2nd ed. Addison-Wesley, 2020. ↩

  4. Grafana Labs. k6 documentation — Test types. ↩