#!/usr/bin/env bash # Простой нагрузочный смоук-тест API Deal (этап 12, пакет C). # # Что делает: логинится (по умолчанию admin/admin), затем в цикле дёргает два # читающих эндпоинта — GET /api/containers и GET /api/cards — до истечения # DURATION_SECONDS, замеряя HTTP-код и время ответа. В конце печатает сводку: # число запросов, ошибки (не 200), RPS, средняя/p95-латентность (сек). # # Зависимости: только bash и curl. Ничего не пишет в БД; читающие ручки. # # Запуск (из корня репозитория, dev-API на :5080): # bash scripts/loadtest/api-loadtest.sh # BASE_URL=http://localhost:5080 LOGIN=admin PASSWORD=admin \ # DURATION_SECONDS=20 PAUSE_SECONDS=0.1 bash scripts/loadtest/api-loadtest.sh # # Переменные окружения: # BASE_URL базовый URL API (по умолчанию http://localhost:5080) # LOGIN/PASSWORD учётные данные тенанта (по умолчанию admin/admin — dev-дефолт bootstrap) # DURATION_SECONDS длительность прогона, с (по умолчанию 15) # PAUSE_SECONDS пауза между итерациями, с (по умолчанию 0.2) # CURL_TIMEOUT таймаут запроса, с (по умолчанию 5) # # Внимание: в PROD включён rate-limit (api-политика — 600/мин на тенанта). Для # нагрузочного прогона используйте dev-стенд или поднимите лимит. set -euo pipefail BASE_URL="${BASE_URL:-http://localhost:5080}" LOGIN="${LOGIN:-admin}" PASSWORD="${PASSWORD:-admin}" DURATION_SECONDS="${DURATION_SECONDS:-15}" PAUSE_SECONDS="${PAUSE_SECONDS:-0.2}" CURL_TIMEOUT="${CURL_TIMEOUT:-5}" command -v curl >/dev/null 2>&1 || { echo "Ошибка: нужен curl" >&2; exit 127; } tmp_dir="$(mktemp -d)" cookie_jar="${tmp_dir}/cookies.txt" lat_file="${tmp_dir}/latencies.txt" trap 'rm -rf "${tmp_dir}"' EXIT login_code="$(curl -sS -o "${tmp_dir}/login.json" -w '%{http_code}' \ -c "${cookie_jar}" -H 'Content-Type: application/json' \ -d "{\"login\":\"${LOGIN}\",\"password\":\"${PASSWORD}\"}" \ --max-time "${CURL_TIMEOUT}" "${BASE_URL}/api/auth/login" || true)" [ -n "${login_code}" ] || login_code="000" if [ "${login_code}" != "200" ]; then echo "LOGIN FAILED (HTTP ${login_code}): $(cat "${tmp_dir}/login.json" 2>/dev/null)" >&2 echo "Проверьте BASE_URL=${BASE_URL} и что API поднят." >&2 exit 1 fi echo "login OK (${LOGIN}) → прогон ${DURATION_SECONDS} с, пауза ${PAUSE_SECONDS} с" started_at="$(date +%s)" end_at=$((started_at + DURATION_SECONDS)) total=0 errors=0 while [ "$(date +%s)" -lt "${end_at}" ]; do for path in /api/containers /api/cards; do result="$(curl -sS -o /dev/null -w '%{http_code} %{time_total}' \ -b "${cookie_jar}" --max-time "${CURL_TIMEOUT}" "${BASE_URL}${path}" || true)" [ -n "${result}" ] || result="000 0" code="${result%% *}" elapsed="${result##* }" total=$((total + 1)) if [ "${code}" != "200" ]; then errors=$((errors + 1)) echo " ! ${path} → HTTP ${code}" >&2 fi echo "${elapsed}" >> "${lat_file}" done sleep "${PAUSE_SECONDS}" done elapsed_total=$(( $(date +%s) - started_at )) [ "${elapsed_total}" -lt 1 ] && elapsed_total=1 if [ "${total}" -eq 0 ]; then echo "Запросов не сделано (DURATION_SECONDS слишком мал?)" >&2 exit 1 fi avg="$(awk '{sum += $1} END {printf "%.4f", sum / NR}' "${lat_file}")" p95_index=$(( (total * 95 + 99) / 100 )) p95="$(sort -n "${lat_file}" | sed -n "${p95_index}p")" max="$(sort -n "${lat_file}" | tail -n 1)" rps="$(awk -v n="${total}" -v d="${elapsed_total}" 'BEGIN {printf "%.2f", n / d}')" echo "---------------------------------------------" echo "запросов: ${total}" echo "ошибок: ${errors}" echo "длительность: ${elapsed_total} с" echo "RPS: ${rps}" echo "латентность: avg=${avg}s p95=${p95}s max=${max}s" echo "---------------------------------------------" [ "${errors}" -eq 0 ] || exit 2