diff --git a/src/config/sys.config b/src/config/sys.config index 0b2fba1..c8763c3 100644 --- a/src/config/sys.config +++ b/src/config/sys.config @@ -16,7 +16,10 @@ ]} ]}, {mnesia, [ - {dir, "data/Mnesia.eventhub@${NODE_NAME}"} + {dir, "data/Mnesia.eventhub@${NODE_NAME}"}, + %% Soften dump_log under write load; override via MNESIA_DUMP_LOG_WRITE_THRESHOLD + %% before start (infra_mnesia:configure_dump_log/0). OTP default ~1000. + {dump_log_write_threshold, 50000} ]}, { cowboy_swagger, [ { static_files, "./_build/default/lib/cowboy_swagger/priv/swagger" } diff --git a/src/eventhub_app.erl b/src/eventhub_app.erl index 0c0a51a..a4e9cfc 100644 --- a/src/eventhub_app.erl +++ b/src/eventhub_app.erl @@ -46,8 +46,9 @@ start_application() -> io:format("~nCluster: discovered nodes ~p, joining cluster~n", [Nodes]), application:set_env(eventhub, extra_db_nodes, Nodes) end, - application:ensure_all_started(mnesia), ok = infra_mnesia:configure_dump_log(), + application:ensure_all_started(mnesia), + ok = infra_mnesia:verify_dump_log(), ok = infra_mnesia:init_tables(), ok = infra_mnesia:wait_for_tables(), ok = migration_engine:ensure_applied(), diff --git a/src/infra/infra_mnesia.erl b/src/infra/infra_mnesia.erl index 55530cc..8f05075 100644 --- a/src/infra/infra_mnesia.erl +++ b/src/infra/infra_mnesia.erl @@ -8,7 +8,7 @@ -export([start_link/0, init_tables/0, wait_for_tables/0, wait_for_table/1]). -export([add_cluster_nodes/1]). --export([configure_dump_log/0]). +-export([configure_dump_log/0, verify_dump_log/0]). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). @@ -48,16 +48,31 @@ add_cluster_nodes(Nodes) -> %% @doc Soften dump_log overload under write-heavy load (IFT stress). %% OTP warns {dump_log, write_threshold|time_threshold} when a new dump %% is requested while the previous dump still runs. +%% Must be set BEFORE mnesia starts — change_config/2 does not accept +%% dump_log_write_threshold (returns {error, dump_log_write_threshold}). %% Override: MNESIA_DUMP_LOG_WRITE_THRESHOLD (default 50000; OTP default ~1000). -spec configure_dump_log() -> ok. configure_dump_log() -> Threshold = env_int("MNESIA_DUMP_LOG_WRITE_THRESHOLD", 50000), - case mnesia:change_config(dump_log_write_threshold, Threshold) of - {ok, Old} -> - io:format("Mnesia dump_log_write_threshold: ~p -> ~p~n", [Old, Threshold]), + case application:load(mnesia) of + ok -> ok; + {error, {already_loaded, mnesia}} -> ok; + {error, LoadErr} -> + io:format("Mnesia load before dump_log config failed: ~p~n", [LoadErr]) + end, + ok = application:set_env(mnesia, dump_log_write_threshold, Threshold), + io:format("Mnesia dump_log_write_threshold set to ~p (before start)~n", [Threshold]), + ok. + +%% @doc Log active threshold after mnesia:start (sanity check for IFT logs). +-spec verify_dump_log() -> ok. +verify_dump_log() -> + case catch mnesia:system_info(dump_log_write_threshold) of + N when is_integer(N) -> + io:format("Mnesia dump_log_write_threshold active: ~p~n", [N]), ok; Other -> - io:format("Mnesia dump_log_write_threshold change failed: ~p~n", [Other]), + io:format("Mnesia dump_log_write_threshold verify: ~p~n", [Other]), ok end. diff --git a/test/tsung/README.md b/test/tsung/README.md index c062110..9a34dcd 100644 --- a/test/tsung/README.md +++ b/test/tsung/README.md @@ -7,7 +7,9 @@ | `eventhub_ift_smoke.xml` | Короткий smoke на IFT после prepare | | `eventhub_ift.xml` | Mix: `auth_browse` 60% + `create_flow` 40%, фазы warmup/ramp/peak | | `eventhub_ift_stress.xml` | **Hot-path stress**: browse 70% / write 30%, ladder arrival, поиск верхней границы | -| `eventhub_ift_stress_hold.xml` | Controlled hold: maxusers=300, thinktime=1s, ladder 0.2→0.1→0.05→0.035 | +| `eventhub_ift_stress_hold.xml` | Controlled hold: maxusers=100, `use_controller_vm`, ladder 0.2→0.1→0.05→0.035 | +| `eventhub_ift_stress_hold_hot.xml` | Hot ladder только 0.1→0.05→0.035, maxusers=100, `use_controller_vm` (`stress-hold-hot`) | +| `eventhub_ift_stress_hold_beams.xml` | **Опасно:** `use_controller_vm=false` — slave beams раздувают concurrent (не для IFT без `CONFIRM_DANGEROUS=1`) | | `eventhub_ift_stress_bump.xml` | **Запрещён по умолчанию** (uncapped arrival; клинит WSL IFT) | | `eventhub_tsung.xml` | Legacy localhost (не для IFT) | | `eventhub_http.xml` | Legacy короткий localhost | @@ -21,17 +23,28 @@ IFT = Docker Swarm **внутри WSL2 на Windows**. Aggressive open-loop бе | Правило | Значение | |---------|----------| | Рекомендуемый stress | `eventhub_ift_stress_hold.xml` (`stress-hold`) | -| `maxusers` | ≤ **300** | +| `maxusers` | ≤ **300** (hold default **100** после ×3/2022 — CPU/RAM saturation при 200) | | `thinktime` | ≥ **1** s на hot-path hold | | Плановый interarrival | ≥ **0.1** s (~10 VU/s); ниже — только с наблюдением | | Uncapped / bump / maxusers≫300 | **запрещено** на IFT без явного согласования | -| Рабочий потолок (2026-07-15) | ~**10 VU/s** hot-path на текущем железе | +| Рабочий потолок (2026-07-15, ×3, с WAF) | ~**10 VU/s** hot-path; **пересчитываем** после Mnesia+loadtest-Traefik | +| Traefik loadtest | prepare подменяет conf **без WAF** (`LOADTEST_TRAEFIK=1`); rate limit 2000/1s остаётся | +| Лестница реплик | потолок сначала **×1**, затем **×2**, **×3** — отдельно (влияние репликации Mnesia) | -**Recover после клина (на Windows-хосте IFT):** +**После каждого stress (обязательно — см. EventHubSpec/WORKFLOW.md §7):** -1. Сбор логов: `\\192.168.1.116\eventhub-cursor\scripts\restart-wsl-and-collect-logs.ps1` (или `collect-ift-loadtest-logs.ps1`) -2. `wsl --shutdown` при необходимости -3. Restore: `restore-ift.ps1` (Windows OpenSSH) → `ift-loadtest-restore.sh` (retry/`--force` при `update out of sequence`) +1. Summary: `python3 test/tsung/summarize-tsung-stress.py ~/.tsung/log//tsung.log` +2. Сбор логов IFT → exchange `ift-to-dev/loadtest-/`: + - SSH ок: `\\192.168.1.116\eventhub-cursor\scripts\collect-ift-loadtest-logs.ps1` (чистит remote dump stamp после download) + - SSH hang: `restart-wsl-and-collect-logs.ps1` или `wsl --shutdown` на HOME-PC → снова collect +3. Разбор: Tsung vs прошлый прогон; архив (429 / Mnesia / archive / OOM); docker-stats; + **метрики бэка:** `GET /metrics` (Prometheus text) + `GET /v1/admin/nodes/metrics?from=&to=` (admin JWT, история в Mnesia); + compose Grafana/Prom — опционально (`KEEP_OBSERVABILITY=1`), при scale 0 mid-run там пусто +4. Очистка leftover на IFT: `cleanup-ift-loadtest-dumps.ps1` / `EventHubDevOps/scripts/cleanup-ift-loadtest-dumps.sh` +5. Отчёт в issue (#30) / строка «Результат» ниже +6. Restore: `restore-ift.ps1` → `ift-loadtest-restore.sh` + +Прогон без п.1–4 не считается завершённым. ### Auth path (smoke / full) @@ -60,17 +73,17 @@ IFT = Docker Swarm **внутри WSL2 на Windows**. Aggressive open-loop бе **Controlled hold ×2** (`20260715-1336/`, `EVENTHUB_REPLICAS=2`): фазы 0.2/0.1 GREEN (mean ≈37 ms); 0.05 OVER мягче (timeout 106, ~3%); overall UNDER_THRESHOLD (err ~0.94%, mean ~64 ms). -**Controlled hold ×3** (`20260715-1411/`, `EVENTHUB_REPLICAS=3`): connect по фазам 0.2→0.035 GREEN (0.05 mean≈40 ms); overall BOUNDARY_HIT из‑за **3772× HTTP 429** — узкое место сместилось на Traefik `api-ratelimit` (per source IP, было 5000/1m≈83 rps). После поднятия IFT-лимитов до 2000/1s (EventHubBack#32) перепроверить hold ×3. +**Controlled hold ×3** (`20260715-1411/`, `EVENTHUB_REPLICAS=3`, до фиксов): connect по фазам 0.2→0.035 GREEN (0.05 mean≈40 ms); overall BOUNDARY_HIT из‑за **3772× HTTP 429** — Traefik `api-ratelimit` (было 5000/1m≈83 rps). + +**Controlled hold ×3 maxusers=100** (`20260715-2102/`, `sha-7fa24e831c16`): **UNDER_THRESHOLD** — 0 errors, mean ~115 ms, late ~331 ms; только фазы 0.2/0.1 (~4 мин); фазы 0.05/0.035 не стартовали после hard-stop `use_controller_vm`. `nodes/metrics`: CPU peak ~60%, mem_available min ~335 MiB (без клина WSL). + +**Controlled hold ×3 maxusers=200** (`20260715-2022/`): BOUNDARY_HIT (err 1.74%, timeouts 175); CPU ~100%, mem ~11 MiB; клин SSH. + +**Controlled hold ×3 после фиксов** (`20260715-1845/`, maxusers=300): **0× HTTP 4xx/5xx**; mean ≈47 ms; **106× error_timeout**; overall BOUNDARY_HIT (error_rate **1.018%**); зависание ~194 VU + клин WSL. Под hold также: `Mnesia is overloaded: {dump_log, write_threshold|time_threshold}`. Краткосрочно: `MNESIA_DUMP_LOG_WRITE_THRESHOLD` (IFT default 50000, EventHubBack#34). Долгосрочно: меньше sync-транзакций / hot-path вне Mnesia. -Restore fallback: оркестратор → WSL SSH, при fail Windows `restore-ift.ps1`. - -Если после stress SSH «висит на banner» — **reboot IFT**, затем: - -```bash -bash /opt/eventhub-ift/devops/scripts/ift-loadtest-restore.sh -``` +Restore fallback: оркестратор → WSL SSH, при fail Windows `restore-ift.ps1`. Restore **после** collect+анализа (см. gate выше); если SSH мёртв — `wsl --shutdown` → collect → restore. ## Подготовка учётки admin @@ -113,20 +126,51 @@ tsung -f test/tsung/eventhub_ift_smoke.xml start tsung -f test/tsung/eventhub_ift.xml start tsung -f test/tsung/eventhub_ift_stress.xml start # оркестратор (prepare + seed при необходимости + restore): -bash ~/.cursor/eventhub/run-ift-loadtest.sh smoke|full|stress|stress-bump|stress-hold -# replicas: EVENTHUB_REPLICAS=2|3 bash ~/.cursor/eventhub/run-ift-loadtest.sh stress-hold +bash ~/.cursor/eventhub/run-ift-loadtest.sh smoke|full|stress|stress-bump|stress-hold|stress-hold-hot|stress-hold-beams +# replicas: EVENTHUB_REPLICAS=1|2|3 bash ~/.cursor/eventhub/run-ift-loadtest.sh stress-hold +# фазы 0.05/0.035: stress-hold-hot (без slave beams); stress-hold-beams — только с CONFIRM_DANGEROUS=1 +# Traefik без WAF в prepare: LOADTEST_TRAEFIK=1 (default); штатный WAF: LOADTEST_TRAEFIK=0 # restore fallback: если WSL SSH не отвечает — Windows OpenSSH # powershell -File $env:USERPROFILE\.cursor\eventhub\restore-ift.ps1 ``` +### Пересчёт потолка (лестница реплик) + +Цель — отделить raw capacity одной ноды от стоимости репликации Mnesia. + +1. Задеплоить образ с рабочим `dump_log_write_threshold` (до `mnesia:start`). +2. Для каждой ступени **×1 → ×2 → ×3** (отдельно, с полным gate после каждого прогона): + - `EVENTHUB_REPLICAS=N bash ~/.cursor/eventhub/run-ift-loadtest.sh stress-hold` (зелёный baseline) + - при UNDER на 0.2/0.1 — `stress-hold-hot` (0.1→0.05→0.035) +3. В отчёте #30: replicas, verdict по фазам, CPU/mem, Mnesia dump_log, образ `sha-*`. +4. Не смешивать ступени в одном прогоне; не использовать beams без `CONFIRM_DANGEROUS=1`. + Отчёт: `~/.tsung/log//`. Для stress — также `~/.tsung/ift-docker-stats-*.log`. -## После прогона +## После прогона (обязательный gate) -```bash -bash /opt/eventhub-ift/devops/scripts/ift-loadtest-restore.sh +```powershell +# 1) Tsung 2A (с дев WSL) +python3 test/tsung/summarize-tsung-stress.py ~/.tsung/log//tsung.log + +# 2) Логи стенда на шару (+ auto-cleanup remote dump этого stamp) +powershell -NoProfile -File \\192.168.1.116\eventhub-cursor\scripts\collect-ift-loadtest-logs.ps1 + +# 3) Разбор архива + метрики бэка: +# curl -sk https://api.ift.eventhub.local/metrics +# GET /v1/admin/nodes/metrics?from=&to= (admin JWT) — история узлов +# 4) Leftover dumps на IFT (все stamps) +powershell -NoProfile -File $env:USERPROFILE\.cursor\eventhub\cleanup-ift-loadtest-dumps.ps1 + +# 5) Restore +powershell -NoProfile -File $env:USERPROFILE\.cursor\eventhub\restore-ift.ps1 ``` +Prepare с внешним Prometheus/Grafana (не путать с `/metrics` приложения): + +```bash +KEEP_OBSERVABILITY=1 EVENTHUB_REPLICAS=3 bash /opt/eventhub-ift/devops/scripts/ift-loadtest-prepare.sh +``` ## Настройка интенсивности В `eventhub_ift.xml` / `eventhub_ift_stress*.xml` блок ``: `interarrival` и `duration` фаз. diff --git a/test/tsung/eventhub_ift_stress_hold.xml b/test/tsung/eventhub_ift_stress_hold.xml index 4dd4bac..a241253 100644 --- a/test/tsung/eventhub_ift_stress_hold.xml +++ b/test/tsung/eventhub_ift_stress_hold.xml @@ -3,13 +3,13 @@ - + diff --git a/test/tsung/eventhub_ift_stress_hold_beams.xml b/test/tsung/eventhub_ift_stress_hold_beams.xml new file mode 100644 index 0000000..4c207bb --- /dev/null +++ b/test/tsung/eventhub_ift_stress_hold_beams.xml @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + + + + + + + + + + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + HTTP/[0-9.]+ 20 + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + diff --git a/test/tsung/eventhub_ift_stress_hold_hot.xml b/test/tsung/eventhub_ift_stress_hold_hot.xml new file mode 100644 index 0000000..38552ec --- /dev/null +++ b/test/tsung/eventhub_ift_stress_hold_hot.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + + + + + + + + + + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + HTTP/[0-9.]+ 20 + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + + HTTP/[0-9.]+ 20 + + + + + + + HTTP/[0-9.]+ 20 + + + + + + +