Baseline de capacidade por réplica¶
Issue de referência: #53 — alternativa deliberada ao teste de 100k usuários em staging.
Por que baseline por réplica, e não um teste de 100k¶
Um teste de carga de 100k usuários contra staging produz um número impressionante e não auditável: depende do tamanho do cluster no dia, do autoscaling, do cache quente e do budget de infraestrutura queimado para o teatro. A decisão registrada na issue #53 substitui isso por uma medição honesta e reproduzível:
medir o p95 dos endpoints críticos contra uma única réplica da API, em níveis crescentes de concorrência, e extrapolar documentadamente (N réplicas × RPS por réplica, menos overhead de balanceamento).
Isso dá três coisas que o teste-espetáculo não dá:
- Número por réplica — a unidade real de planejamento de capacidade no Railway (escala horizontal = adicionar réplicas).
- Gate de regressão permanente — qualquer PR que degrade a latência dos endpoints críticos em ordem de grandeza é pego pelo cron mensal (ou por um dispatch manual antes de releases).
- Reprodutibilidade — qualquer pessoa roda o mesmo comando no mesmo hardware e obtém o mesmo número.
O que é medido¶
O perfil Locust capacity (classe CapacityUser em locustfile.py) é
closed-loop com think-time zero: -u N significa exatamente N
requisições em voo. Mix ponderado:
| Endpoint | Peso | Observação |
|---|---|---|
POST /analyze |
10 | link budget — endpoint mais quente |
POST /coverage/predict (modo ponto) |
5 | caminho ML/terreno, tier PRO+ |
GET /towers/nearest |
5 | consulta geoespacial |
GET /health |
2 | piso de latência do framework |
POST /batch_reports (150 linhas, só submit) |
2 | tag capacity-async; mede o enqueue na réplica, não o worker |
O caminho async só entra quando REDIS_URL está presente e
ASYNC_BACKEND=rq (o default) — o mesmo gate de job_dispatch.rq_enabled()
usado pela API. No CI, um service container Redis habilita o caminho. Ele mede o que a réplica da API faz no
caminho assíncrono — o throughput do worker RQ é outro eixo, medido
separadamente.
Como rodar¶
# Local: boota uma réplica de 1 worker (SQLite, admin key efêmera),
# faz o sweep 4/8/16/32 usuários × 45 s e aplica o gate de p95.
pip install locust
python3 scripts/capacity_baseline.py --out-dir reports/capacity
# Contra um host já em execução (sem boot local; sem gate).
# A chave vai por variável de ambiente: argv é legível por qualquer
# usuário local via ps(1) / /proc/<pid>/cmdline.
export LOCUST_API_KEY="$KEY"
python3 scripts/capacity_baseline.py --host http://localhost:8000 --skip-gate
No GitHub Actions: workflow capacity-baseline (workflow_dispatch
com levels/duration/skip_gate, mais cron mensal). O relatório sai
no step summary e os artefatos (capacity_baseline.json /
capacity_baseline.md + CSVs brutos do Locust) ficam 90 dias.
O gate¶
No nível base (o primeiro de --levels, default 4 usuários), cada
endpoint precisa de:
- p95 ≤ SLO configurado (defaults em
DEFAULT_GATE_P95_MSno script; override viaCAPACITY_GATE_P95_MS, JSON, ex.:{"/analyze": 1200}); - razão de falhas ≤
CAPACITY_MAX_FAILURE_PCT(default 1%).
Os defaults são deliberadamente folgados: runner compartilhado de CI tem jitter; o gate existe para pegar regressões de ordem de grandeza, não variações de 10%.
Como extrapolar (e o que não prometer)¶
- Capacidade horizontal ≈ N réplicas × RPS agregado da réplica no nível em que o p95 ainda respeita o SLO público. Desconte ~10% de overhead de LB/proxy.
- O runner do GitHub (4 vCPU) não é a réplica do Railway — os números
do CI servem para tendência e regressão, não para SLA absoluto. Para
número de SLA, rode o harness numa réplica com o mesmo shape de
produção (
--host+--skip-seedse a torre já existir). - O baseline local usa SQLite e não exercita Postgres/Redis compartilhados sob contenção — em produção o tail de p99 tende a ser pior. Por isso o gate olha p95, e a margem SLO↔baseline deve ser mantida generosa.
- Cache de DEM/terreno frio no primeiro nível pode inflar o p95 inicial;
o sweep usa o primeiro nível como gate depois do warm-up implícito do
boot + seed. Se o primeiro nível oscilar, aumente
--duration.