Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions deploy/stress/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# segredos e artefatos gerados no servidor
.env
harnesses.keys
run/
reports/
docker-compose.replicas.yml
docker-compose.override.yml
43 changes: 43 additions & 0 deletions deploy/stress/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Ambiente de stress: Thinker + White Wizard

Arquivos do ambiente de stress descrito nos tutoriais [#1](../../docs/tutoriais/thinker-with-white-wizard-01.md) (só OpenAI) e [#2](../../docs/tutoriais/thinker-with-white-wizard-02.md) (todos os provedores, 12 harnesses e loop sem fim). A pasta `opt/` espelha o `/opt` do servidor: cada subpasta vai para o caminho de mesmo nome.

Nenhum segredo está aqui. Cada pasta com credenciais tem um `.env.example`; o `.env` de verdade nasce no servidor, com `chmod 600`, pelos comandos dos tutoriais.

| Pasta | Tutorial | O que é |
| --- | --- | --- |
| `opt/wwz-openai/` | #1, seção 3 | White Wizard só OpenAI: API, Valkey e painel (`wwz.yaml` é renderizado com `envsubst` em `run/`) |
| `opt/thinker/` | #1, seção 4; #2, seção 7 | Thinker para stress (`thinker-app` + `thinker-postgres`) e `escala.sh` (réplicas no alias `thinker-pool`) |
| `opt/thinker-stress/` | #1, seção 6 | `stress.sh`: base, conc, rps, rampa, sweep, agentes, traefik, falhas, top |
| `opt/wwz-todos/` | #2, seção 3 | Quatro instâncias do wwz com os seis drivers (perfeito, falhas, lento, instável), Valkey e painel |
| `opt/thinker-stress-02/` | #2, seções 6 e 8 | `provisionar.sh` (24 conexões, 12 harnesses, cascata de fallback), `loop.sh` (geradores eternos) e `stress.sh` (dispara e para) |

## Instalar num servidor

Pré-requisitos e explicações estão nos tutoriais. O caminho curto:

```bash
# copiar para o servidor (os .env.example vao junto; os .env nao existem aqui)
rsync -av deploy/stress/opt/ root@servidor:/opt/

# no servidor: gerar os .env (tutorial #1 secoes 3.1, 4.1 e 5.5; tutorial #2 secoes 2 e 6),
# renderizar os YAML do wwz em run/ e subir cada compose
cd /opt/wwz-openai && docker compose up -d --wait
cd /opt/thinker && docker compose up -d --wait
cd /opt/wwz-todos && docker compose up -d --wait
/opt/thinker-stress-02/provisionar.sh
```

## Disparar o stress

```bash
/opt/thinker-stress/stress.sh conc 100 # tutorial #1, uma rodada medida
/opt/thinker-stress-02/stress.sh iniciar # tutorial #2, com freios: 3 replicas, 10 degraus, limites do compose
/opt/thinker-stress-02/stress.sh iniciar --sem-freios # secao 8.3: sem limites, pede confirmacao
/opt/thinker-stress-02/stress.sh status
/opt/thinker-stress-02/stress.sh parar
```

O modo `--sem-freios` copia `sem-freios.override.yml` para `/opt/thinker/docker-compose.override.yml`, o que tira os limites de CPU e memória do Thinker e liga o fsync do Postgres. Não use num servidor com outros serviços de produção: o loop foi feito para esgotar CPU, memória e disco. O `parar` remove o override, volta a uma réplica e restaura os limites.

Versão mínima do Thinker: **0.31.6** (os bugs que esse ambiente expôs estão na seção 10 do tutorial #2).
9 changes: 9 additions & 0 deletions deploy/stress/opt/thinker-stress-02/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# /opt/thinker-stress-02/.env (chmod 600; ver tutorial #2, secao 6)
# copiados de /opt/wwz-todos/.env
WWZ_KEY_1=wwz-key-troque
WWZ_KEY_2=wwz-key-troque
WWZ_KEY_3=wwz-key-troque
WWZ_KEY_4=wwz-key-troque
WWZ_ADMIN_TOKEN=troque
WWZ_LOAD_IMAGE=white-wizard-load:local
# THINKER_API_KEY e acrescentada pelo provisionar.sh (thinker key create)
93 changes: 93 additions & 0 deletions deploy/stress/opt/thinker-stress-02/loop.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#!/bin/bash
# /opt/thinker-stress-02/loop.sh
# Stress sem fim: a cada degrau sobe mais um lote de geradores (um por
# harness), que rodam para sempre (--restart unless-stopped), ate esgotar
# CPU, memoria e I/O do servidor. Nunca para sozinho.
# Uso:
# ./loop.sh iniciar sobe degraus ate MAX_DEGRAUS e segura a carga
# ./loop.sh status geradores, consumo e ultimo degrau
# ./loop.sh parar remove todos os geradores
set -euo pipefail
cd "$(dirname "$0")"
. ./.env

IMG=${WWZ_LOAD_IMAGE:-white-wizard-load:local}
NET=${NET:-network_public}
ALVO=${ALVO:-http://thinker-pool:2099} # alias comum a todas as replicas
CONC=${CONC:-50} # concorrencia de cada gerador
AGENTES=${AGENTES:-25} # agentes de cada gerador agents
DEGRAU=${DEGRAU:-120} # segundos entre degraus
MAX_DEGRAUS=${MAX_DEGRAUS:-1000000} # na pratica, sem fim
ROTULO=thinker.stress=loop

chave() { grep "^$1=" harnesses.keys | cut -d= -f2-; }

# gera <nome> <harness> <args do wwz-load...>
gera() {
local nome=$1 h=$2; shift 2
docker run -d --name "$nome" --label "$ROTULO" --restart unless-stopped \
--network "$NET" --ulimit nofile=1048576:1048576 \
--log-driver local --log-opt max-size=10m \
-e WWZ_LOAD_KEY="$(chave "$h")" "$IMG" wwz-load "$@" >/dev/null
}

# um lote: um gerador por harness, cada um no seu tipo de cliente
lote() {
local n=$1 h
for h in h-openai-sdk-1 h-openai-sdk-2; do
gera "st-$n-$h" "$h" run --target "$ALVO" --model auto --concurrency "$CONC" --duration 0 --progress 30s --timeout 300s
done
for h in h-anthropic-sdk-1 h-anthropic-sdk-2; do
gera "st-$n-$h" "$h" run --target "$ALVO" --protocol anthropic --model auto --concurrency "$CONC" --duration 0 --progress 30s --timeout 300s
done
for h in h-opencode-1 h-opencode-2; do
gera "st-$n-$h" "$h" agents --target "$ALVO" --model auto --profile coder --agents "$AGENTES" --turns 8 --duration 876000h --progress 30s
done
for h in h-hermes-1 h-hermes-2; do
gera "st-$n-$h" "$h" agents --target "$ALVO" --model auto --profile pesquisa --agents "$AGENTES" --turns 6 --duration 876000h --progress 30s
done
for h in h-open-webui-1 h-open-webui-2; do
gera "st-$n-$h" "$h" agents --target "$ALVO" --model auto --profile chat --agents "$AGENTES" --turns 20 --duration 876000h --progress 30s
done
for h in h-claude-code-1 h-claude-code-2; do
gera "st-$n-$h" "$h" claude-code run --target "$ALVO" --auth api-key --key "$(chave "$h")" --model claude-sonnet-5-5 --concurrency "$CONC" --turns 3 --duration 876000h
done
}

amostra() {
local n=$1
printf '%s degrau=%s geradores=%s ' "$(date -u +%FT%TZ)" "$n" "$(docker ps -q --filter label=$ROTULO | wc -l)"
docker stats --no-stream --format '{{.Name}}={{.CPUPerc}}/{{.MemUsage}}' \
$(docker ps --format '{{.Names}}' | grep -E '^(thinker-|wwz-(perfeito|falhas|lento|instavel)$)') | tr '\n' ' '
echo
}

case "${1:-}" in
iniciar)
if [ -f reports/loop.pid ] && kill -0 "$(cat reports/loop.pid)" 2>/dev/null; then
echo "ja existe um loop rodando (./loop.sh parar)"; exit 1;
fi
mkdir -p reports
echo $$ > reports/loop.pid
n=$(docker ps -a --filter label=$ROTULO --format '{{.Names}}' | sed -n 's/^st-\([0-9]*\)-.*/\1/p' | sort -n | tail -1)
n=${n:-0}
while [ "$n" -lt "$MAX_DEGRAUS" ]; do
n=$((n + 1))
lote "$n"
amostra "$n" | tee -a reports/loop.log
sleep "$DEGRAU"
done
while true; do amostra "$n" | tee -a reports/loop.log; sleep "$DEGRAU"; done
;;
status)
docker ps --filter label=$ROTULO --format '{{.Names}} {{.Status}}' | sort -V | tail -20
tail -5 reports/loop.log 2>/dev/null || true
;;
parar)
[ -f reports/loop.pid ] && kill "$(cat reports/loop.pid)" 2>/dev/null || true
rm -f reports/loop.pid
docker ps -aq --filter label=$ROTULO | xargs -r docker rm -f >/dev/null
echo "geradores removidos";
;;
*) sed -n '/^# Stress sem fim/,/^set -euo/p' "$0" | sed '$d'; exit 2 ;;
esac
121 changes: 121 additions & 0 deletions deploy/stress/opt/thinker-stress-02/provisionar.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/bin/bash
# /opt/thinker-stress-02/provisionar.sh
# Provisiona no Thinker: provedores apontando para as instancias do wwz,
# 12 harnesses (2 por tipo de cliente do wwz-load) e as rotas de cada um.
# Idempotente: pode rodar de novo depois de mudar algo.
set -euo pipefail
cd "$(dirname "$0")"
. ./.env

T="docker exec -e XDG_CONFIG_HOME=/tmp/thinker-cli thinker-app thinker"
TI="docker exec -i -e XDG_CONFIG_HOME=/tmp/thinker-cli thinker-app thinker"

# conecta <provedor> <rotulo> <chave> <base-url>: cria a conexao (se faltar) e
# aponta a base. Uma chave por instancia: o Thinker reaproveita a conexao
# quando a chave se repete, mesmo com outro rotulo.
conecta() {
local p=$1 rotulo=$2 k=$3 base=$4 id
id=$($T provider list </dev/null | jq -r --arg p "$p" --arg l "$rotulo" \
'.providers[] | select(.provider==$p) | .connections[] | select(.label==$l and .is_active) | .id' | head -1)
if [ -z "$id" ]; then
id=$(printf '%s' "$k" | $TI provider connect "$p" --credential-stdin --agent h-openai-sdk-1 --label "$rotulo" | jq -r .id)
fi
$T connection set "$id" --base-url "$base" </dev/null >/dev/null
echo "conexao $p/$rotulo -> $base";
}

# 1 - harnesses: nome plataforma categoria
while read -r nome plat cat; do
$T agent create --name "$nome" --platform "$plat" --category "$cat" --if-absent </dev/null >/dev/null
echo "harness $nome ($plat)";
done <<'EOF'
h-openai-sdk-1 openai-sdk app
h-openai-sdk-2 openai-sdk app
h-anthropic-sdk-1 anthropic-sdk app
h-anthropic-sdk-2 anthropic-sdk app
h-opencode-1 opencode coding
h-opencode-2 opencode coding
h-hermes-1 hermes personal
h-hermes-2 hermes personal
h-open-webui-1 open-webui automation
h-open-webui-2 open-webui automation
h-claude-code-1 claude-code coding
h-claude-code-2 claude-code coding
EOF

# 2 - provedores: uma conexao por instancia do wwz (rotulo = instancia)
n=0
for inst in perfeito falhas lento instavel; do
n=$((n + 1)); k=$(grep "^WWZ_KEY_$n=" .env | cut -d= -f2-)
W=http://wwz-$inst:8000
conecta openai "$inst" "$k" "$W/openai"
conecta anthropic "$inst" "$k" "$W/anthropic"
conecta openrouter "$inst" "$k" "$W/openrouter"
conecta gemini "$inst" "$k" "$W/gemini"
conecta opencode-go "$inst" "$k" "$W/opencode/inference/go/openai"
conecta opencode "$inst" "$k" "$W/opencode/inference/openai"
done
$T provider refresh </dev/null >/dev/null

# 3 - habilita todos os provedores em todos os harnesses
harnesses=$($T agent list </dev/null | jq -r '.agents[].agent_name | select(startswith("h-"))')
for h in $harnesses; do
for p in openai anthropic openrouter gemini opencode-go opencode; do
for inst in perfeito falhas lento instavel; do
$T agent provider enable "$h" "$p" --label "$inst" </dev/null >/dev/null 2>&1 || true
done
done
done

# 4 - rota padrao: <harness> <provedor> <modelo>. Principal na instancia
# "falhas"; fallbacks em "instavel" e "perfeito" (cada harness exercita o
# failover). O CLI nao fixa o rotulo da chave nos fallbacks; a API REST fixa.
if [ -z "${THINKER_API_KEY:-}" ]; then
k=$($T key create --name stress-02 </dev/null | jq -r '.key // .apiKey // .raw_key')
echo "THINKER_API_KEY=$k" >> .env
THINKER_API_KEY=$k
fi
api() {
docker run --rm -i --network network_public curlimages/curl:8.11.1 -s -f -X "$1" \
"http://thinker-app:2099/api/v1$2" -H "X-API-Key: $THINKER_API_KEY" \
-H 'content-type: application/json' --data-binary @-
}
while read -r h p m; do
$T agent configure "$h" --models "$m" --provider "$p" --key-label falhas --force </dev/null >/dev/null
jq -n --arg p "$p" --arg m "$m" '{models: [$m, $m], routes: [
{provider: $p, authType: "api_key", model: $m, keyLabel: "instavel"},
{provider: $p, authType: "api_key", model: $m, keyLabel: "perfeito"}]}' \
| api PUT "/routing/$h/tiers/default/fallbacks" >/dev/null
echo "rota $h -> $p/$m (falhas -> instavel -> perfeito)";
done <<'EOF'
h-openai-sdk-1 openai wwz-fast
h-openai-sdk-2 openai wwz-realistic
h-anthropic-sdk-1 anthropic claude-haiku-5-5
h-anthropic-sdk-2 anthropic claude-sonnet-5-5
h-opencode-1 opencode-go opencode-go/glm-5.3
h-opencode-2 opencode-go opencode-go/kimi-k3
h-hermes-1 openrouter white-wizard/wwz-fast
h-hermes-2 openrouter white-wizard/wwz-realistic
h-open-webui-1 gemini wwz-fast
h-open-webui-2 opencode opencode/big-pickle
h-claude-code-1 anthropic claude-sonnet-5-5
h-claude-code-2 anthropic claude-haiku-5-5
EOF

# 4b - o gerador do Claude Code manda um ID concreto (claude-sonnet-5-5), e ID
# concreto e rota explicita, sem fallback. Um modelo virtual com esse nome, sem
# principal e com Continue on Default, herda a cadeia do tier default
# (falhas -> instavel -> perfeito)
for h in h-claude-code-1 h-claude-code-2; do
jq -n '{name: "claude-sonnet-5-5", continue_on_default: true}' \
| api POST "/routing/$h/virtual-models" >/dev/null
echo "virtual $h claude-sonnet-5-5 -> cadeia do default";
done

# 5 - chaves dos harnesses num arquivo 600 (lido pelo loop)
umask 077
: > harnesses.keys
for h in $harnesses; do
printf '%s=%s\n' "$h" "$($T agent key show "$h" --raw </dev/null)" >> harnesses.keys
done
echo "chaves em $(pwd)/harnesses.keys";
13 changes: 13 additions & 0 deletions deploy/stress/opt/thinker-stress-02/sem-freios.override.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# /opt/thinker-stress-02/sem-freios.override.yml (copiado para /opt/thinker/docker-compose.override.yml pelo stress.sh --sem-freios)
# sem limite de CPU e memoria, banco com fsync de verdade (I/O)
services:
thinker-app:
cpus: 0
mem_limit: 0
environment:
NODE_OPTIONS: --max-old-space-size=32768
thinker-postgres:
cpus: 0
mem_limit: 0
command: [postgres, -c, max_connections=2000, -c, shared_buffers=16GB, -c, synchronous_commit=on,
-c, shared_preload_libraries=pg_stat_statements, -c, track_io_timing=on]
64 changes: 64 additions & 0 deletions deploy/stress/opt/thinker-stress-02/stress.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/bin/bash
# /opt/thinker-stress-02/stress.sh
# Dispara e para o stress do tutorial thinker-with-white-wizard-02.md.
# Uso:
# ./stress.sh iniciar com freios: limites do compose, REPLICAS (3) e MAX_DEGRAUS (10)
# ./stress.sh iniciar --sem-freios secao 8.3: sem limite de CPU/memoria, fsync no banco,
# uma replica por 4 nucleos e degraus sem fim
# ./stress.sh status geradores, replicas e ultimas amostras
# ./stress.sh parar para o loop, volta a 1 replica e tira o override
# Ajustes (variaveis): REPLICAS, CONC, AGENTES, DEGRAU, MAX_DEGRAUS (ver loop.sh)
set -euo pipefail
cd "$(dirname "$0")"
. ./.env

OVERRIDE=/opt/thinker/docker-compose.override.yml

zera_wwz() {
local i
for i in perfeito falhas lento instavel; do
docker run --rm --network network_public curlimages/curl:8.11.1 -s -X POST \
"http://wwz-$i:9000/admin/reset" -H "Authorization: Bearer $WWZ_ADMIN_TOKEN" >/dev/null || true
done
}

case "${1:-}" in
iniciar)
if [ "${2:-}" = "--sem-freios" ]; then
echo "CUIDADO: sem limites, o stress disputa CPU, memoria e disco com os outros servicos do servidor.";
read -r -p "Digite SIM para continuar: " ok
[ "$ok" = "SIM" ] || { echo "cancelado"; exit 1; }
cp sem-freios.override.yml "$OVERRIDE"
(cd /opt/thinker && docker compose up -d --wait >/dev/null)
REPLICAS=${REPLICAS:-$(( $(nproc) / 4 ))}
export MAX_DEGRAUS=${MAX_DEGRAUS:-1000000}
else
rm -f "$OVERRIDE"
REPLICAS=${REPLICAS:-3}
export MAX_DEGRAUS=${MAX_DEGRAUS:-10}
fi
/opt/thinker/escala.sh "$REPLICAS" | tail -"$REPLICAS"
zera_wwz
docker exec thinker-postgres psql -U thinker -d thinker -qc 'select pg_stat_statements_reset()' >/dev/null || true
mkdir -p reports
# cada rodada comeca um loop.log novo; o anterior fica com a data
[ -f reports/loop.log ] && mv reports/loop.log "reports/loop-$(date -u +%Y%m%d-%H%M%S).log"
setsid nohup ./loop.sh iniciar > reports/loop.out 2>&1 < /dev/null &
sleep 3
echo "loop iniciado: $REPLICAS replicas, MAX_DEGRAUS=$MAX_DEGRAUS, DEGRAU=${DEGRAU:-120}s";
echo "acompanhe: tail -f $(pwd)/reports/loop.log | pare: $(pwd)/stress.sh parar";
;;
status)
./loop.sh status
docker ps --filter name=thinker-app --format '{{.Names}} {{.Status}}' | sort -V
[ -f "$OVERRIDE" ] && echo "modo: sem freios" || echo "modo: com freios"
;;
parar)
./loop.sh parar
rm -f "$OVERRIDE"
/opt/thinker/escala.sh 1 | tail -1
(cd /opt/thinker && rm -f docker-compose.replicas.yml && docker compose up -d --wait >/dev/null)
echo "Thinker de volta a 1 replica, com os limites do compose";
;;
*) sed -n '/^# Dispara/,/^set -euo/p' "$0" | sed '$d'; exit 2 ;;
esac
9 changes: 9 additions & 0 deletions deploy/stress/opt/thinker-stress/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# /opt/thinker-stress/.env (chmod 600; ver tutorial #1, secao 5.5)
# thinker agent key show wwz-generico --raw
THINKER_AGENT_KEY=thk_troque
# copiados de /opt/wwz-openai/.env
WWZ_KEY_1=wwz-key-troque
WWZ_ADMIN_TOKEN=troque
THINKER_HOST=thinker.seudominio.com.br
TRAEFIK_IP=172.20.255.253
WWZ_LOAD_IMAGE=white-wizard-load:local
Loading
Loading