From bd8303ca2e79aba55fd406b6eb0963348a7f50a1 Mon Sep 17 00:00:00 2001 From: Naeel Date: Sat, 11 Apr 2026 16:41:07 +0300 Subject: [PATCH] =?UTF-8?q?test:=20stress=5Ftest.sh=20v2=20=E2=80=94=2050?= =?UTF-8?q?=20=D0=B2=D0=BE=D1=80=D0=BA=D0=B5=D1=80=D0=BE=D0=B2,=201000=20m?= =?UTF-8?q?sg,=20long-poll,=20race,=20batch,=2030=20=D0=BE=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B5=D0=B9,=20memory,=20multi-kill,=20verbo?= =?UTF-8?q?se=20progress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/stress_test.sh | 1010 ++++++++++++++++++++++++++---------------- 1 file changed, 638 insertions(+), 372 deletions(-) mode change 100644 => 100755 tests/stress_test.sh diff --git a/tests/stress_test.sh b/tests/stress_test.sh old mode 100644 new mode 100755 index 3d8ca45..bb4d139 --- a/tests/stress_test.sh +++ b/tests/stress_test.sh @@ -1,14 +1,23 @@ #!/bin/bash -# stress_test.sh — стресс-тест shared-sqs: конкурентность, Redis resilience, падение пода +# stress_test.sh — ЖЁСТКИЙ стресс-тест shared-sqs # Created: 2026-04-11 -# Updated: 2026-04-11 — fix: правильный формат queue URL, парсинг tenant API response +# Updated: 2026-04-11 — v2: масштаб, long-poll, race, batch, 50 очередей, memory, multi-kill # Покрывает: -# 1. Параллельная отправка сообщений (N воркеров, M сообщений) -# 2. Параллельное чтение и удаление (гонка за сообщения) -# 3. Multi-tenant изоляция под нагрузкой (нет утечек между тенантами) -# 4. Потеря связи с Redis — поведение при разрыве -# 5. Kill пода — рестарт и восстановление данных из Redis -# 6. Burst — резкий всплеск запросов +# 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 @@ -20,13 +29,16 @@ BASE_URL="${BASE_URL:-https://qu.kube5s.ru}" ADMIN_TOKEN="${ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" TS=$(date +%s) NAMESPACE="shared-sqs" -DEPLOY_NAME="shared-sqs" +POD_LABEL="app.kubernetes.io/name=shared-sqs" -# Параметры нагрузки -CONCURRENT_WORKERS=10 # параллельных воркеров -MESSAGES_PER_WORKER=20 # сообщений на воркер -BURST_SIZE=50 # размер burst-пакета -TENANT_COUNT=3 # тенантов для multi-tenant теста +# Параметры нагрузки — ЖЁСТКИЕ +CONCURRENT_WORKERS=50 # параллельных воркеров +MESSAGES_PER_WORKER=20 # сообщений на воркер (итого 1000) +BURST_SIZE=100 # размер burst-пакета +TENANT_COUNT=5 # тенантов для multi-tenant теста +QUEUE_FLOOD_COUNT=30 # очередей на тенанта +MIXED_DURATION=20 # длительность смешанной нагрузки (сек) +BATCH_WORKERS=10 # воркеров для batch-операций PASS=0 FAIL=0 @@ -37,11 +49,20 @@ TMPDIR_STRESS=$(mktemp -d /tmp/sqs_stress_XXXXX) # ХЕЛПЕРЫ # ═══════════════════════════════════════════ -# json_field — извлекает значение поля из JSON (без зависимости от jq) 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)) @@ -80,7 +101,18 @@ check_ge() { fi } -# sqs — AWS CLI враппер +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 @@ -90,30 +122,27 @@ sqs() { aws --endpoint-url "$BASE_URL" --output json sqs "$@" 2>&1 } -# admin_api — Admin REST API 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 + -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 + -H "Authorization: Bearer $ADMIN_TOKEN" "${BASE_URL}${path}" 2>&1 fi } -# wait_for_pod_ready — ждёт пока под поднимется (max 120s) +qurl() { echo "${BASE_URL}/${1}/${2}"; } + wait_for_pod_ready() { - local max_wait=120 - local waited=0 + 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 app.kubernetes.io/name=shared-sqs \ + 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)" @@ -121,15 +150,15 @@ wait_for_pod_ready() { fi sleep 2 waited=$((waited + 2)) + printf "." >&2 done + echo "" echo " ⚠️ Под не готов после ${max_wait}s" return 1 } -# wait_for_healthy — ждёт пока сервис отвечает на /health wait_for_healthy() { - local max_wait=120 - local waited=0 + local max_wait=120 waited=0 echo " ⏳ Ожидание /health (max ${max_wait}s)..." while [[ $waited -lt $max_wait ]]; do local status @@ -140,201 +169,194 @@ wait_for_healthy() { fi sleep 2 waited=$((waited + 2)) + printf "." >&2 done + echo "" echo " ⚠️ Сервис не отвечает после ${max_wait}s" return 1 } -cleanup() { - rm -rf "$TMPDIR_STRESS" 2>/dev/null +# 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 -echo "╔══════════════════════════════════════════════════════════════╗" -echo "║ shared-sqs STRESS TEST — $(date '+%Y-%m-%d %H:%M:%S') ║" -echo "║ Endpoint: ${BASE_URL} ║" -echo "║ Workers: ${CONCURRENT_WORKERS} Msgs/worker: ${MESSAGES_PER_WORKER} Burst: ${BURST_SIZE} ║" -echo "╚══════════════════════════════════════════════════════════════╝" +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. ПОДГОТОВКА — создаём тенантов и очереди через Admin API +# 1. ПОДГОТОВКА — тенанты и очереди # ═══════════════════════════════════════════════════════════ -echo "═══ 1. Подготовка — тенанты и очереди ═══" +echo "═══ 1. Подготовка — создаём $TENANT_COUNT тенантов ═══" -# Создаём тенантов — API сам генерирует access_key/secret_key -declare -a TENANT_AK -declare -a TENANT_SK -declare -a TENANT_ID +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\":50}") + 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]}" + echo " Тенант ${i}: ID=${TENANT_ID[$i]} AK=${TENANT_AK[$i]:0:20}..." if [[ -z "${TENANT_AK[$i]}" ]]; then - echo " ⛔ ОШИБКА: не удалось создать тенанта ${i}. Ответ: $R" + echo " ⛔ ОШИБКА создания тенанта ${i}: $R" exit 1 fi done -# qurl — формирует правильный Queue URL: BASE_URL/TID/QUEUE_NAME -# Формат: https://qu.kube5s.ru/{tenant_id}/{queue_name} -qurl() { - echo "${BASE_URL}/${1}/${2}" -} - -# Очередь для concurrency-теста (одно имя, но у каждого тенанта — своя изолированная) -STRESS_Q="stress-concurrent-${TS}" -for i in $(seq 1 $TENANT_COUNT); do - R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" create-queue --queue-name "$STRESS_Q") - echo " Тенант ${i}: создана очередь $STRESS_Q → $(echo "$R" | json_field QueueUrl)" -done - -# Дополнительная очередь для burst-теста -BURST_Q="stress-burst-${TS}" -R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" create-queue --queue-name "$BURST_Q") -echo " Burst очередь: $BURST_Q" -echo "" - -# ═══════════════════════════════════════════════════════════ -# 2. КОНКУРЕНТНАЯ ОТПРАВКА — N воркеров пишут параллельно -# ═══════════════════════════════════════════════════════════ -echo "═══ 2. Конкурентная отправка: ${CONCURRENT_WORKERS} воркеров × ${MESSAGES_PER_WORKER} сообщений ═══" - -TOTAL_MSGS=$((CONCURRENT_WORKERS * MESSAGES_PER_WORKER)) -echo " Всего будет отправлено: $TOTAL_MSGS сообщений" - -# Сохраняем credentials первого тенанта для удобства в subshell -T1_AK="${TENANT_AK[1]}" -T1_SK="${TENANT_SK[1]}" -T1_ID="${TENANT_ID[1]}" -STRESS_QURL=$(qurl "$T1_ID" "$STRESS_Q") -BURST_QURL=$(qurl "$T1_ID" "$BURST_Q") - -# Запускаем воркеров параллельно -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 "worker-${w}-msg-${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" - ) & -done -echo " ⏳ Ожидание завершения ${CONCURRENT_WORKERS} воркеров..." -wait - -# Подсчитываем результаты -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 -echo " Отправлено: ${SEND_OK} ok, ${SEND_FAIL} fail" -check_eq "Все сообщения отправлены" "$SEND_OK" "$TOTAL_MSGS" - -# Проверяем через GetQueueAttributes — ApproximateNumberOfMessages -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 " Очередь сообщает: ~${APPROX} сообщений" -check_eq "Количество в очереди совпадает" "${APPROX:-0}" "$TOTAL_MSGS" -echo "" - -# ═══════════════════════════════════════════════════════════ -# 3. КОНКУРЕНТНОЕ ЧТЕНИЕ — гонка за сообщения -# ═══════════════════════════════════════════════════════════ -echo "═══ 3. Конкурентное чтение: ${CONCURRENT_WORKERS} воркеров забирают ═══" - -# Каждый воркер читает и удаляет -for w in $(seq 1 $CONCURRENT_WORKERS); do - ( - received=0 - deleted=0 - empty_rounds=0 - # Читаем пока есть сообщения (max 10 пустых подряд — выход) - while [[ $empty_rounds -lt 10 ]]; 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 - DR=$(sqs "$T1_AK" "$T1_SK" delete-message \ - --queue-url "$STRESS_QURL" \ - --receipt-handle "$handle" 2>&1) - deleted=$((deleted + 1)) - done <<< "$HANDLES" - done - echo "${received}:${deleted}" > "${TMPDIR_STRESS}/recv_w${w}.result" - ) & -done -echo " ⏳ Ожидание воркеров-читателей..." -wait - -# Подсчёт -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 -echo " Прочитано: ${RECV_TOTAL}, удалено: ${DEL_TOTAL}" -# Каждое сообщение может быть прочитано несколько раз (visibility timeout), -# но удалённых не должно быть больше отправленных -check_ge "Прочитано >= отправлено (с повторами ок)" "$RECV_TOTAL" "$TOTAL_MSGS" -check_eq "Удалено = отправлено (нет дубликатов)" "$DEL_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 ИЗОЛЯЦИЯ ПОД НАГРУЗКОЙ -# ═══════════════════════════════════════════════════════════ -echo "═══ 4. Multi-tenant изоляция: ${TENANT_COUNT} тенантов, параллельная нагрузка ═══" - -MSGS_PER_TENANT=30 -# Сохраняем массивы в файлы — subshell не наследует bash-массивы надёжно +# Сохраняем в файлы — для 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=50 +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}") @@ -343,168 +365,372 @@ for i in $(seq 1 $TENANT_COUNT); do 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}-isolation-${m}" 2>&1) + 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 + 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) + 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 + if [[ "$MSG_COUNT" -eq 0 ]]; then empty_rounds=$((empty_rounds + 1)); continue; fi empty_rounds=0 - # Проверяем что тело содержит tenant-{i} while IFS= read -r body; do - if echo "$body" | grep -q "tenant-${i}-"; then - own=$((own + 1)) - else - foreign=$((foreign + 1)) - fi + 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 + 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 +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)) + ALL_OWN=$((ALL_OWN + own)); ALL_FOREIGN=$((ALL_FOREIGN + foreign)) fi done -check_eq "Чужих сообщений: 0 (tenant isolation)" "$ALL_FOREIGN" "0" +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 — резкий всплеск запросов +# 5. BURST — 100 сообщений одновременно # ═══════════════════════════════════════════════════════════ echo "═══ 5. Burst: ${BURST_SIZE} сообщений одновременно ═══" +SEC5_START=$(date +%s) -# Отправляем BURST_SIZE сообщений максимально параллельно for b in $(seq 1 $BURST_SIZE); do ( - R=$(sqs "$T1_AK" "$T1_SK" send-message \ - --queue-url "$BURST_QURL" \ + 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..." +echo " ⏳ Burst отправка..." wait +echo "" BURST_OK=0 for b in $(seq 1 $BURST_SIZE); do - if [[ -f "${TMPDIR_STRESS}/burst_${b}.result" ]]; then - val=$(cat "${TMPDIR_STRESS}/burst_${b}.result") - BURST_OK=$((BURST_OK + val)) - fi + [[ -f "${TMPDIR_STRESS}/burst_${b}.result" ]] && BURST_OK=$((BURST_OK + $(cat "${TMPDIR_STRESS}/burst_${b}.result"))) done -echo " Burst: ${BURST_OK}/${BURST_SIZE} успешно" -check_ge "Burst: >= 90% успешно" "$BURST_OK" "$((BURST_SIZE * 90 / 100))" +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" \ +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 " Очередь burst: ~${BURST_APPROX}" +check_eq "Burst все в очереди" "${BURST_APPROX:-0}" "$BURST_OK" echo "" # ═══════════════════════════════════════════════════════════ -# 6. KILL POD — рестарт и восстановление из Redis +# 6. LONG-POLLING + параллельные producer'ы # ═══════════════════════════════════════════════════════════ -echo "═══ 6. Kill pod — рестарт и восстановление из Redis ═══" +echo "═══ 6. Long-polling: consumer WaitTime=10s + 5 producers ═══" +SEC6_START=$(date +%s) -# Перед киллом — запоминаем сколько сообщений в burst-очереди -BEFORE_KILL="${BURST_APPROX:-0}" -echo " Сообщений в burst-очереди до kill: $BEFORE_KILL" +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") -# Отправляем ещё пачку — чтобы были "свежие" данные в Redis -EXTRA_MSGS=15 -for m in $(seq 1 $EXTRA_MSGS); do - sqs "$T1_AK" "$T1_SK" send-message \ - --queue-url "$BURST_QURL" \ - --message-body "pre-kill-${m}-${TS}" >/dev/null 2>&1 +# Consumer: читает с long-poll, считает сколько получит за 30s +( + end=$((SECONDS + 30)) + 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 -EXPECTED_AFTER=$((BEFORE_KILL + EXTRA_MSGS)) -echo " Отправлено ещё $EXTRA_MSGS сообщений. Ожидаем: ${EXPECTED_AFTER}" +echo " ⏳ Long-poll: consumer ждёт, producers отправляют (30s)..." +wait +echo "" -# Убиваем под -echo " 🔪 Убиваю под..." -POD_NAME=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=shared-sqs \ - -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) -echo " Pod: $POD_NAME" -kubectl delete pod "$POD_NAME" -n "$NAMESPACE" --grace-period=0 --force 2>&1 | head -2 -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 -# Проверяем что burst-очередь восстановилась из Redis sleep 3 -R=$(sqs "$T1_AK" "$T1_SK" get-queue-attributes \ - --queue-url "$BURST_QURL" \ +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}" "$EXPECTED_AFTER" +check_eq "Данные восстановлены из Redis" "${AFTER_KILL:-0}" "${BEFORE_KILL}" -# Проверяем что тенанты тоже восстановились R=$(admin_api GET "/admin/tenants") -TENANT_CHECK=$(echo "$R" | grep -c "stress-t1-${TS}" || true) -check_ge "Тенанты восстановлены из Redis" "$TENANT_CHECK" "1" +TC=$(echo "$R" | grep -c "stress-t1-${TS}" || true) +check_ge "Тенанты восстановлены" "$TC" "1" echo "" # ═══════════════════════════════════════════════════════════ -# 7. REDIS DISCONNECT — сервис при потере Redis +# 12. REDIS DISCONNECT / RECONNECT # ═══════════════════════════════════════════════════════════ -echo "═══ 7. Redis disconnect simulation ═══" +echo "═══ 12. Redis disconnect simulation ═══" -# Находим Redis endpoint из env пода -NEW_POD=$(kubectl get pods -n "$NAMESPACE" -l app.kubernetes.io/name=shared-sqs \ - -o jsonpath='{.items[0].metadata.name}' 2>/dev/null) - -# Добавляем NetworkPolicy чтобы заблокировать трафик к Redis -cat <<'NETPOL' | kubectl apply -n "$NAMESPACE" -f - 2>&1 +cat <<'NETPOL' | kubectl apply -n "$NAMESPACE" -f - 2>&1 | head -1 apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: @@ -534,85 +760,71 @@ spec: - protocol: UDP port: 53 NETPOL -echo " NetworkPolicy: заблокирован egress к Redis (10.0.0.0/8 except DNS)" +echo " NetworkPolicy applied — Redis заблокирован" sleep 5 -# Пробуем API — должен вернуть ошибку или graceful degradation, не 502 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 status при потере Redis: $R_CODE" + -H "Authorization: Bearer $ADMIN_TOKEN" "${BASE_URL}/admin/tenants" 2>/dev/null) +echo " HTTP при потере Redis: $R_CODE" TOTAL=$((TOTAL + 1)) -# Сервис может вернуть 200 (из кеша) или 500/503 (graceful), но НЕ пустоту/таймаут if [[ "$R_CODE" =~ ^[2345][0-9][0-9]$ ]]; then - PASS=$((PASS + 1)) - echo " ✅ Сервис отвечает при потере Redis (HTTP $R_CODE)" + PASS=$((PASS + 1)); echo " ✅ Сервис отвечает (HTTP $R_CODE)" else - FAIL=$((FAIL + 1)) - echo " ❌ Сервис не отвечает при потере Redis (HTTP $R_CODE)" + FAIL=$((FAIL + 1)); echo " ❌ Сервис не отвечает (HTTP $R_CODE)" fi -# Восстанавливаем доступ к Redis — удаляем NetworkPolicy kubectl delete networkpolicy stress-test-block-redis -n "$NAMESPACE" 2>&1 | head -1 -echo " NetworkPolicy удалена — Redis доступен снова" +echo " NetworkPolicy удалена — Redis доступен" sleep 5 -# Проверяем что после восстановления Redis сервис работает -R=$(sqs "$T1_AK" "$T1_SK" send-message \ - --queue-url "$BURST_QURL" \ +R=$(sqs "$T1_AK" "$T1_SK" send-message --queue-url "$BURST_QURL" \ --message-body "after-redis-recovery-${TS}" 2>&1) -check "Сервис работает после восстановления Redis" "$R" "MessageId" +check "Сервис работает после Redis reconnect" "$R" "MessageId" echo "" # ═══════════════════════════════════════════════════════════ -# 8. ПАРАЛЛЕЛЬНЫЕ ОПЕРАЦИИ НА ОДНОЙ ОЧЕРЕДИ (смешанная нагрузка) +# 13. СМЕШАННАЯ НАГРУЗКА — send + receive + delete + getattr (20s) # ═══════════════════════════════════════════════════════════ -echo "═══ 8. Смешанная нагрузка: send + receive + delete одновременно ═══" +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") -# Заполняем 50 сообщений -for m in $(seq 1 50); do - sqs "$T1_AK" "$T1_SK" send-message \ - --queue-url "$MIXED_QURL" \ +# Предзагрузка 100 msg +for m in $(seq 1 100); 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 " Начальная загрузка: 50 сообщений" +echo "" +echo " Начальная загрузка: 100 msg" -# Параллельно: 5 отправителей + 5 читателей-удалителей + 2 GetQueueAttributes -MIXED_DURATION=15 # секунды - -# Отправители -for w in $(seq 1 5); do +# 10 Отправителей +for w in $(seq 1 10); do ( - end=$((SECONDS + MIXED_DURATION)) - ok=0 + 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-send-${w}-$(date +%s%N)" 2>&1) + 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 -# Читатели-удалители -for w in $(seq 1 5); do +# 10 Читателей-удалителей +for w in $(seq 1 10); do ( - end=$((SECONDS + MIXED_DURATION)) - recv=0 + end=$((SECONDS + MIXED_DURATION)); recv=0 while [[ $SECONDS -lt $end ]]; do - R=$(sqs "$T1_AK" "$T1_SK" receive-message \ - --queue-url "$MIXED_QURL" \ + 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" \ + sqs "$T1_AK" "$T1_SK" delete-message --queue-url "$MIXED_QURL" \ --receipt-handle "$handle" >/dev/null 2>&1 recv=$((recv + 1)) done <<< "$HANDLES" @@ -621,80 +833,134 @@ for w in $(seq 1 5); do ) & done -# GetQueueAttributes (не должен падать при нагрузке) -for w in $(seq 1 2); do +# 3 GetQueueAttributes +for w in $(seq 1 3); do ( - end=$((SECONDS + MIXED_DURATION)) - ok=0 - fail=0 + 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" \ + 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 + 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 +MIXED_SENT=0; MIXED_RECV=0; ATTR_OK=0; ATTR_FAIL=0 +for w in $(seq 1 10); 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 +for w in $(seq 1 3); 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)) + ATTR_OK=$((ATTR_OK + ok)); ATTR_FAIL=$((ATTR_FAIL + fail)) fi done -echo " Отправлено: $MIXED_SENT, прочитано+удалено: $MIXED_RECV" -echo " GetQueueAttributes: ok=$ATTR_OK, fail=$ATTR_FAIL" - -check_ge "Смешанная нагрузка: отправлено > 0" "$MIXED_SENT" "1" -check_ge "Смешанная нагрузка: прочитано > 0" "$MIXED_RECV" "1" -check_eq "GetQueueAttributes: ни одного сбоя" "$ATTR_FAIL" "0" +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 "" # ═══════════════════════════════════════════════════════════ -# 9. CLEANUP +# 14. MEMORY CHECK — после нагрузки # ═══════════════════════════════════════════════════════════ -echo "═══ 9. Cleanup ═══" - -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}") - 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 -echo " Очереди и тенанты очищены" -echo "" - -# ═══════════════════════════════════════════════════════════ -# ИТОГИ -# ═══════════════════════════════════════════════════════════ -echo "╔══════════════════════════════════════════════════════════════╗" -echo "║ ИТОГО: ${PASS}/${TOTAL} ✅ ${FAIL}/${TOTAL} ❌ ║" -echo "╚══════════════════════════════════════════════════════════════╝" - -if [[ $FAIL -gt 0 ]]; then - exit 1 +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