From 955229b55ea387578ffdd2fb6a3d58e9a6d45da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Brand=C3=A3o?= Date: Sat, 7 Nov 2026 18:51:21 -0300 Subject: [PATCH 1/2] feat(deploy): ambiente de stress Thinker + White Wizard em deploy/stress (composes, configs do wwz e os scripts stress.sh, provisionar.sh, loop.sh e escala.sh) The files the two white-wizard tutorials build, mirrored from the test server's /opt and checked byte for byte against it, with .env.example in place of every secret. The new thinker-stress-02/stress.sh starts the endless stress with brakes by default (3 replicas, 10 steps, compose limits) or --sem-freios after a typed confirmation, and stops it cleanly. Co-Authored-By: Claude Opus 5.5 --- deploy/stress/.gitignore | 7 + deploy/stress/README.md | 43 ++++++ .../stress/opt/thinker-stress-02/.env.example | 9 ++ deploy/stress/opt/thinker-stress-02/loop.sh | 93 +++++++++++++ .../opt/thinker-stress-02/provisionar.sh | 121 +++++++++++++++++ .../thinker-stress-02/sem-freios.override.yml | 13 ++ deploy/stress/opt/thinker-stress-02/stress.sh | 64 +++++++++ deploy/stress/opt/thinker-stress/.env.example | 9 ++ deploy/stress/opt/thinker-stress/stress.sh | 72 ++++++++++ deploy/stress/opt/thinker/.env.example | 12 ++ deploy/stress/opt/thinker/docker-compose.yml | 125 ++++++++++++++++++ deploy/stress/opt/thinker/escala.sh | 36 +++++ deploy/stress/opt/wwz-openai/.env.example | 7 + .../stress/opt/wwz-openai/docker-compose.yml | 101 ++++++++++++++ deploy/stress/opt/wwz-openai/wwz.yaml | 54 ++++++++ deploy/stress/opt/wwz-todos/.env.example | 9 ++ .../stress/opt/wwz-todos/docker-compose.yml | 119 +++++++++++++++++ deploy/stress/opt/wwz-todos/todos.yaml | 62 +++++++++ .../tutoriais/thinker-with-white-wizard-01.md | 2 + .../tutoriais/thinker-with-white-wizard-02.md | 17 +++ 20 files changed, 975 insertions(+) create mode 100644 deploy/stress/.gitignore create mode 100644 deploy/stress/README.md create mode 100644 deploy/stress/opt/thinker-stress-02/.env.example create mode 100755 deploy/stress/opt/thinker-stress-02/loop.sh create mode 100755 deploy/stress/opt/thinker-stress-02/provisionar.sh create mode 100644 deploy/stress/opt/thinker-stress-02/sem-freios.override.yml create mode 100755 deploy/stress/opt/thinker-stress-02/stress.sh create mode 100644 deploy/stress/opt/thinker-stress/.env.example create mode 100755 deploy/stress/opt/thinker-stress/stress.sh create mode 100644 deploy/stress/opt/thinker/.env.example create mode 100644 deploy/stress/opt/thinker/docker-compose.yml create mode 100755 deploy/stress/opt/thinker/escala.sh create mode 100644 deploy/stress/opt/wwz-openai/.env.example create mode 100644 deploy/stress/opt/wwz-openai/docker-compose.yml create mode 100644 deploy/stress/opt/wwz-openai/wwz.yaml create mode 100644 deploy/stress/opt/wwz-todos/.env.example create mode 100644 deploy/stress/opt/wwz-todos/docker-compose.yml create mode 100644 deploy/stress/opt/wwz-todos/todos.yaml diff --git a/deploy/stress/.gitignore b/deploy/stress/.gitignore new file mode 100644 index 00000000..9c40f53d --- /dev/null +++ b/deploy/stress/.gitignore @@ -0,0 +1,7 @@ +# segredos e artefatos gerados no servidor +.env +harnesses.keys +run/ +reports/ +docker-compose.replicas.yml +docker-compose.override.yml diff --git a/deploy/stress/README.md b/deploy/stress/README.md new file mode 100644 index 00000000..c1728483 --- /dev/null +++ b/deploy/stress/README.md @@ -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). diff --git a/deploy/stress/opt/thinker-stress-02/.env.example b/deploy/stress/opt/thinker-stress-02/.env.example new file mode 100644 index 00000000..72c6c327 --- /dev/null +++ b/deploy/stress/opt/thinker-stress-02/.env.example @@ -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) diff --git a/deploy/stress/opt/thinker-stress-02/loop.sh b/deploy/stress/opt/thinker-stress-02/loop.sh new file mode 100755 index 00000000..74e18f6e --- /dev/null +++ b/deploy/stress/opt/thinker-stress-02/loop.sh @@ -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 +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 diff --git a/deploy/stress/opt/thinker-stress-02/provisionar.sh b/deploy/stress/opt/thinker-stress-02/provisionar.sh new file mode 100755 index 00000000..b807e067 --- /dev/null +++ b/deploy/stress/opt/thinker-stress-02/provisionar.sh @@ -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 : 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 + 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 + 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 + +# 3 - habilita todos os provedores em todos os harnesses +harnesses=$($T agent list /dev/null 2>&1 || true + done + done +done + +# 4 - rota padrao: . 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 > .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 + 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 > harnesses.keys +done +echo "chaves em $(pwd)/harnesses.keys"; diff --git a/deploy/stress/opt/thinker-stress-02/sem-freios.override.yml b/deploy/stress/opt/thinker-stress-02/sem-freios.override.yml new file mode 100644 index 00000000..e485991a --- /dev/null +++ b/deploy/stress/opt/thinker-stress-02/sem-freios.override.yml @@ -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] diff --git a/deploy/stress/opt/thinker-stress-02/stress.sh b/deploy/stress/opt/thinker-stress-02/stress.sh new file mode 100755 index 00000000..002343ad --- /dev/null +++ b/deploy/stress/opt/thinker-stress-02/stress.sh @@ -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 diff --git a/deploy/stress/opt/thinker-stress/.env.example b/deploy/stress/opt/thinker-stress/.env.example new file mode 100644 index 00000000..59c78c38 --- /dev/null +++ b/deploy/stress/opt/thinker-stress/.env.example @@ -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 diff --git a/deploy/stress/opt/thinker-stress/stress.sh b/deploy/stress/opt/thinker-stress/stress.sh new file mode 100755 index 00000000..ae2eecc2 --- /dev/null +++ b/deploy/stress/opt/thinker-stress/stress.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# /opt/thinker-stress/stress.sh +# Stress do Thinker com o wwz-load (White Wizard). +# Uso: ./stress.sh [flags extras do wwz-load] +# base carga direto no wwz-openai (teto do simulador) +# conc N concorrencia fixa N pelo Thinker (padrao 100) +# rps N taxa fixa N req/s pelo Thinker (padrao 200) +# rampa taxa crescendo de 50 a 2000 req/s +# sweep degraus 50,100,200,400,800,1600 req/s +# agentes N N agentes multi-turno com ferramentas (padrao 100) +# traefik N concorrencia N passando pelo Traefik com TLS (padrao 100) +# falhas N concorrencia N no modelo virtual com falhas e fallback +# top painel ao vivo do wwz-openai +set -euo pipefail +cd "$(dirname "$0")" +. ./.env + +IMG=${WWZ_LOAD_IMAGE:-white-wizard-load:local} +NET=${NET:-network_public} +THINKER=${THINKER:-http://thinker-app:2099} +DUR=${DUR:-60s} +MODEL=${MODEL:-auto} +STAMP=$(date -u +%Y%m%d-%H%M%S) +mkdir -p reports && chmod 777 reports + +# executa o wwz-load num container descartavel na rede dos servicos +wl() { + docker run --rm --network "$NET" \ + --ulimit nofile=1048576:1048576 \ + ${EXTRA_DOCKER_ARGS:-} \ + -e WWZ_LOAD_KEY="${KEY:-$THINKER_AGENT_KEY}" \ + -e WWZ_ADMIN_TOKEN="$WWZ_ADMIN_TOKEN" \ + -v "$PWD/reports:/reports" "$IMG" wwz-load "$@" +} + +modo=${1:-conc}; shift || true +case "$modo" in + base) + KEY=$WWZ_KEY_1 wl run --target http://wwz-openai:8000 --concurrency "${1:-100}" \ + --duration "$DUR" --admin http://wwz-openai:9000 --output json --out /reports/base-$STAMP.json "${@:2}" ;; + conc) + wl run --target "$THINKER" --model "$MODEL" --concurrency "${1:-100}" --duration "$DUR" \ + --warmup 5s --output json --out /reports/conc-$STAMP.json "${@:2}" ;; + rps) + wl run --target "$THINKER" --model "$MODEL" --rps "${1:-200}" --duration "$DUR" \ + --max-inflight 20000 --output json --out /reports/rps-$STAMP.json "${@:2}" ;; + rampa) + wl run --target "$THINKER" --model "$MODEL" --ramp 50:2000:5m --step-duration 15s \ + --max-inflight 20000 --output json --out /reports/rampa-$STAMP.json "$@" ;; + sweep) + wl run --target "$THINKER" --model "$MODEL" --sweep --sweep-rates 50,100,200,400,800,1600 \ + --step-duration 30s --sweep-stop-error-pct 30 --max-inflight 20000 \ + --output json --out /reports/sweep-$STAMP.json "$@" ;; + agentes) + wl agents --target "$THINKER" --model "$MODEL" --agents "${1:-100}" --profile mixed \ + --turns 5 --duration "$DUR" --retries 2 --output json --out /reports/agentes-$STAMP.json "${@:2}" ;; + traefik) + EXTRA_DOCKER_ARGS="--add-host $THINKER_HOST:$TRAEFIK_IP" \ + wl run --target "https://$THINKER_HOST" --model "$MODEL" --concurrency "${1:-100}" \ + --duration "$DUR" --output json --out /reports/traefik-$STAMP.json "${@:2}" ;; + falhas) + wl run --target "$THINKER" --model wwz-falhas --concurrency "${1:-100}" --duration "$DUR" \ + --output json --out /reports/falhas-$STAMP.json "${@:2}" ;; + top) + docker run --rm -it --network "$NET" -e WWZ_ADMIN_TOKEN="$WWZ_ADMIN_TOKEN" "$IMG" \ + wwz-load top --admin http://wwz-openai:9000 ;; + *) sed -n 3,15p "$0"; exit 2 ;; +esac + +# resumo do ultimo relatorio +ultimo=$(ls -t reports/*.json 2>/dev/null | head -1 || true) +[ -n "$ultimo" ] && jq -c '(.total // {}) | {requests, ok, errors, rps, ttft_p50: .ttft_ms.p50, ttft_p99: .ttft_ms.p99, e2e_p99: .e2e_ms.p99, errors_by_type, sessions}' "$ultimo"; diff --git a/deploy/stress/opt/thinker/.env.example b/deploy/stress/opt/thinker/.env.example new file mode 100644 index 00000000..0a571a3e --- /dev/null +++ b/deploy/stress/opt/thinker/.env.example @@ -0,0 +1,12 @@ +# /opt/thinker/.env (chmod 600; ver tutorial #1, secao 4.1) +DOMINIO=seudominio.com.br +# fixe a versao testada em vez de latest +THINKER_VERSION=latest +BETTER_AUTH_SECRET=troque-openssl-rand-hex-32 +THINKER_ENCRYPTION_KEY=troque-outro-openssl-rand-hex-32 +THINKER_SETUP_TOKEN=troque-openssl-rand-hex-24 +POSTGRES_PASSWORD=troque-openssl-rand-hex-24 +THINKER_ADMIN_EMAIL=stress@thinker.local +THINKER_ADMIN_PASSWORD=troque-openssl-rand-hex-16 +# IP do Traefik na network_public (docker inspect traefik-app) +TRUSTED_PROXIES=172.20.255.253 diff --git a/deploy/stress/opt/thinker/docker-compose.yml b/deploy/stress/opt/thinker/docker-compose.yml new file mode 100644 index 00000000..befeb425 --- /dev/null +++ b/deploy/stress/opt/thinker/docker-compose.yml @@ -0,0 +1,125 @@ +# /opt/thinker/docker-compose.yml +# Thinker para teste de stress (thinker-app + thinker-postgres) atras do Traefik. +name: thinker + +services: + thinker-postgres: + image: postgres:18-alpine + container_name: thinker-postgres + restart: unless-stopped + shm_size: 2g + cpus: 8 + mem_limit: 16g + environment: + POSTGRES_USER: thinker + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: thinker + command: + - postgres + - -c + - max_connections=400 + - -c + - shared_buffers=4GB + - -c + - effective_cache_size=12GB + - -c + - work_mem=16MB + - -c + - maintenance_work_mem=512MB + - -c + - wal_buffers=64MB + - -c + - max_wal_size=8GB + - -c + - checkpoint_timeout=15min + - -c + - synchronous_commit=off + - -c + - random_page_cost=1.1 + - -c + - track_io_timing=on + - -c + - shared_preload_libraries=pg_stat_statements + - -c + - pg_stat_statements.track=top + volumes: [pgdata:/var/lib/postgresql] + healthcheck: + test: pg_isready -U thinker + interval: 5s + timeout: 3s + retries: 10 + security_opt: ["no-new-privileges:true"] + networks: [interna] + + thinker-app: + image: tmsoftbrasil/thinker:${THINKER_VERSION:-latest} + container_name: thinker-app + restart: unless-stopped + stop_grace_period: 30s + cpus: 8 + mem_limit: 8g + pids_limit: 4096 + ulimits: + nofile: {soft: 1048576, hard: 1048576} + read_only: true + tmpfs: ["/tmp:size=256m"] + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + environment: + PORT: "2099" + DATABASE_URL: postgresql://thinker:${POSTGRES_PASSWORD}@thinker-postgres:5432/thinker + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} + THINKER_ENCRYPTION_KEY: ${THINKER_ENCRYPTION_KEY} + THINKER_SETUP_TOKEN: ${THINKER_SETUP_TOKEN} + BETTER_AUTH_URL: https://thinker.${DOMINIO} + TRUSTED_PROXIES: ${TRUSTED_PROXIES} + # provedores simulados na rede Docker (http://wwz-*:8000) sao URLs privadas + THINKER_PROVIDER_PRIVATE_URLS: allow + # sem catalogos publicos: o teste nao depende da internet + THINKER_CATALOG_SYNC: "false" + # guardrails do proxy levantados para o stress (padroes: 10, 200/min, 500/min) + THINKER_CONCURRENCY_MAX: ${THINKER_CONCURRENCY_MAX:-100000} + THINKER_RATE_MAX_REQUESTS: ${THINKER_RATE_MAX_REQUESTS:-100000000} + THINKER_IP_RATE_MAX_REQUESTS: ${THINKER_IP_RATE_MAX_REQUESTS:-100000000} + THROTTLE_LIMIT: "10000" + DB_POOL_MAX: ${DB_POOL_MAX:-80} + AUTH_DB_POOL_MAX: "10" + PROVIDER_TIMEOUT_MS: "180000" + THINKER_RECORDING_STORAGE: auto + THINKER_RECORDING_FILESYSTEM_PATH: /data/request-recordings + NODE_OPTIONS: --max-old-space-size=6144 + UV_THREADPOOL_SIZE: "16" + volumes: [recordings:/data/request-recordings] + depends_on: + thinker-postgres: {condition: service_healthy} + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:2099/api/v1/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 15s + timeout: 5s + start_period: 90s + retries: 3 + networks: + interna: {} + network_public: + # alias comum: as replicas do tutorial #2 entram nele + aliases: [thinker-pool] + labels: + traefik.enable: "true" + traefik.docker.network: network_public + traefik.http.routers.thinker.entrypoints: web,websecure + traefik.http.routers.thinker.rule: Host(`thinker.${DOMINIO}`) + traefik.http.routers.thinker.tls: "true" + traefik.http.routers.thinker.tls.certresolver: letsencrypt + traefik.http.services.thinker.loadbalancer.server.port: "2099" + +volumes: + pgdata: {name: thinker_pgdata} + recordings: {name: thinker_recordings} + +networks: + interna: + name: thinker-interna + internal: true + network_public: + external: true + name: network_public diff --git a/deploy/stress/opt/thinker/escala.sh b/deploy/stress/opt/thinker/escala.sh new file mode 100755 index 00000000..b5dd8847 --- /dev/null +++ b/deploy/stress/opt/thinker/escala.sh @@ -0,0 +1,36 @@ +#!/bin/bash +# /opt/thinker/escala.sh +# Sobe N replicas do Thinker (thinker-app + thinker-app-2..N) atras do mesmo +# alias de rede (thinker-pool) e do mesmo servico do Traefik. +# Uso: ./escala.sh N (N=1 volta a uma replica so) +set -euo pipefail +cd /opt/thinker +N=${1:?informe o numero de replicas} +POOL=${DB_POOL_POR_REPLICA:-40} + +{ + echo "# gerado por escala.sh: replicas extras do thinker-app" + if [ "$N" -lt 2 ]; then echo "services: {}"; else echo "services:"; fi + i=2 + while [ "$i" -le "$N" ]; do + cat < docker-compose.replicas.yml + +# replicas que sobraram de uma escala maior +for c in $(docker ps -a --format '{{.Names}}' | grep -E '^thinker-app-[0-9]+$' || true); do + if [ "${c#thinker-app-}" -gt "$N" ]; then docker rm -f "$c" >/dev/null; echo "removida $c"; fi; +done + +DB_POOL_MAX=$POOL docker compose -f docker-compose.yml -f docker-compose.replicas.yml up -d --wait +docker ps --filter name=thinker-app --format '{{.Names}} {{.Status}}' | sort -V diff --git a/deploy/stress/opt/wwz-openai/.env.example b/deploy/stress/opt/wwz-openai/.env.example new file mode 100644 index 00000000..e3a33082 --- /dev/null +++ b/deploy/stress/opt/wwz-openai/.env.example @@ -0,0 +1,7 @@ +# /opt/wwz-openai/.env (chmod 600; gere os valores com openssl rand, ver tutorial #1, secao 3.1) +DOMINIO=seudominio.com.br +WWZ_ADMIN_TOKEN=troque-openssl-rand-hex-24 +WWZ_PANEL_ADMIN_PASSWORD=troque-openssl-rand-hex-12 +WWZ_KEY_1=wwz-key-troque +WWZ_KEY_2=wwz-key-troque +WWZ_KEY_3=wwz-key-troque diff --git a/deploy/stress/opt/wwz-openai/docker-compose.yml b/deploy/stress/opt/wwz-openai/docker-compose.yml new file mode 100644 index 00000000..e3f4e784 --- /dev/null +++ b/deploy/stress/opt/wwz-openai/docker-compose.yml @@ -0,0 +1,101 @@ +# /opt/wwz-openai/docker-compose.yml +# White Wizard somente OpenAI: API (wwz-openai), Valkey e painel admin. +name: wwz-openai + +services: + wwz-openai: + image: white-wizard:local + container_name: wwz-openai + restart: unless-stopped + read_only: true + tmpfs: [/run, /tmp] + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + cpus: ${WWZ_CPUS:-4} + mem_limit: ${WWZ_MEM:-8g} + ulimits: + nofile: {soft: 1048576, hard: 1048576} + environment: + WWZ_CONFIG: /etc/wwz/wwz.yaml + WWZ_ADMIN_TOKEN: ${WWZ_ADMIN_TOKEN} + WWZ_VALKEY_ADDR: wwz-openai-valkey:6379 + WWZ_LOG_LEVEL: warn + GOGC: "200" + volumes: ["./run/wwz.yaml:/etc/wwz/wwz.yaml:ro"] + depends_on: + wwz-openai-valkey: {condition: service_healthy} + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck", "http://127.0.0.1:9000/health/ready"] + interval: 5s + timeout: 3s + retries: 10 + networks: [interna, network_public] + labels: + traefik.enable: "true" + traefik.docker.network: network_public + traefik.http.routers.wwz-openai.entrypoints: web,websecure + traefik.http.routers.wwz-openai.rule: Host(`wwz-openai.${DOMINIO}`) + traefik.http.routers.wwz-openai.tls: "true" + traefik.http.routers.wwz-openai.tls.certresolver: letsencrypt + traefik.http.routers.wwz-openai.service: wwz-openai + traefik.http.services.wwz-openai.loadbalancer.server.port: "8000" + + wwz-openai-valkey: + image: valkey/valkey:8.1-alpine + container_name: wwz-openai-valkey + restart: unless-stopped + user: "999:999" + command: ["valkey-server", "--save", "", "--appendonly", "no", "--maxmemory", "2gb"] + tmpfs: [/run, /data] + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + timeout: 2s + retries: 10 + networks: [interna] + + wwz-openai-panel: + image: white-wizard-panel:local + container_name: wwz-openai-panel + restart: unless-stopped + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + environment: + WWZ_PANEL_LISTEN: ":8100" + WWZ_PANEL_DB: /data/panel.db + WWZ_PANEL_PUBLIC_URL: https://wwz-openai-panel.${DOMINIO} + WWZ_PANEL_VALKEY_ADDR: wwz-openai-valkey:6379 + WWZ_PANEL_WWZ_ADMIN_URL: http://wwz-openai:9000 + WWZ_ADMIN_TOKEN: ${WWZ_ADMIN_TOKEN} + WWZ_PANEL_ADMIN_USER: admin + WWZ_PANEL_ADMIN_PASSWORD: ${WWZ_PANEL_ADMIN_PASSWORD} + volumes: [panel-data:/data] + depends_on: + wwz-openai: {condition: service_healthy} + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck", "http://127.0.0.1:8100/health/ready"] + interval: 5s + timeout: 3s + retries: 10 + networks: [interna, network_public] + labels: + traefik.enable: "true" + traefik.docker.network: network_public + traefik.http.routers.wwz-openai-panel.entrypoints: web,websecure + traefik.http.routers.wwz-openai-panel.rule: Host(`wwz-openai-panel.${DOMINIO}`) + traefik.http.routers.wwz-openai-panel.tls: "true" + traefik.http.routers.wwz-openai-panel.tls.certresolver: letsencrypt + traefik.http.routers.wwz-openai-panel.service: wwz-openai-panel + traefik.http.services.wwz-openai-panel.loadbalancer.server.port: "8100" + +volumes: + panel-data: {name: wwz-openai-panel-data} + +networks: + interna: + name: wwz-openai-interna + network_public: + external: true + name: network_public diff --git a/deploy/stress/opt/wwz-openai/wwz.yaml b/deploy/stress/opt/wwz-openai/wwz.yaml new file mode 100644 index 00000000..d1fb0dd1 --- /dev/null +++ b/deploy/stress/opt/wwz-openai/wwz.yaml @@ -0,0 +1,54 @@ +# /opt/wwz-openai/wwz.yaml +# White Wizard somente OpenAI, para estressar o Thinker. +server: + listen: ":8000" + auth: keys + reuseport: true + concurrency: 1000000 + trusted_proxies: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] + +admin: + listen: ":9000" + +# eventos no Valkey para o painel (wwz-panel) +store: + event_stream: true + event_sample: 1 + +drivers: + - {name: openai} + +# contas: da 01 (ilimitada e perfeita) a 03 (apertada e com falhas) +accounts: + - {id: conta-01} + - {id: conta-02, limits: {rpm: 60000, tpm: 10000000, concurrency: 5000}} + - {id: conta-03, limits: {rpm: 600, tpm: 100000, concurrency: 50}} + +keys: + - {key: ${WWZ_KEY_1}, label: conta-01, account: conta-01, errors: sem-erro, latency: rapida} + - {key: ${WWZ_KEY_2}, label: conta-02, account: conta-02, errors: erro-05, latency: realista} + - {key: ${WWZ_KEY_3}, label: conta-03, account: conta-03, errors: erro-20, latency: realista} + +profiles: + latency: + rapida: {ttft: 0ms, itl: 0ms} + realista: {queue: {dist: lognormal, median: 20ms, p99: 300ms}, ttft: {dist: normal, mean: 450ms, stddev: 120ms}, itl: {dist: normal, mean: 18ms, stddev: 4ms}} + errors: + sem-erro: {rate: 0} + erro-05: {rate: 5, types: [rate_limit, server_error, overloaded], mid_stream_rate: 1} + erro-20: {rate: 20, types: [rate_limit, server_error, overloaded, bad_gateway, gateway_timeout, connection_reset], mid_stream_rate: 6, mid_stream_types: [stream_truncated, stream_error_event]} + +models: + - id: wwz-fast + owned_by: white-wizard + protocols: [openai] + context_window: 128000 + capabilities: {tools: true, reasoning: true, json_schema: true} + generation: default + - id: gpt-5-sim + owned_by: white-wizard + protocols: [openai] + context_window: 400000 + capabilities: {tools: true, reasoning: true, json_schema: true} + latency: realista + generation: default diff --git a/deploy/stress/opt/wwz-todos/.env.example b/deploy/stress/opt/wwz-todos/.env.example new file mode 100644 index 00000000..22c78d35 --- /dev/null +++ b/deploy/stress/opt/wwz-todos/.env.example @@ -0,0 +1,9 @@ +# /opt/wwz-todos/.env (chmod 600; ver tutorial #2, secao 2). Uma chave por instancia: +# o Thinker reaproveita a conexao quando a chave se repete. +DOMINIO=seudominio.com.br +WWZ_ADMIN_TOKEN=troque-openssl-rand-hex-24 +WWZ_PANEL_ADMIN_PASSWORD=troque-openssl-rand-hex-12 +WWZ_KEY_1=wwz-key-troque +WWZ_KEY_2=wwz-key-troque +WWZ_KEY_3=wwz-key-troque +WWZ_KEY_4=wwz-key-troque diff --git a/deploy/stress/opt/wwz-todos/docker-compose.yml b/deploy/stress/opt/wwz-todos/docker-compose.yml new file mode 100644 index 00000000..e4fd777e --- /dev/null +++ b/deploy/stress/opt/wwz-todos/docker-compose.yml @@ -0,0 +1,119 @@ +# /opt/wwz-todos/docker-compose.yml +# White Wizard com os seis drivers em quatro instancias de comportamento +# diferente (perfeito, falhas, lento, instavel), Valkey e painel admin. +name: wwz-todos + +x-wwz: &wwz + image: white-wizard:local + restart: unless-stopped + read_only: true + tmpfs: [/run, /tmp] + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + cpus: ${WWZ_CPUS:-4} + mem_limit: ${WWZ_MEM:-8g} + ulimits: + nofile: {soft: 1048576, hard: 1048576} + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck", "http://127.0.0.1:9000/health/ready"] + interval: 5s + timeout: 3s + retries: 10 + networks: [interna, network_public] + +services: + wwz-perfeito: + <<: *wwz + container_name: wwz-perfeito + environment: + WWZ_CONFIG: /etc/wwz/wwz.yaml + WWZ_ADMIN_TOKEN: ${WWZ_ADMIN_TOKEN} + WWZ_VALKEY_ADDR: wwz-todos-valkey:6379 + WWZ_LOG_LEVEL: warn + GOGC: "200" + volumes: ["./run/perfeito.yaml:/etc/wwz/wwz.yaml:ro"] + depends_on: + wwz-todos-valkey: {condition: service_healthy} + + wwz-falhas: + <<: *wwz + container_name: wwz-falhas + environment: &memoria + WWZ_CONFIG: /etc/wwz/wwz.yaml + WWZ_ADMIN_TOKEN: ${WWZ_ADMIN_TOKEN} + WWZ_STORE: memory + WWZ_LOG_LEVEL: warn + GOGC: "200" + volumes: ["./run/falhas.yaml:/etc/wwz/wwz.yaml:ro"] + + wwz-lento: + <<: *wwz + container_name: wwz-lento + environment: *memoria + volumes: ["./run/lento.yaml:/etc/wwz/wwz.yaml:ro"] + + wwz-instavel: + <<: *wwz + container_name: wwz-instavel + environment: *memoria + volumes: ["./run/instavel.yaml:/etc/wwz/wwz.yaml:ro"] + + wwz-todos-valkey: + image: valkey/valkey:8.1-alpine + container_name: wwz-todos-valkey + restart: unless-stopped + user: "999:999" + command: ["valkey-server", "--save", "", "--appendonly", "no", "--maxmemory", "2gb"] + tmpfs: [/run, /data] + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 5s + timeout: 2s + retries: 10 + networks: [interna] + + wwz-todos-panel: + image: white-wizard-panel:local + container_name: wwz-todos-panel + restart: unless-stopped + security_opt: ["no-new-privileges:true"] + cap_drop: [ALL] + environment: + WWZ_PANEL_LISTEN: ":8100" + WWZ_PANEL_DB: /data/panel.db + WWZ_PANEL_PUBLIC_URL: https://wwz-todos-panel.${DOMINIO} + WWZ_PANEL_VALKEY_ADDR: wwz-todos-valkey:6379 + WWZ_PANEL_WWZ_ADMIN_URL: http://wwz-perfeito:9000 + WWZ_ADMIN_TOKEN: ${WWZ_ADMIN_TOKEN} + WWZ_PANEL_ADMIN_USER: admin + WWZ_PANEL_ADMIN_PASSWORD: ${WWZ_PANEL_ADMIN_PASSWORD} + volumes: [panel-data:/data] + depends_on: + wwz-perfeito: {condition: service_healthy} + healthcheck: + test: ["CMD", "/usr/local/bin/healthcheck", "http://127.0.0.1:8100/health/ready"] + interval: 5s + timeout: 3s + retries: 10 + networks: [interna, network_public] + labels: + traefik.enable: "true" + traefik.docker.network: network_public + traefik.http.routers.wwz-todos-panel.entrypoints: web,websecure + traefik.http.routers.wwz-todos-panel.rule: Host(`wwz-todos-panel.${DOMINIO}`) + traefik.http.routers.wwz-todos-panel.tls: "true" + traefik.http.routers.wwz-todos-panel.tls.certresolver: letsencrypt + traefik.http.routers.wwz-todos-panel.service: wwz-todos-panel + traefik.http.services.wwz-todos-panel.loadbalancer.server.port: "8100" + +volumes: + panel-data: {name: wwz-todos-panel-data} + +networks: + interna: + name: wwz-todos-interna + network_public: + external: true + name: network_public diff --git a/deploy/stress/opt/wwz-todos/todos.yaml b/deploy/stress/opt/wwz-todos/todos.yaml new file mode 100644 index 00000000..e6528da7 --- /dev/null +++ b/deploy/stress/opt/wwz-todos/todos.yaml @@ -0,0 +1,62 @@ +# /opt/wwz-todos/todos.yaml +# White Wizard com os seis drivers por prefixo. Modelo renderizado com envsubst: +# ${WWZ_KEY_1..4} (chaves) e ${WWZ_ERROS}/${WWZ_LATENCIA} (perfil desta instancia). +server: + listen: ":8000" + auth: keys + reuseport: true + concurrency: 1000000 + trusted_proxies: [10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16] + +admin: + listen: ":9000" + +store: + event_stream: true + +usage: + extended: true + +drivers: + - {name: openai, prefix: /openai} + - {name: anthropic, prefix: /anthropic} + - {name: openrouter, prefix: /openrouter} + # gratuitos: 200 requisicoes por dia por padrao, como no real + - {name: opencode, prefix: /opencode, options: {free_requests_per_day: 1000000000}} + - {name: gemini, prefix: /gemini} + - {name: azure, prefix: /azure} + +accounts: + - {id: conta-stress} + +keys: + - {key: ${WWZ_KEY_1}, label: stress-1, account: conta-stress} + - {key: ${WWZ_KEY_2}, label: stress-2, account: conta-stress} + - {key: ${WWZ_KEY_3}, label: stress-3, account: conta-stress} + - {key: ${WWZ_KEY_4}, label: stress-4, account: conta-stress} + +profiles: + latency: + rapida: {ttft: 0ms, itl: 0ms} + realista: {queue: {dist: lognormal, median: 20ms, p99: 300ms}, ttft: {dist: normal, mean: 450ms, stddev: 120ms}, itl: {dist: normal, mean: 18ms, stddev: 4ms}} + cauda: {queue: {dist: lognormal, median: 50ms, p99: 8s}, ttft: {dist: lognormal, median: 800ms, p99: 20s}, itl: {dist: normal, mean: 30ms, stddev: 15ms}} + errors: + sem-erro: {rate: 0} + falhas-20: {rate: 20, types: [rate_limit, server_error, overloaded, bad_gateway, gateway_timeout, connection_reset], mid_stream_rate: 6, mid_stream_types: [stream_truncated, stream_error_event]} + instavel: + rate: 2 + types: [server_error] + outages: + - {schedule: "0 */10 * * * *", duration: 2m, type: overloaded} + +models: + - {id: wwz-fast, owned_by: white-wizard, protocols: ["*"], context_window: 128000, capabilities: {tools: true, reasoning: true, json_schema: true}, latency: "${WWZ_LATENCIA}", errors: "${WWZ_ERROS}", generation: default} + - {id: wwz-realistic, owned_by: white-wizard, protocols: ["*"], context_window: 128000, capabilities: {tools: true, reasoning: true}, latency: realista, errors: "${WWZ_ERROS}", generation: default} + # ids que os clientes Anthropic e o Claude Code pedem + - {id: claude-sonnet-5-5, owned_by: anthropic, drivers: [anthropic], protocols: [anthropic], context_window: 1000000, max_output: 128000, capabilities: {tools: true, reasoning: true, json_schema: true}, pricing: {input_per_m: 2, output_per_m: 10}, latency: "${WWZ_LATENCIA}", errors: "${WWZ_ERROS}", generation: default} + - {id: claude-haiku-5-5, owned_by: anthropic, drivers: [anthropic], protocols: [anthropic], context_window: 1000000, max_output: 128000, capabilities: {tools: true, reasoning: true, json_schema: true}, pricing: {input_per_m: 0.1, output_per_m: 0.5}, latency: "${WWZ_LATENCIA}", errors: "${WWZ_ERROS}", generation: default} + # ids do catalogo real do OpenCode: o Thinker descobre os modelos do OpenCode + # pelo catalogo dele, nao pelo /v1/models da base configurada + - {id: glm-5.3, owned_by: zhipu, drivers: [opencode], protocols: [openai], catalogs: {opencode-go: {}}, latency: "${WWZ_LATENCIA}", errors: "${WWZ_ERROS}", generation: default} + - {id: kimi-k3, owned_by: moonshot, drivers: [opencode], protocols: [openai], catalogs: {opencode-go: {}}, latency: "${WWZ_LATENCIA}", errors: "${WWZ_ERROS}", generation: default} + - {id: big-pickle, owned_by: opencode, drivers: [opencode], protocols: [openai], catalogs: {opencode: {pricing: {input_per_m: 0, output_per_m: 0}}}, latency: "${WWZ_LATENCIA}", errors: "${WWZ_ERROS}", generation: default} diff --git a/docs/tutoriais/thinker-with-white-wizard-01.md b/docs/tutoriais/thinker-with-white-wizard-01.md index 46ef333d..566afa49 100644 --- a/docs/tutoriais/thinker-with-white-wizard-01.md +++ b/docs/tutoriais/thinker-with-white-wizard-01.md @@ -11,6 +11,8 @@ Pré-requisitos: - Acesso ao repositório do **White Wizard** (`github.com/patrickbrandao/white-wizard`); - `jq`, `envsubst` (pacote `gettext-base`) e `openssl` no servidor. +Todos os arquivos deste tutorial também estão prontos em [`deploy/stress/opt/`](../../deploy/stress/README.md), no mesmo caminho do servidor, com `.env.example` no lugar dos segredos. + Todos os comandos rodam como **root** no servidor. Onde aparecer `seudominio.com.br`, troque pelo seu domínio. ## 1 – Como funciona diff --git a/docs/tutoriais/thinker-with-white-wizard-02.md b/docs/tutoriais/thinker-with-white-wizard-02.md index 7e11699c..8276925e 100644 --- a/docs/tutoriais/thinker-with-white-wizard-02.md +++ b/docs/tutoriais/thinker-with-white-wizard-02.md @@ -8,6 +8,8 @@ Pré-requisitos: - DNS de `wwz-todos-panel.seudominio.com.br` apontando para o servidor; - Um servidor que você **aceita derrubar**. O loop da seção 8 não tem freio. +Os arquivos deste tutorial (e o `stress.sh` de disparo da seção 8.4) estão em [`deploy/stress/opt/`](../../deploy/stress/README.md), no mesmo caminho do servidor. + **CRÍTICO**: não rode este teste num servidor que tem outros serviços de produção. Ele foi feito para esgotar recursos; o kernel vai começar a matar processo por falta de memória e o disco vai encher de gravações. Você foi avisado. ## 1 – O que muda em relação ao #1 @@ -762,6 +764,21 @@ sysctl fs.file-max net.core.somaxconn net.ipv4.ip_local_port_range; Com dezenas de milhares de conexões, o `ip_local_port_range` padrão (28 mil portas por destino) acaba antes da CPU. Ajuste por sua conta e risco: é configuração do sistema. +#### 8.4 – Atalho: um comando para disparar + +O [`deploy/stress/opt/thinker-stress-02/stress.sh`](../../deploy/stress/opt/thinker-stress-02/stress.sh) junta os passos acima. Copie-o com o `sem-freios.override.yml` para `/opt/thinker-stress-02/`: + +```Bash +cd /opt/thinker-stress-02; +./stress.sh iniciar; # com freios: 3 replicas, 10 degraus, limites do compose +./stress.sh iniciar --sem-freios; # secao 8.3: pede confirmacao (digite SIM) +./stress.sh status; +./stress.sh parar; # para o loop, volta a 1 replica, tira o override + # Thinker de volta a 1 replica, com os limites do compose +``` + +O override "sem freios" fica guardado fora de `/opt/thinker`: só vira `docker-compose.override.yml` (que o compose lê sozinho) no `--sem-freios`, e o `parar` o remove. Cada rodada começa um `reports/loop.log` novo e guarda o anterior com a data. + ## 9 – Lendo os resultados O `wwz-load` sem `--duration` só imprime o relatório quando para. Com o loop, os números vêm de três lugares. From 28a8fcd00ccdfc302baf7b9a606e5ccdbd540a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Brand=C3=A3o?= Date: Sat, 7 Nov 2026 18:51:30 -0300 Subject: [PATCH 2/2] chore(release): version 0.31.9 --- docs/releases/0.31.9.md | 9 +++++++++ package-lock.json | 2 +- packages/thinker/package.json | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 docs/releases/0.31.9.md diff --git a/docs/releases/0.31.9.md b/docs/releases/0.31.9.md new file mode 100644 index 00000000..97a96756 --- /dev/null +++ b/docs/releases/0.31.9.md @@ -0,0 +1,9 @@ +# Thinker 0.31.9 + +```bash +docker pull tmsoftbrasil/thinker:0.31.9 +``` + +## Mudanças + +- feat(deploy): ambiente de stress Thinker + White Wizard em deploy/stress (composes, configs do wwz e os scripts stress.sh, provisionar.sh, loop.sh e escala.sh) diff --git a/package-lock.json b/package-lock.json index 08bc3662..4c93d630 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14768,7 +14768,7 @@ } }, "packages/thinker": { - "version": "0.31.8" + "version": "0.31.9" } } } diff --git a/packages/thinker/package.json b/packages/thinker/package.json index bf9fbd25..3a355bf1 100644 --- a/packages/thinker/package.json +++ b/packages/thinker/package.json @@ -1,6 +1,6 @@ { "name": "thinker", - "version": "0.31.8", + "version": "0.31.9", "private": true, "description": "Canonical release version for the Thinker Docker image. This package holds no code — the real backend and frontend live in ../backend and ../frontend. See README.md for details." }