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));import math
import os
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor
def pedir(url: str, planejado: float) -> tuple[float, bool]:
try:
with urllib.request.urlopen(url, timeout=10) as resposta:
resposta.read()
ok = resposta.status == 200
except OSError:
ok = False
return time.perf_counter() - planejado, ok
def gerar_carga(url: str, por_segundo: int, segundos: int) -> str:
"""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)."""
inicio = time.perf_counter()
futuros = []
with ThreadPoolExecutor(max_workers=500) as executor:
for i in range(por_segundo * segundos):
planejado = inicio + i / por_segundo
espera = planejado - time.perf_counter()
if espera > 0:
time.sleep(espera)
futuros.append(executor.submit(pedir, url, planejado))
resultados = [f.result() for f in futuros]
latencias = sorted(lat * 1000 for lat, _ in resultados)
erros = sum(1 for _, ok in resultados if not ok)
def p(q: float) -> str:
return f"{latencias[math.ceil(q * len(latencias)) - 1]:.0f}"
return (f"{len(latencias)} pedidos, {erros} erros | p50 {p(0.5)} ms, p95 {p(0.95)} ms, "
f"p99 {p(0.99)} ms, máx {p(1)} ms")
print(gerar_carga(os.environ["ALVO"], 90, 10))package main
import (
"fmt"
"io"
"math"
"net/http"
"os"
"sort"
"sync"
"time"
)
// gerarCarga usa o 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)
func gerarCarga(url string, porSegundo, segundos int) string {
cliente := &http.Client{Timeout: 10 * time.Second}
total := porSegundo * segundos
latencias := make([]float64, total)
erros := make([]bool, total)
inicio := time.Now()
var wg sync.WaitGroup
for i := range total {
planejado := inicio.Add(time.Duration(i) * time.Second / time.Duration(porSegundo))
time.Sleep(time.Until(planejado))
wg.Go(func() {
resposta, err := cliente.Get(url)
if err != nil {
erros[i] = true
} else {
io.Copy(io.Discard, resposta.Body)
resposta.Body.Close()
erros[i] = resposta.StatusCode != http.StatusOK
}
latencias[i] = float64(time.Since(planejado).Microseconds()) / 1000
})
}
wg.Wait()
nErros := 0
for _, e := range erros {
if e {
nErros++
}
}
sort.Float64s(latencias)
p := func(q float64) float64 { return latencias[int(math.Ceil(q*float64(total)))-1] }
return fmt.Sprintf("%d pedidos, %d erros | p50 %.0f ms, p95 %.0f ms, p99 %.0f ms, máx %.0f ms",
total, nErros, p(0.5), p(0.95), p(0.99), p(1))
}
func main() {
fmt.Println(gerarCarga(os.Getenv("ALVO"), 90, 10))
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
public class Carga {
// 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)
static String gerarCarga(String url, int porSegundo, int segundos) throws Exception {
var cliente = HttpClient.newHttpClient();
var pedido = HttpRequest.newBuilder(URI.create(url))
.timeout(Duration.ofSeconds(10)).build();
int total = porSegundo * segundos;
var latencias = Collections.synchronizedList(new ArrayList<Double>());
var erros = new AtomicInteger();
long inicio = System.nanoTime();
var pendentes = new ArrayList<CompletableFuture<?>>();
for (int i = 0; i < total; i++) {
long planejado = inicio + i * 1_000_000_000L / porSegundo;
long espera = planejado - System.nanoTime();
if (espera > 0) Thread.sleep(espera / 1_000_000, (int) (espera % 1_000_000));
pendentes.add(cliente.sendAsync(pedido, HttpResponse.BodyHandlers.discarding())
.handle((resposta, erro) -> {
if (erro != null || resposta.statusCode() != 200) erros.incrementAndGet();
latencias.add((System.nanoTime() - planejado) / 1e6);
return null;
}));
}
CompletableFuture.allOf(pendentes.toArray(new CompletableFuture[0])).join();
List<Double> ordenadas = new ArrayList<>(latencias);
Collections.sort(ordenadas);
java.util.function.DoubleFunction<Long> p =
q -> Math.round(ordenadas.get((int) Math.ceil(q * ordenadas.size()) - 1));
return String.format("%d pedidos, %d erros | p50 %d ms, p95 %d ms, p99 %d ms, máx %d ms",
ordenadas.size(), erros.get(), p.apply(0.5), p.apply(0.95), p.apply(0.99), p.apply(1));
}
public static void main(String[] args) throws Exception {
System.out.println(gerarCarga(System.getenv("ALVO"), 90, 10));
}
}using System.Collections.Concurrent;
using System.Diagnostics;
// 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 Task<string> GerarCarga(string url, int porSegundo, int segundos)
{
using var cliente = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var total = porSegundo * segundos;
var latencias = new ConcurrentBag<double>();
var erros = 0;
var relogio = Stopwatch.StartNew();
var pendentes = new List<Task>();
for (var i = 0; i < total; i++)
{
var planejado = TimeSpan.FromSeconds((double)i / porSegundo);
var espera = planejado - relogio.Elapsed;
if (espera > TimeSpan.Zero) await Task.Delay(espera);
pendentes.Add(Task.Run(async () =>
{
try
{
using var resposta = await cliente.GetAsync(url);
if (!resposta.IsSuccessStatusCode) Interlocked.Increment(ref erros);
}
catch (Exception) { Interlocked.Increment(ref erros); }
latencias.Add((relogio.Elapsed - planejado).TotalMilliseconds);
}));
}
await Task.WhenAll(pendentes);
var ordenadas = latencias.Order().ToArray();
long P(double q) => (long)Math.Round(ordenadas[(int)Math.Ceiling(q * ordenadas.Length) - 1]);
return $"{ordenadas.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.WriteLine(await GerarCarga(Environment.GetEnvironmentVariable("ALVO")!, 90, 10));O resultado, nas cinco linguagens
| Pedidos | Erros | p50 | p95 | p99 | Máximo | |
|---|---|---|---|---|---|---|
| TypeScript | 900 | 0 | 5 ms | 898 ms | 978 ms | 1.000 ms |
| Python | 900 | 0 | 6 ms | 850 ms | 972 ms | 1.006 ms |
| Go | 900 | 0 | 5 ms | 900 ms | 980 ms | 1.004 ms |
| Java | 900 | 0 | 6 ms | 877 ms | 974 ms | 1.003 ms |
| C# | 900 | 0 | 5 ms | 877 ms | 972 ms | 1.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 resposta | Relógio | |
|---|---|---|
| TypeScript | fetch sem await, guardando as promessas | performance.now() |
| Python | ThreadPoolExecutor com até 500 threads e urllib | time.perf_counter() |
| Go | Uma goroutine por pedido (wg.Go) | time.Now() e time.Since |
| Java | HttpClient.sendAsync | System.nanoTime() |
| C# | Task.Run com HttpClient | Stopwatch |
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]