Teste de carga com locust e uv
Este skill leva um projeto de "quanto ele aguenta?" a um relatório com números em que a equipe pode agir. Siga as etapas em ordem, porque cada uma alimenta a próxima. O usuário toma duas decisões, o nível de teste e o cenário de tráfego, e elas são tomadas somente depois que o usuário vê o que você encontrou no código.
$SKILL é a pasta deste skill, o diretório de onde você leu este SKILL.md. Defina-o uma vez no shell
(SKILL=/caminho/para/locust-load-test). O teste de carga em si fica no repositório alvo, em loadtest/
ou onde o usuário escolher.
1 Descobrir -> loadtest/endpoints.json cada endpoint, com seus chamadores e dicas de custo
2 Classificar -> loadtest/PLAN.md tabela core / suporte / periférico; mostre ao usuário
3 Perguntar -> níveis + cenário + ambiente + restrições
4 Construir -> loadtest/ (um projeto uv) harness.py, payloads/, scripts de execução
5 Executar -> loadtest/results/<run>/ fumaça primeiro, depois os níveis escolhidos, o mais arriscado por último
6 Coletar -> results/<run>/metrics, perfis visão do servidor, quando acessível
7 Relatar -> loadtest/report.html + descobertas em texto simples
1. Descobrir os endpoints
uv run $SKILL/scripts/discover_endpoints.py . -o loadtest/endpoints.json --markdown loadtest/endpoints.md
O scanner lê o código e não escreve mais nada. Ele encontra declarações de rota nas stacks comuns:
Python, Node, JVM, Go, Ruby, .NET e PHP frameworks, além de arquivos OpenAPI, esquemas GraphQL, protos gRPC,
websockets e SSE. Ele resolve prefixos de roteador e mounts quando possível. Ele também conta os pontos de
chamada do cliente (fetch, axios, httpx, requests, clientes gerados, testes, e os links, formulários e
atributos htmx de templates renderizados no servidor) para cada endpoint, para que você veja o que a
própria UI e serviços do produto usam. Cada entrada tem o método, o caminho, arquivo:linha, o handler,
dicas de auth, dicas de body, dica de streaming, dicas de custo, as chamadas que o handler faz e quem o
chama.
A própria lista de rotas de um framework supera qualquer varredura estática, porque o framework já
resolveu cada prefixo, mount e rota gerada. Quando o app pode produzir uma, use-a como fonte da verdade.
Isso significa um documento OpenAPI de um servidor dev em execução (/openapi.json), flask routes, bin/rails routes, php artisan route:list --json ou /actuator/mappings do Spring:
uv run $SKILL/scripts/discover_endpoints.py . --route-dump http://localhost:8000/openapi.json -o loadtest/endpoints.json
Em seguida, leia a saída criticamente antes de confiar nela:
unmatched_client_callssão rotas que o scan perdeu ou chamadas para outros serviços.prefix_resolved: falseenotesmarcam os palpites.shared_handler: truesignifica que uma função atende vários métodos e o scan não conseguiu dizer a qual método suas dicas de custo pertencem; leia esse handler.existing_load_testslista locustfiles ou scripts k6 já no repositório. Reutilize seus payloads.- Verifique cinco entradas nos arquivos de origem.
Leia references/discovery.md quando o inventário parecer escasso, quando uma stack não for coberta ou quando
você precisar do comando de dump de rotas para um framework.
2. Classificar: core, suporte, periférico
Classifique os endpoints por julgamento. Não os classifique por palavra-chave: o mesmo /search é core em uma
loja e suporte em um console de administração.
| Nível | O que é | Exemplos típicos |
|---|---|---|
| core | Aquilo para que os usuários vêm e geralmente onde o trabalho pesado acontece: computação, chamadas de modelo, processamento de dados, o caminho principal de escrita | checkout em uma loja; converter ou extrair em um serviço de documentos; completions em uma API de chat |
| suporte | O que torna o core utilizável | login, sessões, configurações, listas e busca sobre os próprios dados do usuário, uploads que alimentam o core, CRUD de administração |
| periférico | Aquilo para que o produto não existe | health, métricas, docs, arquivos estáticos, rotas de debug, webhooks de serviços que não possui |
Evidências a considerar:
- o que o README e os docs dizem que o produto é;
- o que a UI chama mais (
client_calls) e o que os testes exercitam (test_calls); - o que o handler custa: siga
callspara a camada de serviço, já quecost_hintsvê apenas o corpo do handler. Chamadas de modelo, escritas em DB, chamadas externas, trabalho de CPU (hashing de senha, compressão, imagens:cpu), processamento de arquivos e publicações em fila são o que torna um endpoint caro; - nomenclatura;
- o que o próprio monitoramento ou logs da equipe dizem ser mais movimentado, se você puder vê-los.
Escreva loadtest/PLAN.md com esta tabela, uma linha por endpoint que você pretende testar, e uma lista curta dos
endpoints que você deixou de fora e por quê:
| endpoint | nível | por quê | dicas de custo | fonte do payload |
|---|---|---|---|---|
| POST /api/orders/{id}/checkout | core | o caminho de compra; UI o chama de 3 lugares | escritas em db, chamada de API de pagamento | tests/fixtures/order.json |
Mostre esta tabela ao usuário antes de fazer as perguntas e convide correções. Um nível errado faz com que cada número posterior responda à pergunta errada, e o usuário conhece seu produto melhor que qualquer varredura.
3. Perguntar ao usuário
Use a ferramenta AskUserQuestion. Se não estiver disponível, faça as mesmas perguntas como uma lista numerada simples e espere as respostas. Pergunte somente depois que o usuário tiver visto a tabela do plano.
- Q1 Nível core. Nível 1, 2 ou 3.
- Q2 Nível suporte. Nível 1, 2 ou 3, ou deixe o suporte de fora.
- Q3 Cenário de carga de usuário. Duas a quatro opções derivadas deste projeto (veja abaixo), com a que você recomenda primeiro, marcada "(Recomendado)", e uma linha sobre por que se encaixa.
- Q4 Ambiente alvo. Qual implantação testar (staging, uma stack local, uma cópia de teste de carga). Nunca teste um sistema de produção a menos que o usuário diga sim explicitamente. A carga se espalha para bancos de dados compartilhados, tenants e contas.
| Nível | Execuções | Respostas | Custos |
|---|---|---|---|
| 1 Fumaça / baseline | carga baixa e constante (1 a alguns usuários) por alguns minutos | cada caminho funciona e qual é a latência sem contenção? | minutos; risco insignificante |
| 2 Carga esperada | ramp em degraus até o pico esperado, mantê-lo, depois uma curta imersão nesse nível | atinge a meta de latência no seu momento mais movimentado, com que folga? | cerca de uma hora; o alvo opera em seu pico real |
| 3 Estresse / capacidade | ramp em degraus até que uma regra de parada dispare (o ponto de inflexão e o ponto de ruptura), além de um pico | onde quebra, como e se recupera? | uma a duas horas; o alvo saturará e pode cair, então precisa de uma janela e alguém monitorando |
Derive as opções da Q3 do que o projeto diz sobre seu tráfego: README e docs (quem usa, contagens de assentos,
tenants), configuração (limites de taxa, contagens de workers e réplicas, limites de autoscaler, tamanhos de fila,
pools de conexão), cron e jobs em lote, e os próprios timeouts e retries dos chamadores. Formatos comuns são
ramp em degraus de horário comercial, pico matinal, imersão constante, rajada de integração de outro serviço,
importação em lote e tráfego misto interativo mais de fundo. Cada opção declara seus números: usuários, taxa de
spawn, tempo de pensamento, duração e mix de requisições. Quando o projeto não diz nada sobre seu tráfego,
diga, nomeie a suposição em que cada número se baseia e pergunte pelo pico esperado na Q3.
references/tiers-and-scenarios.md mostra como transformar configuração nesses números. Leia antes de perguntar.
Após as respostas, resolva duas coisas em texto simples:
- Credenciais. Vêm de um arquivo ou variável de ambiente que o harness lê. Nunca são coladas no chat.
- Restrições. Liste as que encontrou e peça o resto: limites de taxa, janelas de licença ou ativação, quanto custa uma reinicialização, tenants compartilhados e o custo por requisição (tokens de modelo, APIs pagas). Uma reinicialização dentro de uma janela de licença pode travar um serviço por um dia, e algumas tarefas pesadas paralelas com uma entrada enorme podem derrubar um alvo. Pergunte antes da carga, não depois.
- Estado dos dados. Quantas linhas as listas e pesquisas têm no uso real, se os dados podem ser redefinidos entre execuções e quais linhas um usuário de teste pode editar ou excluir. Uma tabela vazia torna uma página de lista gratuita, e linhas que uma execução adiciona fazem com que cada etapa posterior leia mais que a anterior.
4. Construir o harness
mkdir -p loadtest && cd loadtest
cp $SKILL/templates/pyproject.toml . && uv sync # seu próprio .venv e uv.lock; locust travado, Python 3.11+
cp $SKILL/templates/{harness.py,run_local.sh,run_plan.sh,collect.sh,seed.py,loadtest.env.example} .
cp $SKILL/templates/run_docker.sh . # quando o gerador roda na rede Docker do alvo
cp -r $SKILL/templates/{run_k8s.sh,k8s} . # quando roda dentro do cluster do alvo
git -C .. status --short # apenas loadtest/ pode aparecer
Não execute uv init aqui. Em um repositório que é ele próprio um projeto uv, ele adiciona loadtest ao
[tool.uv.workspace] do app, instala locust no .venv do app e escreve um uv.lock no nível do app. Se isso
aconteceu, desfaça: git checkout -- pyproject.toml no app, delete o novo uv.lock e execute
uv sync lá. Em seguida, preencha o template:
REQUESTS. Preencha a tabela emharness.pydo PLAN.md: nível, nome, peso, mix, body e status esperado. Pegue os payloads dos testes, fixtures e schemas do projeto e coloque-os emloadtest/payloads/. Nunca use dados de produção. Um app renderizado no servidor responde 200 para sua página de login e também para suas páginas de erro, então verifique o que apenas a página correta tem:expect_texteexpect_locationonde um redirecionamento é a resposta. Uma linha que é redirecionada sem dizer isso falha.REQ_TIMEOUT. Defina-o para o timeout do chamador real: o gateway, o abort do frontend, o SDK do cliente ou o read timeout do proxy. O harness mantém cada requisição até ele como um prazo para toda a resposta, então uma resposta posterior falha comodeadline exceededem vez de contar como um sucesso que o produto já teria reportado como falha.MIX. Dê a cada mix de tráfego com um custo diferente sua própria tag e execute cada mix como seu próprio cenário (entradas curtas tipo chat, entradas tipo documento, uma opção que custa extra). Suas capacidades podem diferir várias vezes, e uma execução combinada esconde a cara.- Auth. Defina
AUTHpara que cada usuário simulado faça login uma vez, não uma vez por requisição. Um login de formulário com um cookie de sessão também precisa deLOGIN_EXPECT_LOCATIONouLOGIN_EXPECT_TEXT, já que uma senha errada também responde 200 (vejaloadtest.env.example). Sign-ins recebem estágiologine ficam fora dos números de passo e regras de parada: cada um pousa no início do passo que adicionou seu usuário, e um hash de senha lento encerraria a execução em um custo que a carga constante nunca paga. Para testar o login em si, faça dele uma linha. - Dados. Preencha
seed.pypara o app e executepython3 seed.py up --users N --rows M: através da API do app, cada conta de teste recebe suas próprias linhas, queSEED_MANIFESTentrega ao seu usuário; as chaves de linhacaptureeconsumerastreiam o que um usuário cria e exclui. Emparelhe criações com exclusões ou reseede entre execuções. - Jornadas. Fluxos de várias etapas (upload depois poll, criar depois ler) são um método em uma classe de usuário; o template tem um exemplo.
references/harness.md cobre todos os campos, as formas, os modos de auth, streaming, protocolos não-HTTP e
as três colocações. Leia enquanto constrói.
5. Executar com segurança
- Fumaça primeiro. Execute
RUN_NAME=smoke SHAPE=smoke ./run_local.sh: um usuário por classe de usuário envia cada linha uma vez, em ordem de tabela (coloque a linha que cria um id antes das linhas que o usam), depois fica ocioso. O log termina comNOTE smoke: every row was sentou nomeia as linhas que não foram. Verifique zero falhas, latência plausível e bodies corretos. Isso prova a fiação antes que qualquer coisa custe carga. - Verifique se as máquinas estão quietas. Antes de uma execução que mede capacidade, execute
uptimeevmstat 1 5no gerador e no host do alvo. Outros jobs nos mesmos núcleos fazem cada número ser uma medida da contenção. O harness amostra seu próprio host antes de cada execução emrun.json(host_checke uma nota); coloque os números do host alvo emNOTESquando for outra máquina. O relatório coloca um banner em uma execução cujo host ou gerador atingiu 90 % de CPU durante seus passos e adverte sobre seu ponto de inflexão. Se uma máquina estiver ocupada, espere, fixe em núcleos ociosos (taskset) ou mova, e diga qual. - Depois os níveis escolhidos, o mais seguro primeiro. Execute carga esperada, depois imersão, depois
estresse, depois pico. Entradas arriscadas (os maiores payloads, as opções de custo extra) vão por último,
para que, se machucarem o alvo, nada antes delas seja perdido.
run_plan.shexecuta a lista em ordem e inicia cada execução apenas quando a verificação de saúde do alvo passa. - Coloque o gerador próximo aos chamadores reais quando o caminho de rede importar: o mesmo cluster,
VNet ou rede compose (
run_k8s.sh,run_docker.sh). Uma execução de laptop mede tanto a internet quanto o serviço. Cada execução registraGENERATOR, e a CPU do próprio gerador vai parametrics/client.csv; acima de cerca de 90 % de CPU, os números descrevem o gerador. - Respeite as regras de parada. O harness encerra uma execução quando a razão de falhas passa
STOP_FAIL_RATIO, quando p95 passaSTOP_P95_MS, quando mais da metade dos últimos 20 s falhou ou quando a maioria dos sign-ins falhou. Muitos serviços enfileiram em vez de descarregar carga: zero erros e latência que só cresce. Portanto, defina o teto de p95 para o timeout do chamador ou o SLO. Não ultrapasse uma regra de parada "para ver". - Monitore a saúde entre execuções e deixe as filas drenarem (
COOLDOWNemrun_plan.sh). - Limpe tudo o que você criou:
./run_k8s.sh down(pod, ConfigMaps, Secret), contêineres parados, usuários de teste e dados de teste. Liste para o usuário qualquer coisa que não possa ser removida, como linhas soft-deleted ou entradas de auditoria.
Leia references/safety.md antes da primeira execução além de fumaça e antes de qualquer execução contra um
sistema compartilhado.
6. Coletar o lado do servidor
Os números do cliente dizem o que aconteceu. Os números do servidor dizem por quê. Inicie os coletores antes do locust, pare-os depois dele e escreva no mesmo diretório de execução:
uv run $SKILL/scripts/collect_server.py --run-dir loadtest/results/<run> --target http://localhost:8000
O script de coleta extrai métricas do servidor (CPU, memória, I/O) e perfis de CPU, se disponíveis. Consulte
references/collect.md para mais detalhes.
7. Relatar
uv run $SKILL/scripts/report.py loadtest/results/<run>
Gera loadtest/report.html com gráficos de latência, throughput, taxas de erro, capacidade e ponto de inflexão,
além de um resumo em texto simples das descobertas: onde o serviço quebra, como e se recupera, e recomendações
acionáveis para a equipe.