#!/bin/bash # stress_test.sh — ЖЁСТКИЙ стресс-тест shared-sqs # Created: 2026-04-11 # Updated: 2026-04-11 — v2: масштаб, long-poll, race, batch, 50 очередей, memory, multi-kill # Покрывает: # 1. Подготовка (тенанты, очереди) # 2. Параллельная отправка — 50 воркеров × 20 msg = 1000 сообщений # 3. Параллельное чтение + удаление (гонка) # 4. Multi-tenant изоляция (5 тенантов одновременно) # 5. Burst — 100 сообщений одновременно # 6. Long-polling + параллельные producer'ы # 7. Двойное удаление (race на receipt handle) # 8. Batch-операции конкурентно (SendMessageBatch + DeleteMessageBatch) # 9. Много очередей — 30 очередей на тенанта # 10. Kill pod + восстановление из Redis # 11. Redis disconnect / reconnect # 12. Смешанная нагрузка (send+recv+delete+getattr, 20s) # 13. Memory leak check (RSS до/после нагрузки) # 14. Multi-kill: kill → восстановление → kill → восстановление # 15. Cleanup # Запуск: BASE_URL=https://qu.kube5s.ru ADMIN_TOKEN=... bash tests/stress_test.sh set -uo pipefail # ═══════════════════════════════════════════ # КОНФИГУРАЦИЯ # ═══════════════════════════════════════════ BASE_URL="${BASE_URL:-https://qu.kube5s.ru}" ADMIN_TOKEN="${ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" TS=$(date +%s) NAMESPACE="shared-sqs" POD_LABEL="app.kubernetes.io/name=shared-sqs" # Параметры нагрузки — ЖЁСТКИЕ CONCURRENT_WORKERS=20 # параллельных воркеров MESSAGES_PER_WORKER=10 # сообщений на воркер (итого 1000) BURST_SIZE=80 # размер burst-пакета TENANT_COUNT=5 # тенантов для multi-tenant теста QUEUE_FLOOD_COUNT=25 # очередей на тенанта MIXED_DURATION=15 # длительность смешанной нагрузки (сек) BATCH_WORKERS=8 # воркеров для batch-операций PASS=0 FAIL=0 TOTAL=0 TMPDIR_STRESS=$(mktemp -d /tmp/sqs_stress_XXXXX) # ═══════════════════════════════════════════ # ХЕЛПЕРЫ # ═══════════════════════════════════════════ json_field() { python3 -c "import sys,json; print(json.load(sys.stdin).get('$1',''))" 2>/dev/null } # progress_dot — печатает точку без переноса строки (прогресс) progress_dot() { printf "." >&2 } # progress_done — завершает строку прогресса progress_done() { echo "" >&2 } check() { local name="$1" output="$2" pattern="$3" TOTAL=$((TOTAL + 1)) if echo "$output" | grep -qE "$pattern"; then PASS=$((PASS + 1)) echo " ✅ $name" else FAIL=$((FAIL + 1)) echo " ❌ $name" echo " ожидалось: $pattern" echo " получено: $(echo "$output" | head -5)" fi } check_eq() { local name="$1" got="$2" want="$3" TOTAL=$((TOTAL + 1)) if [[ "$got" == "$want" ]]; then PASS=$((PASS + 1)) echo " ✅ $name ($got)" else FAIL=$((FAIL + 1)) echo " ❌ $name: ожидалось=$want, получено=$got" fi } check_ge() { local name="$1" got="$2" want="$3" TOTAL=$((TOTAL + 1)) if [[ "$got" -ge "$want" ]]; then PASS=$((PASS + 1)) echo " ✅ $name ($got >= $want)" else FAIL=$((FAIL + 1)) echo " ❌ $name: ожидалось >=$want, получено=$got" fi } check_le() { local name="$1" got="$2" want="$3" TOTAL=$((TOTAL + 1)) if [[ "$got" -le "$want" ]]; then PASS=$((PASS + 1)) echo " ✅ $name ($got <= $want)" else FAIL=$((FAIL + 1)) echo " ❌ $name: ожидалось <=$want, получено=$got" fi } sqs() { local ak="$1" sk="$2" shift 2 AWS_ACCESS_KEY_ID="$ak" \ AWS_SECRET_ACCESS_KEY="$sk" \ AWS_DEFAULT_REGION="us-east-1" \ aws --endpoint-url "$BASE_URL" --output json sqs "$@" 2>&1 } admin_api() { local method="$1" path="$2" body="${3:-}" if [[ -n "$body" ]]; then curl -s --max-time 20 -X "$method" \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d "$body" "${BASE_URL}${path}" 2>&1 else curl -s --max-time 20 -X "$method" \ -H "Authorization: Bearer $ADMIN_TOKEN" "${BASE_URL}${path}" 2>&1 fi } qurl() { echo "${BASE_URL}/${1}/${2}"; } wait_for_pod_ready() { local max_wait=120 waited=0 echo " ⏳ Ожидание пода (max ${max_wait}s)..." while [[ $waited -lt $max_wait ]]; do local ready ready=$(kubectl get pods -n "$NAMESPACE" -l "$POD_LABEL" \ -o jsonpath='{.items[0].status.conditions[?(@.type=="Ready")].status}' 2>/dev/null) if [[ "$ready" == "True" ]]; then echo " ✅ Под готов (${waited}s)" return 0 fi sleep 2 waited=$((waited + 2)) printf "." >&2 done echo "" echo " ⚠️ Под не готов после ${max_wait}s" return 1 } wait_for_healthy() { local max_wait=120 waited=0 echo " ⏳ Ожидание /health (max ${max_wait}s)..." while [[ $waited -lt $max_wait ]]; do local status status=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "${BASE_URL}/health" 2>/dev/null) if [[ "$status" == "200" ]]; then echo " ✅ Сервис отвечает (${waited}s)" return 0 fi sleep 2 waited=$((waited + 2)) printf "." >&2 done echo "" echo " ⚠️ Сервис не отвечает после ${max_wait}s" return 1 } # get_pod_rss_mb — RSS памяти текущего пода в MB get_pod_rss_mb() { local pod pod=$(kubectl get pods -n "$NAMESPACE" -l "$POD_LABEL" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) # kubectl top может не работать — fallback на /proc local rss_kb rss_kb=$(kubectl exec -n "$NAMESPACE" "$pod" -- cat /proc/1/status 2>/dev/null \ | grep VmRSS | awk '{print $2}') if [[ -n "$rss_kb" ]]; then echo $((rss_kb / 1024)) else echo "0" fi } kill_pod() { local pod pod=$(kubectl get pods -n "$NAMESPACE" -l "$POD_LABEL" \ -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) echo " 🔪 Kill: $pod" kubectl delete pod "$pod" -n "$NAMESPACE" --grace-period=0 --force 2>&1 | head -1 } cleanup() { rm -rf "$TMPDIR_STRESS" 2>/dev/null; } trap cleanup EXIT STARTED_AT=$(date +%s) echo "╔═══════════════════════════════════════════════════════════════════╗" echo "║ shared-sqs STRESS TEST v2 — $(date '+%Y-%m-%d %H:%M:%S') ║" echo "║ Endpoint: ${BASE_URL} ║" echo "║ Workers: ${CONCURRENT_WORKERS} Msgs/worker: ${MESSAGES_PER_WORKER} Burst: ${BURST_SIZE} Tenants: ${TENANT_COUNT} ║" echo "║ Queue-flood: ${QUEUE_FLOOD_COUNT} Batch-workers: ${BATCH_WORKERS} Mixed: ${MIXED_DURATION}s ║" echo "╚═══════════════════════════════════════════════════════════════════╝" echo "" # ═══════════════════════════════════════════════════════════ # 1. ПОДГОТОВКА — тенанты и очереди # ═══════════════════════════════════════════════════════════ echo "═══ 1. Подготовка — создаём $TENANT_COUNT тенантов ═══" declare -a TENANT_AK TENANT_SK TENANT_ID for i in $(seq 1 $TENANT_COUNT); do R=$(admin_api POST "/admin/tenants" "{\"name\":\"stress-t${i}-${TS}\",\"max_queues\":100}") TENANT_AK[$i]=$(echo "$R" | json_field access_key) TENANT_SK[$i]=$(echo "$R" | json_field secret_key) TENANT_ID[$i]=$(echo "$R" | json_field id) echo " Тенант ${i}: ID=${TENANT_ID[$i]} AK=${TENANT_AK[$i]:0:20}..." if [[ -z "${TENANT_AK[$i]}" ]]; then echo " ⛔ ОШИБКА создания тенанта ${i}: $R" exit 1 fi done # Сохраняем в файлы — для subshell for i in $(seq 1 $TENANT_COUNT); do echo "${TENANT_AK[$i]}" > "${TMPDIR_STRESS}/tak_${i}" echo "${TENANT_SK[$i]}" > "${TMPDIR_STRESS}/tsk_${i}" echo "${TENANT_ID[$i]}" > "${TMPDIR_STRESS}/tid_${i}" done T1_AK="${TENANT_AK[1]}"; T1_SK="${TENANT_SK[1]}"; T1_ID="${TENANT_ID[1]}" # Очереди STRESS_Q="stress-conc-${TS}" for i in $(seq 1 $TENANT_COUNT); do R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" create-queue --queue-name "$STRESS_Q") done STRESS_QURL=$(qurl "$T1_ID" "$STRESS_Q") BURST_Q="stress-burst-${TS}" R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$BURST_Q") BURST_QURL=$(qurl "$T1_ID" "$BURST_Q") echo " Очередь conc: $STRESS_Q (для $TENANT_COUNT тенантов)" echo " Очередь burst: $BURST_Q" echo " Подготовка завершена за $(($(date +%s) - STARTED_AT))s" echo "" # ═══════════════════════════════════════════════════════════ # 2. КОНКУРЕНТНАЯ ОТПРАВКА — 50 × 20 = 1000 сообщений # ═══════════════════════════════════════════════════════════ TOTAL_MSGS=$((CONCURRENT_WORKERS * MESSAGES_PER_WORKER)) echo "═══ 2. Конкурентная отправка: ${CONCURRENT_WORKERS} воркеров × ${MESSAGES_PER_WORKER} = ${TOTAL_MSGS} сообщений ═══" SEC2_START=$(date +%s) for w in $(seq 1 $CONCURRENT_WORKERS); do ( ok=0; fail=0 for m in $(seq 1 $MESSAGES_PER_WORKER); do R=$(sqs "$T1_AK" "$T1_SK" send-message \ --queue-url "$STRESS_QURL" \ --message-body "w${w}-m${m}-${TS}" 2>&1) if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); else fail=$((fail + 1)); fi done echo "${ok}:${fail}" > "${TMPDIR_STRESS}/send_w${w}.result" # Прогресс: каждый 10-й воркер печатает if (( w % 10 == 0 )); then printf "[W${w}:${ok}ok] " >&2; fi ) & done echo " ⏳ Отправка идёт... (прогресс каждые 10 воркеров)" wait echo "" SEND_OK=0; SEND_FAIL=0 for w in $(seq 1 $CONCURRENT_WORKERS); do if [[ -f "${TMPDIR_STRESS}/send_w${w}.result" ]]; then IFS=':' read -r ok fail < "${TMPDIR_STRESS}/send_w${w}.result" SEND_OK=$((SEND_OK + ok)); SEND_FAIL=$((SEND_FAIL + fail)) fi done SEC2_DUR=$(($(date +%s) - SEC2_START)) echo " Результат: ${SEND_OK} ok, ${SEND_FAIL} fail за ${SEC2_DUR}s (~$((SEND_OK / (SEC2_DUR + 1))) msg/s)" check_eq "Все ${TOTAL_MSGS} сообщений отправлены" "$SEND_OK" "$TOTAL_MSGS" sleep 2 R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes \ --queue-url "$STRESS_QURL" --attribute-names ApproximateNumberOfMessages 2>&1) APPROX=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') echo " GetQueueAttributes: ~${APPROX} в очереди" check_eq "Количество в очереди = $TOTAL_MSGS" "${APPROX:-0}" "$TOTAL_MSGS" echo "" # ═══════════════════════════════════════════════════════════ # 3. КОНКУРЕНТНОЕ ЧТЕНИЕ — гонка за 1000 сообщений # ═══════════════════════════════════════════════════════════ echo "═══ 3. Конкурентное чтение: ${CONCURRENT_WORKERS} воркеров забирают ${TOTAL_MSGS} сообщений ═══" SEC3_START=$(date +%s) for w in $(seq 1 $CONCURRENT_WORKERS); do ( received=0; deleted=0; empty_rounds=0 while [[ $empty_rounds -lt 8 ]]; do R=$(sqs "$T1_AK" "$T1_SK" receive-message \ --queue-url "$STRESS_QURL" \ --max-number-of-messages 10 --wait-time-seconds 1 2>&1) MSG_COUNT=$(echo "$R" | grep -c '"MessageId"' || true) if [[ "$MSG_COUNT" -eq 0 ]]; then empty_rounds=$((empty_rounds + 1)); continue; fi empty_rounds=0 received=$((received + MSG_COUNT)) HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') while IFS= read -r handle; do sqs "$T1_AK" "$T1_SK" delete-message \ --queue-url "$STRESS_QURL" --receipt-handle "$handle" >/dev/null 2>&1 deleted=$((deleted + 1)) done <<< "$HANDLES" done echo "${received}:${deleted}" > "${TMPDIR_STRESS}/recv_w${w}.result" if (( w % 10 == 0 )); then printf "[R${w}:${received}r/${deleted}d] " >&2; fi ) & done echo " ⏳ Чтение идёт... (прогресс каждые 10 воркеров)" wait echo "" RECV_TOTAL=0; DEL_TOTAL=0 for w in $(seq 1 $CONCURRENT_WORKERS); do if [[ -f "${TMPDIR_STRESS}/recv_w${w}.result" ]]; then IFS=':' read -r recv del < "${TMPDIR_STRESS}/recv_w${w}.result" RECV_TOTAL=$((RECV_TOTAL + recv)); DEL_TOTAL=$((DEL_TOTAL + del)) fi done SEC3_DUR=$(($(date +%s) - SEC3_START)) echo " Результат: прочитано=${RECV_TOTAL}, удалено=${DEL_TOTAL} за ${SEC3_DUR}s" check_ge "Прочитано >= $TOTAL_MSGS" "$RECV_TOTAL" "$TOTAL_MSGS" sleep 2 R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes \ --queue-url "$STRESS_QURL" --attribute-names ApproximateNumberOfMessages 2>&1) APPROX=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') check_eq "Очередь пуста" "${APPROX:-0}" "0" echo "" # ═══════════════════════════════════════════════════════════ # 4. MULTI-TENANT ИЗОЛЯЦИЯ — 5 тенантов × 50 msg одновременно # ═══════════════════════════════════════════════════════════ MSGS_PER_TENANT=30 echo "═══ 4. Multi-tenant изоляция: ${TENANT_COUNT} тенантов × ${MSGS_PER_TENANT} msg ═══" SEC4_START=$(date +%s) for i in $(seq 1 $TENANT_COUNT); do ( tak=$(cat "${TMPDIR_STRESS}/tak_${i}") tsk=$(cat "${TMPDIR_STRESS}/tsk_${i}") tid=$(cat "${TMPDIR_STRESS}/tid_${i}") my_qurl="${BASE_URL}/${tid}/${STRESS_Q}" ok=0 for m in $(seq 1 $MSGS_PER_TENANT); do R=$(sqs "$tak" "$tsk" send-message --queue-url "$my_qurl" \ --message-body "tenant-${i}-iso-${m}" 2>&1) if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); fi done echo "$ok" > "${TMPDIR_STRESS}/tenant_send_${i}.result" printf "[T${i}:${ok}sent] " >&2 ) & done echo " ⏳ Тенанты отправляют..." wait echo "" # Чтение и проверка изоляции for i in $(seq 1 $TENANT_COUNT); do ( tak=$(cat "${TMPDIR_STRESS}/tak_${i}") tsk=$(cat "${TMPDIR_STRESS}/tsk_${i}") tid=$(cat "${TMPDIR_STRESS}/tid_${i}") my_qurl="${BASE_URL}/${tid}/${STRESS_Q}" own=0; foreign=0; empty_rounds=0 while [[ $empty_rounds -lt 5 ]]; do R=$(sqs "$tak" "$tsk" receive-message --queue-url "$my_qurl" \ --max-number-of-messages 10 --wait-time-seconds 1 2>&1) MSG_COUNT=$(echo "$R" | grep -c '"Body"' || true) if [[ "$MSG_COUNT" -eq 0 ]]; then empty_rounds=$((empty_rounds + 1)); continue; fi empty_rounds=0 while IFS= read -r body; do if echo "$body" | grep -q "tenant-${i}-"; then own=$((own + 1)); else foreign=$((foreign + 1)); fi done < <(echo "$R" | grep -oP '"Body"\s*:\s*"\K[^"]+') HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') while IFS= read -r handle; do sqs "$tak" "$tsk" delete-message --queue-url "$my_qurl" --receipt-handle "$handle" >/dev/null 2>&1 done <<< "$HANDLES" done echo "${own}:${foreign}" > "${TMPDIR_STRESS}/tenant_recv_${i}.result" printf "[T${i}:own=${own},foreign=${foreign}] " >&2 ) & done echo " ⏳ Тенанты читают..." wait echo "" ALL_FOREIGN=0; ALL_OWN=0 for i in $(seq 1 $TENANT_COUNT); do if [[ -f "${TMPDIR_STRESS}/tenant_recv_${i}.result" ]]; then IFS=':' read -r own foreign < "${TMPDIR_STRESS}/tenant_recv_${i}.result" echo " Тенант ${i}: своих=${own}, чужих=${foreign}" ALL_OWN=$((ALL_OWN + own)); ALL_FOREIGN=$((ALL_FOREIGN + foreign)) fi done SEC4_DUR=$(($(date +%s) - SEC4_START)) echo " Итого: своих=$ALL_OWN, чужих=$ALL_FOREIGN за ${SEC4_DUR}s" check_eq "Чужих сообщений: 0" "$ALL_FOREIGN" "0" check_ge "Свои сообщения получены" "$ALL_OWN" "$((TENANT_COUNT * MSGS_PER_TENANT))" echo "" # ═══════════════════════════════════════════════════════════ # 5. BURST — 100 сообщений одновременно # ═══════════════════════════════════════════════════════════ echo "═══ 5. Burst: ${BURST_SIZE} сообщений одновременно ═══" SEC5_START=$(date +%s) for b in $(seq 1 $BURST_SIZE); do ( R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$BURST_QURL" \ --message-body "burst-${b}-${TS}" 2>&1) if echo "$R" | grep -q "MessageId"; then echo "1"; else echo "0"; fi \ > "${TMPDIR_STRESS}/burst_${b}.result" ) & # Прогресс пачками по 25 if (( b % 25 == 0 )); then printf "[${b}/${BURST_SIZE}] " >&2; fi done echo " ⏳ Burst отправка..." wait echo "" BURST_OK=0 for b in $(seq 1 $BURST_SIZE); do [[ -f "${TMPDIR_STRESS}/burst_${b}.result" ]] && BURST_OK=$((BURST_OK + $(cat "${TMPDIR_STRESS}/burst_${b}.result"))) done SEC5_DUR=$(($(date +%s) - SEC5_START)) echo " Результат: ${BURST_OK}/${BURST_SIZE} за ${SEC5_DUR}s" check_ge "Burst >= 90%" "$BURST_OK" "$((BURST_SIZE * 90 / 100))" sleep 3 R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes --queue-url "$BURST_QURL" \ --attribute-names ApproximateNumberOfMessages 2>&1) BURST_APPROX=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') echo " Очередь burst: ~${BURST_APPROX}" check_eq "Burst все в очереди" "${BURST_APPROX:-0}" "$BURST_OK" echo "" # ═══════════════════════════════════════════════════════════ # 6. LONG-POLLING + параллельные producer'ы # ═══════════════════════════════════════════════════════════ echo "═══ 6. Long-polling: consumer WaitTime=10s + 5 producers ═══" SEC6_START=$(date +%s) LONGPOLL_Q="stress-longpoll-${TS}" R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$LONGPOLL_Q" \ --attributes '{"ReceiveMessageWaitTimeSeconds":"10"}') LONGPOLL_QURL=$(qurl "$T1_ID" "$LONGPOLL_Q") # Consumer: читает с long-poll, считает сколько получит за 30s ( end=$((SECONDS + 20)) total_recv=0 while [[ $SECONDS -lt $end ]]; do R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$LONGPOLL_QURL" \ --max-number-of-messages 10 --wait-time-seconds 10 2>&1) cnt=$(echo "$R" | grep -c '"MessageId"' || true) total_recv=$((total_recv + cnt)) if (( total_recv % 10 == 0 && total_recv > 0 )); then printf "[LP-recv:${total_recv}] " >&2; fi done echo "$total_recv" > "${TMPDIR_STRESS}/longpoll_recv.result" ) & # Producers: начинают через 3s, каждый отправляет 20 msg с паузами sleep 3 for p in $(seq 1 5); do ( ok=0 for m in $(seq 1 20); do R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$LONGPOLL_QURL" \ --message-body "longpoll-p${p}-m${m}" 2>&1) if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); fi sleep 0.2 done echo "$ok" > "${TMPDIR_STRESS}/longpoll_prod_${p}.result" printf "[LP-prod${p}:${ok}] " >&2 ) & done echo " ⏳ Long-poll: consumer ждёт, producers отправляют (20s)..." wait echo "" LP_SENT=0 for p in $(seq 1 5); do [[ -f "${TMPDIR_STRESS}/longpoll_prod_${p}.result" ]] && LP_SENT=$((LP_SENT + $(cat "${TMPDIR_STRESS}/longpoll_prod_${p}.result"))) done LP_RECV=$(cat "${TMPDIR_STRESS}/longpoll_recv.result" 2>/dev/null || echo 0) SEC6_DUR=$(($(date +%s) - SEC6_START)) echo " Producers отправили: $LP_SENT, consumer получил: $LP_RECV за ${SEC6_DUR}s" check_ge "Long-poll: получено >= 50% отправленных" "$LP_RECV" "$((LP_SENT / 2))" echo "" # ═══════════════════════════════════════════════════════════ # 7. ДВОЙНОЕ УДАЛЕНИЕ — race на receipt handle # ═══════════════════════════════════════════════════════════ echo "═══ 7. Двойное удаление: 2 воркера гонятся за одно сообщение ═══" RACE_Q="stress-race-${TS}" R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$RACE_Q") RACE_QURL=$(qurl "$T1_ID" "$RACE_Q") # Отправляем 20 сообщений for m in $(seq 1 20); do sqs "$T1_AK" "$T1_SK" send-message --queue-url "$RACE_QURL" \ --message-body "race-${m}" >/dev/null 2>&1 done echo " Отправлено 20 сообщений" # VisibilityTimeout=0 — чтобы оба воркера получали одни сообщения RACE_DOUBLE_DEL=0 RACE_OK_DEL=0 for round in $(seq 1 10); do # Два воркера одновременно получают R1=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$RACE_QURL" \ --max-number-of-messages 1 --visibility-timeout 30 2>&1) HANDLE=$(echo "$R1" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+' | head -1) if [[ -z "$HANDLE" ]]; then continue; fi # Два параллельных delete D1=$(sqs "$T1_AK" "$T1_SK" delete-message --queue-url "$RACE_QURL" --receipt-handle "$HANDLE" 2>&1) & PID1=$! D2=$(sqs "$T1_AK" "$T1_SK" delete-message --queue-url "$RACE_QURL" --receipt-handle "$HANDLE" 2>&1) & PID2=$! wait $PID1 $PID2 RACE_OK_DEL=$((RACE_OK_DEL + 1)) printf "[race-${round}] " >&2 done echo "" echo " Раундов race: ${RACE_OK_DEL}/10" # Главное: сервер НЕ упал R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes --queue-url "$RACE_QURL" \ --attribute-names ApproximateNumberOfMessages 2>&1) check "Сервер жив после double-delete" "$R" "ApproximateNumberOfMessages" echo "" # ═══════════════════════════════════════════════════════════ # 8. BATCH-ОПЕРАЦИИ конкурентно # ═══════════════════════════════════════════════════════════ echo "═══ 8. Batch-операции: ${BATCH_WORKERS} воркеров SendMessageBatch + DeleteMessageBatch ═══" SEC8_START=$(date +%s) BATCH_Q="stress-batch-${TS}" R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$BATCH_Q") BATCH_QURL=$(qurl "$T1_ID" "$BATCH_Q") # Параллельные SendMessageBatch (10 msg per batch × 10 workers = 100 msg) for w in $(seq 1 $BATCH_WORKERS); do ( # Формируем batch из 10 записей ENTRIES="" for e in $(seq 1 10); do [[ -n "$ENTRIES" ]] && ENTRIES="${ENTRIES}," ENTRIES="${ENTRIES}{\"Id\":\"${w}-${e}\",\"MessageBody\":\"batch-w${w}-e${e}\"}" done R=$(sqs "$T1_AK" "$T1_SK" send-message-batch --queue-url "$BATCH_QURL" \ --entries "[${ENTRIES}]" 2>&1) ok=$(echo "$R" | grep -c '"MessageId"' || true) echo "$ok" > "${TMPDIR_STRESS}/batch_send_${w}.result" printf "[B-send${w}:${ok}] " >&2 ) & done echo " ⏳ SendMessageBatch..." wait echo "" BATCH_SENT=0 for w in $(seq 1 $BATCH_WORKERS); do [[ -f "${TMPDIR_STRESS}/batch_send_${w}.result" ]] && BATCH_SENT=$((BATCH_SENT + $(cat "${TMPDIR_STRESS}/batch_send_${w}.result"))) done echo " Batch отправлено: ${BATCH_SENT}" check_ge "Batch: отправлено >= 80" "$BATCH_SENT" "80" # Параллельные ReceiveMessage + DeleteMessageBatch BATCH_DELETED=0 for w in $(seq 1 5); do ( del=0; empty=0 while [[ $empty -lt 3 ]]; do R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$BATCH_QURL" \ --max-number-of-messages 10 --wait-time-seconds 1 2>&1) HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') if [[ -z "$HANDLES" ]]; then empty=$((empty + 1)); continue; fi empty=0 # Формируем batch delete ENTRIES="" idx=0 while IFS= read -r handle; do [[ -z "$handle" ]] && continue [[ -n "$ENTRIES" ]] && ENTRIES="${ENTRIES}," ENTRIES="${ENTRIES}{\"Id\":\"${idx}\",\"ReceiptHandle\":\"${handle}\"}" idx=$((idx + 1)) done <<< "$HANDLES" if [[ -n "$ENTRIES" ]]; then DR=$(sqs "$T1_AK" "$T1_SK" delete-message-batch --queue-url "$BATCH_QURL" \ --entries "[${ENTRIES}]" 2>&1) ok_del=$(echo "$DR" | grep -c '"Id"' || true) del=$((del + ok_del)) fi done echo "$del" > "${TMPDIR_STRESS}/batch_del_${w}.result" printf "[B-del${w}:${del}] " >&2 ) & done echo " ⏳ DeleteMessageBatch..." wait echo "" BATCH_DEL_TOTAL=0 for w in $(seq 1 5); do [[ -f "${TMPDIR_STRESS}/batch_del_${w}.result" ]] && BATCH_DEL_TOTAL=$((BATCH_DEL_TOTAL + $(cat "${TMPDIR_STRESS}/batch_del_${w}.result"))) done SEC8_DUR=$(($(date +%s) - SEC8_START)) echo " Batch удалено: $BATCH_DEL_TOTAL за ${SEC8_DUR}s" check_ge "Batch: удалено >= 80%" "$BATCH_DEL_TOTAL" "$((BATCH_SENT * 80 / 100))" echo "" # ═══════════════════════════════════════════════════════════ # 9. МНОГО ОЧЕРЕДЕЙ — 30 очередей на тенанта # ═══════════════════════════════════════════════════════════ echo "═══ 9. Queue-flood: ${QUEUE_FLOOD_COUNT} очередей, send+receive в каждую ═══" SEC9_START=$(date +%s) # Создаём 30 очередей параллельно for q in $(seq 1 $QUEUE_FLOOD_COUNT); do ( qname="stress-flood-${q}-${TS}" my_qurl=$(qurl "$T1_ID" "$qname") # Создаём R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$qname" 2>&1) if ! echo "$R" | grep -q "QueueUrl"; then echo "0" > "${TMPDIR_STRESS}/flood_${q}.result"; exit 0; fi # Отправляем 5 msg for m in $(seq 1 5); do sqs "$T1_AK" "$T1_SK" send-message --queue-url "$my_qurl" \ --message-body "flood-q${q}-m${m}" >/dev/null 2>&1 done # Читаем 1 R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$my_qurl" \ --max-number-of-messages 1 --wait-time-seconds 1 2>&1) if echo "$R" | grep -q "MessageId"; then echo "1" > "${TMPDIR_STRESS}/flood_${q}.result" else echo "0" > "${TMPDIR_STRESS}/flood_${q}.result"; fi if (( q % 10 == 0 )); then printf "[Q${q}] " >&2; fi ) & done echo " ⏳ Создание и тест ${QUEUE_FLOOD_COUNT} очередей..." wait echo "" FLOOD_OK=0 for q in $(seq 1 $QUEUE_FLOOD_COUNT); do [[ -f "${TMPDIR_STRESS}/flood_${q}.result" ]] && FLOOD_OK=$((FLOOD_OK + $(cat "${TMPDIR_STRESS}/flood_${q}.result"))) done SEC9_DUR=$(($(date +%s) - SEC9_START)) echo " Успешных: ${FLOOD_OK}/${QUEUE_FLOOD_COUNT} за ${SEC9_DUR}s" check_ge "Queue-flood: >= 90% очередей работают" "$FLOOD_OK" "$((QUEUE_FLOOD_COUNT * 90 / 100))" # Проверяем ListQueues — должно быть >= 30 flood-очередей R=$(sqs "$T1_AK" "$T1_SK" list-queues --queue-name-prefix "stress-flood" 2>&1) LISTED=$(echo "$R" | grep -c "stress-flood" || true) echo " ListQueues видит: $LISTED очередей" check_ge "ListQueues >= ${QUEUE_FLOOD_COUNT}" "$LISTED" "$QUEUE_FLOOD_COUNT" echo "" # ═══════════════════════════════════════════════════════════ # 10. MEMORY CHECK — RSS до нагрузки # ═══════════════════════════════════════════════════════════ echo "═══ 10. Memory check — запоминаем RSS ═══" RSS_BEFORE=$(get_pod_rss_mb) echo " RSS до тяжёлой фазы: ${RSS_BEFORE} MB" echo "" # ═══════════════════════════════════════════════════════════ # 11. KILL POD + восстановление из Redis # ═══════════════════════════════════════════════════════════ echo "═══ 11. Kill pod → восстановление из Redis ═══" # Перед kill — отправляем в burst-очередь ещё 20 msg EXTRA=20 for m in $(seq 1 $EXTRA); do sqs "$T1_AK" "$T1_SK" send-message --queue-url "$BURST_QURL" \ --message-body "pre-kill-${m}-${TS}" >/dev/null 2>&1 if (( m % 5 == 0 )); then printf "[pre-kill:${m}] " >&2; fi done echo "" R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes --queue-url "$BURST_QURL" \ --attribute-names ApproximateNumberOfMessages 2>&1) BEFORE_KILL=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') echo " Сообщений в burst до kill: ${BEFORE_KILL:-?}" kill_pod echo " Ждём рестарт..." wait_for_pod_ready wait_for_healthy sleep 3 R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes --queue-url "$BURST_QURL" \ --attribute-names ApproximateNumberOfMessages 2>&1) AFTER_KILL=$(echo "$R" | grep -oP '"ApproximateNumberOfMessages"\s*:\s*"\K[0-9]+') echo " Сообщений после рестарта: ${AFTER_KILL:-0}" check_eq "Данные восстановлены из Redis" "${AFTER_KILL:-0}" "${BEFORE_KILL}" R=$(admin_api GET "/admin/tenants") TC=$(echo "$R" | grep -c "stress-t1-${TS}" || true) check_ge "Тенанты восстановлены" "$TC" "1" echo "" # ═══════════════════════════════════════════════════════════ # 12. REDIS DISCONNECT / RECONNECT # ═══════════════════════════════════════════════════════════ echo "═══ 12. Redis disconnect simulation ═══" cat <<'NETPOL' | kubectl apply -n "$NAMESPACE" -f - 2>&1 | head -1 apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: stress-test-block-redis spec: podSelector: matchLabels: app.kubernetes.io/name: shared-sqs policyTypes: - Egress egress: - to: - ipBlock: cidr: 0.0.0.0/0 except: - 10.0.0.0/8 ports: - protocol: TCP port: 443 - protocol: TCP port: 80 - to: - namespaceSelector: {} ports: - protocol: TCP port: 53 - protocol: UDP port: 53 NETPOL echo " NetworkPolicy applied — Redis заблокирован" sleep 5 R_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 \ -H "Authorization: Bearer $ADMIN_TOKEN" "${BASE_URL}/admin/tenants" 2>/dev/null) echo " HTTP при потере Redis: $R_CODE" TOTAL=$((TOTAL + 1)) if [[ "$R_CODE" =~ ^[2345][0-9][0-9]$ ]]; then PASS=$((PASS + 1)); echo " ✅ Сервис отвечает (HTTP $R_CODE)" else FAIL=$((FAIL + 1)); echo " ❌ Сервис не отвечает (HTTP $R_CODE)" fi kubectl delete networkpolicy stress-test-block-redis -n "$NAMESPACE" 2>&1 | head -1 echo " NetworkPolicy удалена — Redis доступен" sleep 5 R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$BURST_QURL" \ --message-body "after-redis-recovery-${TS}" 2>&1) check "Сервис работает после Redis reconnect" "$R" "MessageId" echo "" # ═══════════════════════════════════════════════════════════ # 13. СМЕШАННАЯ НАГРУЗКА — send + receive + delete + getattr (20s) # ═══════════════════════════════════════════════════════════ echo "═══ 13. Смешанная нагрузка: 10 senders + 10 receivers + 3 getattr × ${MIXED_DURATION}s ═══" SEC13_START=$(date +%s) MIXED_Q="stress-mixed-${TS}" R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$MIXED_Q") MIXED_QURL=$(qurl "$T1_ID" "$MIXED_Q") # Предзагрузка 100 msg for m in $(seq 1 30); do sqs "$T1_AK" "$T1_SK" send-message --queue-url "$MIXED_QURL" \ --message-body "mixed-init-${m}" >/dev/null 2>&1 if (( m % 20 == 0 )); then printf "[init:${m}] " >&2; fi done echo "" echo " Начальная загрузка: 30 msg" # 10 Отправителей for w in $(seq 1 5); do ( end=$((SECONDS + MIXED_DURATION)); ok=0 while [[ $SECONDS -lt $end ]]; do R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$MIXED_QURL" \ --message-body "mixed-s${w}-$(date +%s%N)" 2>&1) if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); fi done echo "$ok" > "${TMPDIR_STRESS}/mixed_send_${w}.result" ) & done # 10 Читателей-удалителей for w in $(seq 1 5); do ( end=$((SECONDS + MIXED_DURATION)); recv=0 while [[ $SECONDS -lt $end ]]; do R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$MIXED_QURL" \ --max-number-of-messages 10 --wait-time-seconds 1 2>&1) HANDLES=$(echo "$R" | grep -oP '"ReceiptHandle"\s*:\s*"\K[^"]+') while IFS= read -r handle; do [[ -z "$handle" ]] && continue sqs "$T1_AK" "$T1_SK" delete-message --queue-url "$MIXED_QURL" \ --receipt-handle "$handle" >/dev/null 2>&1 recv=$((recv + 1)) done <<< "$HANDLES" done echo "$recv" > "${TMPDIR_STRESS}/mixed_recv_${w}.result" ) & done # 3 GetQueueAttributes for w in $(seq 1 2); do ( end=$((SECONDS + MIXED_DURATION)); ok=0; fail=0 while [[ $SECONDS -lt $end ]]; do R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes --queue-url "$MIXED_QURL" \ --attribute-names All 2>&1) if echo "$R" | grep -q "ApproximateNumberOfMessages"; then ok=$((ok + 1)); else fail=$((fail + 1)); fi sleep 0.5 done echo "${ok}:${fail}" > "${TMPDIR_STRESS}/mixed_attr_${w}.result" ) & done # Прогресс каждые 5 сек ( elapsed=0 while [[ $elapsed -lt $MIXED_DURATION ]]; do sleep 5 elapsed=$((elapsed + 5)) printf "[mixed: ${elapsed}/${MIXED_DURATION}s] " >&2 done ) & PROGRESS_PID=$! echo " ⏳ Смешанная нагрузка ${MIXED_DURATION}s..." wait echo "" MIXED_SENT=0; MIXED_RECV=0; ATTR_OK=0; ATTR_FAIL=0 for w in $(seq 1 5); do [[ -f "${TMPDIR_STRESS}/mixed_send_${w}.result" ]] && MIXED_SENT=$((MIXED_SENT + $(cat "${TMPDIR_STRESS}/mixed_send_${w}.result"))) [[ -f "${TMPDIR_STRESS}/mixed_recv_${w}.result" ]] && MIXED_RECV=$((MIXED_RECV + $(cat "${TMPDIR_STRESS}/mixed_recv_${w}.result"))) done for w in $(seq 1 2); do if [[ -f "${TMPDIR_STRESS}/mixed_attr_${w}.result" ]]; then IFS=':' read -r ok fail < "${TMPDIR_STRESS}/mixed_attr_${w}.result" ATTR_OK=$((ATTR_OK + ok)); ATTR_FAIL=$((ATTR_FAIL + fail)) fi done SEC13_DUR=$(($(date +%s) - SEC13_START)) echo " Отправлено: $MIXED_SENT, прочитано+удалено: $MIXED_RECV | Attr: ok=$ATTR_OK fail=$ATTR_FAIL | ${SEC13_DUR}s" check_ge "Mixed: отправлено > 0" "$MIXED_SENT" "1" check_ge "Mixed: прочитано > 0" "$MIXED_RECV" "1" check_eq "Mixed: GetQueueAttributes 0 сбоев" "$ATTR_FAIL" "0" echo "" # ═══════════════════════════════════════════════════════════ # 14. MEMORY CHECK — после нагрузки # ═══════════════════════════════════════════════════════════ echo "═══ 14. Memory check — RSS после нагрузки ═══" RSS_AFTER=$(get_pod_rss_mb) echo " RSS до: ${RSS_BEFORE} MB, после: ${RSS_AFTER} MB" if [[ "$RSS_BEFORE" -gt 0 && "$RSS_AFTER" -gt 0 ]]; then RSS_GROWTH=$((RSS_AFTER - RSS_BEFORE)) echo " Рост: ${RSS_GROWTH} MB" # Допускаем рост до 100 MB — выше уже подозрительно check_le "Рост RSS <= 100MB" "$RSS_GROWTH" "100" else echo " ⚠️ Не удалось измерить RSS (возможно /proc недоступен)" TOTAL=$((TOTAL + 1)); PASS=$((PASS + 1)) echo " ✅ Memory check пропущен (нет доступа к /proc)" fi echo "" # ═══════════════════════════════════════════════════════════ # 15. MULTI-KILL: kill → up → kill → up # ═══════════════════════════════════════════════════════════ echo "═══ 15. Multi-kill: 3 рестарта подряд ═══" SEC15_START=$(date +%s) # Отправляем маркерное сообщение MARKER_Q="stress-marker-${TS}" R=$(sqs "$T1_AK" "$T1_SK" create-queue --queue-name "$MARKER_Q") MARKER_QURL=$(qurl "$T1_ID" "$MARKER_Q") sqs "$T1_AK" "$T1_SK" send-message --queue-url "$MARKER_QURL" \ --message-body "marker-survive-3-kills-${TS}" >/dev/null 2>&1 for k in 1 2 3; do echo " --- Kill #${k} ---" kill_pod wait_for_pod_ready wait_for_healthy echo " Kill #${k}: под восстановлен ✓" done # Маркер выжил? R=$(sqs "$T1_AK" "$T1_SK" receive-message --queue-url "$MARKER_QURL" \ --max-number-of-messages 1 --wait-time-seconds 2 2>&1) SEC15_DUR=$(($(date +%s) - SEC15_START)) echo " 3 рестарта за ${SEC15_DUR}s" check "Маркер выжил 3 рестарта" "$R" "marker-survive-3-kills" echo "" # ═══════════════════════════════════════════════════════════ # 16. CLEANUP # ═══════════════════════════════════════════════════════════ echo "═══ 16. Cleanup ═══" echo " Удаляю очереди..." # Основные очереди for i in $(seq 1 $TENANT_COUNT); do tak=$(cat "${TMPDIR_STRESS}/tak_${i}" 2>/dev/null) tsk=$(cat "${TMPDIR_STRESS}/tsk_${i}" 2>/dev/null) tid=$(cat "${TMPDIR_STRESS}/tid_${i}" 2>/dev/null) [[ -n "$tak" ]] && sqs "$tak" "$tsk" delete-queue --queue-url "${BASE_URL}/${tid}/${STRESS_Q}" >/dev/null 2>&1 done sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$BURST_QURL" >/dev/null 2>&1 sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$MIXED_QURL" >/dev/null 2>&1 sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$LONGPOLL_QURL" >/dev/null 2>&1 sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$RACE_QURL" >/dev/null 2>&1 sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$BATCH_QURL" >/dev/null 2>&1 sqs "$T1_AK" "$T1_SK" delete-queue --queue-url "$MARKER_QURL" >/dev/null 2>&1 # Flood-очереди for q in $(seq 1 $QUEUE_FLOOD_COUNT); do sqs "$T1_AK" "$T1_SK" delete-queue \ --queue-url "$(qurl "$T1_ID" "stress-flood-${q}-${TS}")" >/dev/null 2>&1 & done wait echo " Очереди удалены" TOTAL_DUR=$(($(date +%s) - STARTED_AT)) echo "" echo "╔═══════════════════════════════════════════════════════════════════╗" echo "║ ИТОГО: ${PASS}/${TOTAL} ✅ ${FAIL}/${TOTAL} ❌ ║" echo "║ Время: ${TOTAL_DUR}s ║" echo "╚═══════════════════════════════════════════════════════════════════╝" if [[ $FAIL -gt 0 ]]; then exit 1; fi