DevGuideTK

Testes de carga

Como descobrir o limite do sistema antes que os usuários descubram.

O código desta página foi executado nas cinco linguagens contra o servidor com pausas do nível Conceito: 4 ms de CPU por pedido, preso a um núcleo, travando 1 segundo a cada 5 segundos. O gerador rodou em outros quatro núcleos.

Um gerador de carga de modelo aberto, em cada linguagem

Ferramentas como o k6 resolvem o dia a dia. Escrever um gerador pequeno mostra o que elas fazem por dentro, e o detalhe que separa uma medida honesta de uma enganosa. Cada versão envia 90 pedidos por segundo durante 10 segundos, sem esperar as respostas anteriores, e mede a latência a partir do instante planejado de cada pedido, e não do instante em que ele de fato saiu. Se o próprio gerador atrasar, o atraso entra na conta, como entraria para um usuário.

// Modelo aberto: os pedidos saem numa taxa fixa, sem esperar as respostas anteriores,
// e a latência conta a partir do instante planejado (evita a omissão coordenada)
async function gerarCarga(url: string, porSegundo: number, segundos: number) {
  const total = porSegundo * segundos;
  const latencias: number[] = [];
  let erros = 0;
  const inicio = performance.now();
  const pendentes: Promise<void>[] = [];
  for (let i = 0; i < total; i++) {
    const planejado = inicio + (i * 1000) / porSegundo;
    const espera = planejado - performance.now();
    if (espera > 0) await new Promise((r) => setTimeout(r, espera));
    pendentes.push(
      fetch(url, { signal: AbortSignal.timeout(10_000) })
        .then(async (r) => { await r.arrayBuffer(); if (!r.ok) erros++; })
        .catch(() => { erros++; })
        .finally(() => latencias.push(performance.now() - planejado)),
    );
  }
  await Promise.all(pendentes);
  return resumo(latencias, erros);
}

function resumo(latencias: number[], erros: number) {
  const ordenadas = [...latencias].sort((a, b) => a - b);
  const p = (q: number) => ordenadas[Math.ceil(q * ordenadas.length) - 1].toFixed(0);
  return `${latencias.length} pedidos, ${erros} erros | p50 ${p(0.5)} ms, p95 ${p(0.95)} ms, `
    + `p99 ${p(0.99)} ms, máx ${p(1)} ms`;
}

console.log(await gerarCarga(process.env.ALVO!, 90, 10));

O resultado, nas cinco linguagens

PedidosErrosp50p95p99Máximo
TypeScript90005 ms898 ms978 ms1.000 ms
Python90006 ms850 ms972 ms1.006 ms
Go90005 ms900 ms980 ms1.004 ms
Java90006 ms877 ms974 ms1.003 ms
C#90005 ms877 ms972 ms1.001 ms

As cinco versões concordaram entre si e com o k6 no modelo aberto (p95 de 848 ms): metade dos pedidos levou 5 ms, e os que chegaram durante as pausas esperaram até 1 segundo. A pequena variação entre elas vem do momento em que cada execução começou em relação às pausas do servidor.

O que muda de uma linguagem para outra

Disparar sem esperar a respostaRelógio
TypeScriptfetch sem await, guardando as promessasperformance.now()
PythonThreadPoolExecutor com até 500 threads e urllibtime.perf_counter()
GoUma goroutine por pedido (wg.Go)time.Now() e time.Since
JavaHttpClient.sendAsyncSystem.nanoTime()
C#Task.Run com HttpClientStopwatch

Todas usam um relógio monotônico, que não volta nem salta com ajustes do relógio do sistema. Medir latência com o relógio de parede (Date.now(), time.time()) pode dar números errados, até negativos, se o horário for ajustado durante o teste.

O limite do gerador é o próprio gerador: em Python, as threads limitam a taxa possível, e um gerador sobrecarregado atrasa os pedidos. Medir a partir do instante planejado faz esse atraso aparecer como latência, em vez de sumir.

Os scripts do k6

O degrau de taxa constante, usado para achar o joelho (a taxa vem da variável TAXA):

import http from "k6/http";

export const options = {
  scenarios: {
    carga: {
      executor: "constant-arrival-rate",
      rate: Number(__ENV.TAXA),
      timeUnit: "1s",
      duration: "20s",
      preAllocatedVUs: 50,
      maxVUs: 2000,
    },
  },
  summaryTrendStats: ["avg", "p(50)", "p(95)", "p(99)", "max"],
};

export default function () {
  http.get("http://alvo:3000/", { timeout: "10s" });
}

E o modelo fechado, que escondeu as pausas:

import http from "k6/http";
import { sleep } from "k6";

export const options = {
  scenarios: { carga: { executor: "constant-vus", vus: 10, duration: "20s" } },
};

export default function () {
  http.get("http://alvo:3000/");
  sleep(0.1);
}

Rodados com k6 run -e TAXA=260 taxa.js (k6 2.4 no teste). Quando a taxa passou da capacidade, o k6 informou dropped_iterations: pedidos que ele não conseguiu iniciar no horário porque todos os usuários virtuais estavam presos esperando respostas. Esse número também é um sinal de sobrecarga.

Lista de revisão

Modelo

  • O teste usa taxa de chegada (modelo aberto) para simular usuários?
  • A latência é medida a partir do instante planejado?
  • O relatório mostra p95, p99 e erros, e não só a média?

Cenário

  • A mistura de rotas e os dados lembram os de produção?
  • Há aquecimento antes da medição?
  • A carga sobe em degraus até passar do joelho?
  • Há um teste longo para encontrar vazamentos?

Execução

  • O gerador roda fora da máquina testada, sem ser ele mesmo o gargalo?
  • As métricas do servidor são acompanhadas durante o teste?

Continuidade

  • Os objetivos viraram limites automáticos que reprovam o teste?
  • As rotas críticas têm um teste de carga na integração contínua ou antes de cada lançamento grande?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Escrever um teste de carga com k6

    Quero escrever meu primeiro teste de carga com k6.
    
    Pergunte quais rotas quero testar e como funciona a autenticação.
    
    Vamos em etapas:
    1. Um script com taxa de chegada constante para uma rota.
    2. A mistura de rotas com pesos, e usuários de teste diferentes.
    3. Degraus de taxa até passar do joelho.
    4. Limites de p95, p99 e erros que reprovam o teste.
    5. A leitura do resultado e das métricas do servidor.
  • Encontrar o gargalo durante um teste de carga

    Meu sistema fica lento num teste de carga e quero achar o gargalo.
    
    Pergunte qual linguagem, banco e infraestrutura eu uso, e peça o resultado do teste.
    
    Me guie: confirmar que o gerador não é o gargalo, olhar CPU, memória, conexões e filas do servidor, coletar um perfil sob carga, olhar as consultas mais lentas no banco e testar a hipótese com uma correção e um novo teste.
  • Revisar um teste de carga

    Vou colar abaixo o script de um teste de carga e o resultado. Revise.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. O teste usa modelo aberto, ou mede a partir do instante planejado?
    2. O resultado mostra percentis e erros?
    3. A mistura de rotas e os dados são realistas?
    4. Há aquecimento e degraus até passar do limite?
    5. O gerador roda separado do sistema testado?
    6. Há limites automáticos de aprovação?
    
    Para cada problema, explique como ele distorce o resultado e mostre a correção.
    
    Script e resultado:
    [cole aqui]