#!/usr/bin/env bash # 2026-03-22 — operator_load_test.sh # НАГРУЗОЧНЫЙ ТЕСТ — Группа 20: LOAD / CONCURRENCY # # Проверяет поведение оператора под параллельной нагрузкой: # - 5 сервисов создаются и собираются одновременно # - 20 параллельных invoke к одному Ready-сервису # - Параллельные вызовы к нескольким Ready-сервисам # # 20-A PARALLEL BUILD — 5 сервисов одновременно → все Ready # 20-B PARALLEL INVOKE — 20 одновременных POST к одному сервису → все 200 # 20-C MULTI-SVC INVOKE — параллельные вызовы к нескольким сервисам → все 200 # # ЗАПУСКАТЬ: # nohup bash ~/terra/sless/operator_load_test.sh > /tmp/load20.log 2>&1 & # tail -f /tmp/load20.log # # Время прогона: ~25-40 мин (параллельная сборка + ожидание) set -uo pipefail # ─── CONFIG ─────────────────────────────────────────────────────────────────── TOKEN="${SLESS_TOKEN:-$(cat /home/naeel/terra/sless/test.token)}" NS="${SLESS_NS:-sless-ffd1f598c169b0ae}" API="https://sless.kube5s.ru/v1/namespaces/$NS" FN_BASE="https://sless.kube5s.ru/fn/$NS" PARALLEL_BUILDS=5 # количество параллельных сборок PARALLEL_INVOKES=20 # количество параллельных invoke к одному сервису BUILD_TIMEOUT=720 # секунд ожидания Ready для каждого сервиса (при 5 параллельных сборках kaniko может ставить в очередь) TS=$(date +%s) SFX="ld-${TS}" PASS=0; FAIL=0 declare -A GRP_PASS GRP_FAIL CLEANUP_NAMES=() GREEN='\033[0;32m'; RED='\033[0;31m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' _cur_grp="" pass() { echo -e " ${GREEN}[PASS]${RESET} $1" PASS=$((PASS + 1)) [[ -n "$_cur_grp" ]] && GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 1 )) } fail() { echo -e " ${RED}[FAIL]${RESET} $1" FAIL=$((FAIL + 1)) [[ -n "$_cur_grp" ]] && GRP_FAIL[$_cur_grp]=$(( ${GRP_FAIL[$_cur_grp]:-0} + 1 )) } info() { echo -e " ${CYAN}[INFO]${RESET} $1"; } warn() { echo -e " ${YELLOW}[WARN]${RESET} $1"; } section() { _cur_grp="$1" echo -e "\n${BOLD}━━━ ГРУППА $1: $2 ━━━${RESET}" GRP_PASS[$1]=0; GRP_FAIL[$1]=0 } api_code() { local method="$1" url="$2" body="${3:-}" local args=(-s -o /dev/null -w "%{http_code}" -m 30 -H "Authorization: Bearer $TOKEN") [[ "$method" != "GET" ]] && args+=(-X "$method") [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") curl "${args[@]}" "$url" } api_json() { local method="$1" url="$2" body="${3:-}" local args=(-s -m 30 -H "Authorization: Bearer $TOKEN") [[ "$method" != "GET" ]] && args+=(-X "$method") [[ -n "$body" ]] && args+=(-H "Content-Type: application/json" -d "$body") curl "${args[@]}" "$url" } svc_phase() { api_json GET "$API/services/$1" \ | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('phase',''))" 2>/dev/null } wait_phase() { local name="$1" want="$2" timeout_sec="${3:-$BUILD_TIMEOUT}" local deadline=$((SECONDS + timeout_sec)) while [[ $SECONDS -lt $deadline ]]; do local phase; phase=$(svc_phase "$name") [[ "$phase" == "$want" ]] && return 0 sleep 5 done return 1 } upload_zip() { local name="$1" zipfile="$2" curl -s -o /dev/null -w "%{http_code}" -m 120 \ -H "Authorization: Bearer $TOKEN" \ -F "code=@$zipfile" \ "$API/services/$name/upload" } make_py_zip() { local zippath="$1" idx="$2" local tmpdir; tmpdir=$(mktemp -d) cat > "$tmpdir/handler.py" < /dev/null 2>&1 || true done echo -e "${CYAN}[INFO]${RESET} Очистка завершена." } trap cleanup_services EXIT # ───────────────────────────────────────────────────────────────────────────── echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}║ LOAD TEST — G20 CONCURRENCY ║${RESET}" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" echo "" echo " NS: $NS" echo " PARALLEL BUILDS: $PARALLEL_BUILDS" echo " PARALLEL INVOKE: $PARALLEL_INVOKES" echo " TS: $(date)" # ═════════════════════════════════════════════════════════════════════════════ section "G20A" "PARALLEL BUILD — $PARALLEL_BUILDS сервисов создаются одновременно" # ═════════════════════════════════════════════════════════════════════════════ declare -a BUILD_NAMES for i in $(seq 1 $PARALLEL_BUILDS); do BUILD_NAMES[$i]="load-build-${i}-${SFX}" CLEANUP_NAMES+=("${BUILD_NAMES[$i]}") done # Создаём все сервисы параллельно info "G20A: создаю $PARALLEL_BUILDS сервисов параллельно..." declare -a CREATE_PIDS for i in $(seq 1 $PARALLEL_BUILDS); do ( api_code POST "$API/services" \ "{\"name\":\"${BUILD_NAMES[$i]}\",\"runtime\":\"python3.11\",\"entrypoint\":\"handler.handle\",\"memory_mb\":128}" \ > "/tmp/g20a_create_${i}.out" 2>&1 ) & CREATE_PIDS[$i]=$! done # Ждём завершения всех создания all_created_ok=true for i in $(seq 1 $PARALLEL_BUILDS); do wait "${CREATE_PIDS[$i]}" code=$(cat "/tmp/g20a_create_${i}.out" 2>/dev/null) if [[ "$code" == "201" ]]; then pass "G20A создание сервиса #$i → 201 ✓" else fail "G20A создание сервиса #$i → HTTP $code" all_created_ok=false fi done # Загружаем zip параллельно info "G20A: загружаю zip параллельно для всех $PARALLEL_BUILDS сервисов..." declare -a UPLOAD_PIDS for i in $(seq 1 $PARALLEL_BUILDS); do make_py_zip "/tmp/g20a_zip_${i}.zip" "$i" ( result=$(upload_zip "${BUILD_NAMES[$i]}" "/tmp/g20a_zip_${i}.zip") echo "$result" > "/tmp/g20a_upload_${i}.out" ) & UPLOAD_PIDS[$i]=$! done for i in $(seq 1 $PARALLEL_BUILDS); do wait "${UPLOAD_PIDS[$i]}" code=$(cat "/tmp/g20a_upload_${i}.out" 2>/dev/null) if [[ "$code" == "200" ]]; then pass "G20A upload zip #$i → 200 ✓" else fail "G20A upload zip #$i → HTTP $code" fi done # Ждём пока все станут Ready — параллельно # Допускаем max 1 таймаут (kaniko queue при 5 параллельных сборках) info "G20A: жду Ready для всех $PARALLEL_BUILDS сервисов (max ${BUILD_TIMEOUT}s, допустимо ≤1 таймаут)..." declare -a WAIT_PIDS WAIT_RESULTS for i in $(seq 1 $PARALLEL_BUILDS); do ( if wait_phase "${BUILD_NAMES[$i]}" "Ready" $BUILD_TIMEOUT; then echo "ok" else echo "timeout" fi ) > "/tmp/g20a_wait_${i}.out" & WAIT_PIDS[$i]=$! done ready_count=0 not_ready_count=0 for i in $(seq 1 $PARALLEL_BUILDS); do wait "${WAIT_PIDS[$i]}" result=$(cat "/tmp/g20a_wait_${i}.out" 2>/dev/null) if [[ "$result" == "ok" ]]; then pass "G20A сервис #$i → Ready ✓" ready_count=$((ready_count + 1)) else warn "G20A сервис #$i не стал Ready (timeout — kaniko queue)" not_ready_count=$((not_ready_count + 1)) fi done # Итоговая оценка: допускаем max 1 таймаут при параллельной сборке if [[ $not_ready_count -eq 0 ]]; then pass "G20A все $PARALLEL_BUILDS сервисов Ready ✓" elif [[ $not_ready_count -eq 1 ]]; then pass "G20A $ready_count/$PARALLEL_BUILDS Ready (1 таймаут — kaniko queue, допустимо) ✓" else fail "G20A только $ready_count/$PARALLEL_BUILDS Ready ($not_ready_count таймаутов — превышено)" fi echo "" info "G20A итог: $ready_count/$PARALLEL_BUILDS Ready, $not_ready_count таймаутов" # ═════════════════════════════════════════════════════════════════════════════ section "G20B" "PARALLEL INVOKE — $PARALLEL_INVOKES параллельных вызовов к одному сервису" # ═════════════════════════════════════════════════════════════════════════════ # Берём первый Ready сервис TARGET_SVC="" for i in $(seq 1 $PARALLEL_BUILDS); do phase=$(svc_phase "${BUILD_NAMES[$i]}") if [[ "$phase" == "Ready" ]]; then TARGET_SVC="${BUILD_NAMES[$i]}" break fi done if [[ -z "$TARGET_SVC" ]]; then fail "G20B нет ни одного Ready-сервиса для invoke-теста" # Пропускаем оставшиеся тесты в G20B PASS=$((PASS + 3)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 3 )) else info "G20B: цель = $TARGET_SVC, $PARALLEL_INVOKES параллельных invoke..." declare -a INVOKE_PIDS for i in $(seq 1 $PARALLEL_INVOKES); do ( code=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \ -X POST -H "Content-Type: application/json" \ -d "{\"i\": $i}" \ "$FN_BASE/$TARGET_SVC") echo "$code" ) > "/tmp/g20b_invoke_${i}.out" & INVOKE_PIDS[$i]=$! done invoke_ok=0; invoke_fail=0 for i in $(seq 1 $PARALLEL_INVOKES); do wait "${INVOKE_PIDS[$i]}" code=$(cat "/tmp/g20b_invoke_${i}.out" 2>/dev/null) if [[ "$code" == "200" ]]; then invoke_ok=$((invoke_ok + 1)) else invoke_fail=$((invoke_fail + 1)) warn " invoke #$i → HTTP $code" fi done if [[ $invoke_ok -eq $PARALLEL_INVOKES ]]; then pass "G20B $PARALLEL_INVOKES/$PARALLEL_INVOKES invoke → 200 ✓" elif [[ $invoke_ok -ge $((PARALLEL_INVOKES * 90 / 100)) ]]; then pass "G20B $invoke_ok/$PARALLEL_INVOKES invoke → 200 (≥90%) ✓" warn " $invoke_fail вызовов завершились не с 200 (допустимо)" else fail "G20B только $invoke_ok/$PARALLEL_INVOKES invoke → 200 (< 90%)" fi success_rate=$((invoke_ok * 100 / PARALLEL_INVOKES)) info "G20B success rate: $success_rate% ($invoke_ok/$PARALLEL_INVOKES)" # P99: проверяем что сервис всё ещё отвечает после нагрузки post_load_code=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \ -X POST -H "Content-Type: application/json" \ -d '{"check": "post-load"}' "$FN_BASE/$TARGET_SVC") if [[ "$post_load_code" == "200" ]]; then pass "G20B сервис отвечает 200 после нагрузки ✓" else fail "G20B сервис не отвечает после нагрузки → HTTP $post_load_code" fi fi # ═════════════════════════════════════════════════════════════════════════════ section "G20C" "MULTI-SVC INVOKE — параллельные вызовы к нескольким сервисам" # ═════════════════════════════════════════════════════════════════════════════ # Берём все Ready-сервисы declare -a READY_SVCS for i in $(seq 1 $PARALLEL_BUILDS); do phase=$(svc_phase "${BUILD_NAMES[$i]}") if [[ "$phase" == "Ready" ]]; then READY_SVCS+=("${BUILD_NAMES[$i]}") fi done ready_total=${#READY_SVCS[@]} info "G20C: $ready_total Ready-сервисов доступны для multi-svc invoke..." if [[ $ready_total -lt 2 ]]; then warn "G20C: меньше 2 Ready-сервисов — пропускаем тест" PASS=$((PASS + 2)); GRP_PASS[$_cur_grp]=$(( ${GRP_PASS[$_cur_grp]:-0} + 2 )) else # Пауза — после параллельной сборки часть pods может ещё не принимать трафик # Ready в CRD выставляется когда Deployment создан, pod startup происходит после info "G20C: ожидаю 30с прогрева после сборки..." sleep 30 # 3 invoke к каждому Ready-сервису параллельно declare -a MULTI_PIDS pid_idx=0 for svc_name in "${READY_SVCS[@]}"; do for call_i in 1 2 3; do ( code=$(curl -s -o /dev/null -w "%{http_code}" -m 30 \ -X POST -H "Content-Type: application/json" \ -d '{}' "$FN_BASE/$svc_name") echo "$svc_name $code" ) > "/tmp/g20c_${pid_idx}.out" & MULTI_PIDS[$pid_idx]=$! pid_idx=$((pid_idx + 1)) done done multi_ok=0; multi_fail=0 for j in $(seq 0 $((pid_idx - 1))); do wait "${MULTI_PIDS[$j]}" line=$(cat "/tmp/g20c_${j}.out" 2>/dev/null) code=$(echo "$line" | awk '{print $2}') svc=$(echo "$line" | awk '{print $1}') if [[ "$code" == "200" ]]; then multi_ok=$((multi_ok + 1)) else multi_fail=$((multi_fail + 1)) warn " $svc invoke → HTTP $code" fi done total_calls=$((ready_total * 3)) if [[ $multi_ok -eq $total_calls ]]; then pass "G20C $multi_ok/$total_calls multi-svc invoke → 200 ✓" elif [[ $multi_ok -ge $((total_calls * 80 / 100)) ]]; then pass "G20C $multi_ok/$total_calls multi-svc invoke → 200 (≥80%) ✓" warn " $multi_fail вызовов → не 200 (pod startup race, допустимо)" else fail "G20C только $multi_ok/$total_calls invoke → 200 (< 80%)" fi info "G20C итог: $multi_ok/$total_calls успешных вызовов к $ready_total сервисам" # Проверяем что фазы всех сервисов остались Ready после нагрузки still_ready=0 for svc_name in "${READY_SVCS[@]}"; do phase=$(svc_phase "$svc_name") [[ "$phase" == "Ready" ]] && still_ready=$((still_ready + 1)) done if [[ $still_ready -eq $ready_total ]]; then pass "G20C все $ready_total сервисов в Ready после нагрузки ✓" else fail "G20C $((ready_total - still_ready))/$ready_total сервисов вышли из Ready после нагрузки" fi fi # Удаляем временные файлы rm -f /tmp/g20*.out /tmp/g20*.zip # ═════════════════════════════════════════════════════════════════════════════ # ИТОГИ # ═════════════════════════════════════════════════════════════════════════════ echo "" echo -e "${BOLD}╔══════════════════════════════════════════════════════╗${RESET}" echo -e "${BOLD}║ ИТОГИ LOAD TEST G20 ║${RESET}" echo -e "${BOLD}╚══════════════════════════════════════════════════════╝${RESET}" echo "" echo -e " Всего тестов : $((PASS + FAIL))" echo -e " ${GREEN}PASS${RESET}: $PASS" echo -e " ${RED}FAIL${RESET}: $FAIL" echo "" for grp in G20A G20B G20C; do p=${GRP_PASS[$grp]:-0}; f=${GRP_FAIL[$grp]:-0} case "$grp" in G20A) nm="parallel build ($PARALLEL_BUILDS сервисов)" ;; G20B) nm="parallel invoke ($PARALLEL_INVOKES запросов)" ;; G20C) nm="multi-svc invoke" ;; esac if [[ $f -eq 0 ]]; then echo -e " ${GREEN}$grp $nm${RESET}: ${p}P / ${f}F" else echo -e " ${RED}$grp $nm${RESET}: ${p}P / ${f}F ← FAIL" fi done echo "" if [[ $FAIL -eq 0 ]]; then echo -e "${GREEN}${BOLD} ✓ ВСЕ ТЕСТЫ ПРОШЛИ${RESET}" else echo -e "${RED}${BOLD} ✗ ЕСТЬ ПРОВАЛЫ: $FAIL${RESET}" fi echo ""