657 lines
29 KiB
Bash
657 lines
29 KiB
Bash
#!/bin/bash
|
||
# stress_test.sh — стресс-тест shared-sqs: конкурентность, Redis resilience, падение пода
|
||
# Created: 2026-04-11
|
||
# Покрывает:
|
||
# 1. Параллельная отправка сообщений (N воркеров, M сообщений)
|
||
# 2. Параллельное чтение и удаление (гонка за сообщения)
|
||
# 3. Multi-tenant изоляция под нагрузкой (нет утечек между тенантами)
|
||
# 4. Потеря связи с Redis — поведение при разрыве
|
||
# 5. Kill пода — рестарт и восстановление данных из Redis
|
||
# 6. Burst — резкий всплеск запросов
|
||
# Запуск: 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"
|
||
DEPLOY_NAME="shared-sqs"
|
||
|
||
# Параметры нагрузки
|
||
CONCURRENT_WORKERS=10 # параллельных воркеров
|
||
MESSAGES_PER_WORKER=20 # сообщений на воркер
|
||
BURST_SIZE=50 # размер burst-пакета
|
||
TENANT_COUNT=3 # тенантов для multi-tenant теста
|
||
|
||
PASS=0
|
||
FAIL=0
|
||
TOTAL=0
|
||
TMPDIR_STRESS=$(mktemp -d /tmp/sqs_stress_XXXXX)
|
||
|
||
# ═══════════════════════════════════════════
|
||
# ХЕЛПЕРЫ
|
||
# ═══════════════════════════════════════════
|
||
|
||
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
|
||
}
|
||
|
||
# sqs — AWS CLI враппер
|
||
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 — 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
|
||
else
|
||
curl -s --max-time 20 -X "$method" \
|
||
-H "Authorization: Bearer $ADMIN_TOKEN" \
|
||
"${BASE_URL}${path}" 2>&1
|
||
fi
|
||
}
|
||
|
||
# wait_for_pod_ready — ждёт пока под поднимется (max 120s)
|
||
wait_for_pod_ready() {
|
||
local max_wait=120
|
||
local 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 \
|
||
-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))
|
||
done
|
||
echo " ⚠️ Под не готов после ${max_wait}s"
|
||
return 1
|
||
}
|
||
|
||
# wait_for_healthy — ждёт пока сервис отвечает на /health
|
||
wait_for_healthy() {
|
||
local max_wait=120
|
||
local 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))
|
||
done
|
||
echo " ⚠️ Сервис не отвечает после ${max_wait}s"
|
||
return 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 "╚══════════════════════════════════════════════════════════════╝"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 1. ПОДГОТОВКА — создаём тенантов и очереди
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 1. Подготовка — тенанты и очереди ═══"
|
||
|
||
# Создаём тенантов для теста
|
||
declare -a TENANT_AK
|
||
declare -a TENANT_SK
|
||
for i in $(seq 1 $TENANT_COUNT); do
|
||
R=$(admin_api POST "/admin/tenants" "{\"name\":\"stress-t${i}-${TS}\",\"access_key\":\"STRESSAK${i}${TS}\",\"secret_key\":\"STRESSSK${i}${TS}\"}")
|
||
TENANT_AK[$i]="STRESSAK${i}${TS}"
|
||
TENANT_SK[$i]="STRESSSK${i}${TS}"
|
||
echo " Тенант ${i}: AK=${TENANT_AK[$i]}"
|
||
done
|
||
|
||
# Очередь для 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")
|
||
done
|
||
echo " Очередь: $STRESS_Q создана для $TENANT_COUNT тенантов"
|
||
|
||
# Дополнительная очередь для burst-теста
|
||
BURST_Q="stress-burst-${TS}"
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" create-queue --queue-name "$BURST_Q")
|
||
echo " Очередь: $BURST_Q (burst)"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 2. КОНКУРЕНТНАЯ ОТПРАВКА — N воркеров пишут параллельно
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 2. Конкурентная отправка: ${CONCURRENT_WORKERS} воркеров × ${MESSAGES_PER_WORKER} сообщений ═══"
|
||
|
||
TOTAL_MSGS=$((CONCURRENT_WORKERS * MESSAGES_PER_WORKER))
|
||
echo " Всего будет отправлено: $TOTAL_MSGS сообщений"
|
||
|
||
# Запускаем воркеров параллельно
|
||
for w in $(seq 1 $CONCURRENT_WORKERS); do
|
||
(
|
||
ok=0
|
||
fail=0
|
||
for m in $(seq 1 $MESSAGES_PER_WORKER); do
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--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 "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--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 "${TENANT_AK[1]}" "${TENANT_SK[1]}" receive-message \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--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 "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-message \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--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 "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--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
|
||
# Каждый тенант отправляет уникальные сообщения в одноимённую очередь
|
||
for i in $(seq 1 $TENANT_COUNT); do
|
||
(
|
||
ok=0
|
||
for m in $(seq 1 $MSGS_PER_TENANT); do
|
||
R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--message-body "tenant-${i}-isolation-${m}" 2>&1)
|
||
if echo "$R" | grep -q "MessageId"; then ok=$((ok + 1)); fi
|
||
done
|
||
echo "$ok" > "${TMPDIR_STRESS}/tenant_send_${i}.result"
|
||
) &
|
||
done
|
||
wait
|
||
|
||
# Каждый тенант читает — должен видеть ТОЛЬКО свои сообщения
|
||
for i in $(seq 1 $TENANT_COUNT); do
|
||
(
|
||
own=0
|
||
foreign=0
|
||
empty_rounds=0
|
||
while [[ $empty_rounds -lt 5 ]]; do
|
||
R=$(sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" receive-message \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--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
|
||
# Проверяем что тело содержит tenant-{i}
|
||
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 "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" delete-message \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" \
|
||
--receipt-handle "$handle" >/dev/null 2>&1
|
||
done <<< "$HANDLES"
|
||
done
|
||
echo "${own}:${foreign}" > "${TMPDIR_STRESS}/tenant_recv_${i}.result"
|
||
) &
|
||
done
|
||
wait
|
||
|
||
# Проверяем: чужих сообщений быть не должно
|
||
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
|
||
check_eq "Чужих сообщений: 0 (tenant isolation)" "$ALL_FOREIGN" "0"
|
||
check_ge "Свои сообщения получены" "$ALL_OWN" "$((TENANT_COUNT * MSGS_PER_TENANT))"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 5. BURST — резкий всплеск запросов
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 5. Burst: ${BURST_SIZE} сообщений одновременно ═══"
|
||
|
||
# Отправляем BURST_SIZE сообщений максимально параллельно
|
||
for b in $(seq 1 $BURST_SIZE); do
|
||
(
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${BURST_Q}" \
|
||
--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"
|
||
) &
|
||
done
|
||
echo " ⏳ Ожидание burst..."
|
||
wait
|
||
|
||
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
|
||
done
|
||
echo " Burst: ${BURST_OK}/${BURST_SIZE} успешно"
|
||
check_ge "Burst: >= 90% успешно" "$BURST_OK" "$((BURST_SIZE * 90 / 100))"
|
||
|
||
# Проверяем что очередь получила все
|
||
sleep 3
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \
|
||
--queue-url "${BASE_URL}/queue/${BURST_Q}" \
|
||
--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. KILL POD — рестарт и восстановление из Redis
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 6. Kill pod — рестарт и восстановление из Redis ═══"
|
||
|
||
# Перед киллом — запоминаем сколько сообщений в burst-очереди
|
||
BEFORE_KILL="${BURST_APPROX:-0}"
|
||
echo " Сообщений в burst-очереди до kill: $BEFORE_KILL"
|
||
|
||
# Отправляем ещё пачку — чтобы были "свежие" данные в Redis
|
||
EXTRA_MSGS=15
|
||
for m in $(seq 1 $EXTRA_MSGS); do
|
||
sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${BURST_Q}" \
|
||
--message-body "pre-kill-${m}-${TS}" >/dev/null 2>&1
|
||
done
|
||
EXPECTED_AFTER=$((BEFORE_KILL + EXTRA_MSGS))
|
||
echo " Отправлено ещё $EXTRA_MSGS сообщений. Ожидаем: ${EXPECTED_AFTER}"
|
||
|
||
# Убиваем под
|
||
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 " Под удалён, ждём рестарт..."
|
||
|
||
# Ждём пока новый под поднимется
|
||
wait_for_pod_ready
|
||
wait_for_healthy
|
||
|
||
# Проверяем что burst-очередь восстановилась из Redis
|
||
sleep 3
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \
|
||
--queue-url "${BASE_URL}/queue/${BURST_Q}" \
|
||
--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"
|
||
|
||
# Проверяем что тенанты тоже восстановились
|
||
R=$(admin_api GET "/admin/tenants")
|
||
TENANT_CHECK=$(echo "$R" | grep -c "stress-t1-${TS}" || true)
|
||
check_ge "Тенанты восстановлены из Redis" "$TENANT_CHECK" "1"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 7. REDIS DISCONNECT — сервис при потере Redis
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 7. 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
|
||
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: заблокирован egress к Redis (10.0.0.0/8 except DNS)"
|
||
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"
|
||
TOTAL=$((TOTAL + 1))
|
||
# Сервис может вернуть 200 (из кеша) или 500/503 (graceful), но НЕ пустоту/таймаут
|
||
if [[ "$R_CODE" =~ ^[2345][0-9][0-9]$ ]]; then
|
||
PASS=$((PASS + 1))
|
||
echo " ✅ Сервис отвечает при потере Redis (HTTP $R_CODE)"
|
||
else
|
||
FAIL=$((FAIL + 1))
|
||
echo " ❌ Сервис не отвечает при потере Redis (HTTP $R_CODE)"
|
||
fi
|
||
|
||
# Восстанавливаем доступ к Redis — удаляем NetworkPolicy
|
||
kubectl delete networkpolicy stress-test-block-redis -n "$NAMESPACE" 2>&1 | head -1
|
||
echo " NetworkPolicy удалена — Redis доступен снова"
|
||
sleep 5
|
||
|
||
# Проверяем что после восстановления Redis сервис работает
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${BURST_Q}" \
|
||
--message-body "after-redis-recovery-${TS}" 2>&1)
|
||
check "Сервис работает после восстановления Redis" "$R" "MessageId"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 8. ПАРАЛЛЕЛЬНЫЕ ОПЕРАЦИИ НА ОДНОЙ ОЧЕРЕДИ (смешанная нагрузка)
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 8. Смешанная нагрузка: send + receive + delete одновременно ═══"
|
||
|
||
MIXED_Q="stress-mixed-${TS}"
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" create-queue --queue-name "$MIXED_Q")
|
||
|
||
# Заполняем 50 сообщений
|
||
for m in $(seq 1 50); do
|
||
sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${MIXED_Q}" \
|
||
--message-body "mixed-init-${m}" >/dev/null 2>&1
|
||
done
|
||
echo " Начальная загрузка: 50 сообщений"
|
||
|
||
# Параллельно: 5 отправителей + 5 читателей-удалителей + 2 GetQueueAttributes
|
||
MIXED_DURATION=15 # секунды
|
||
|
||
# Отправители
|
||
for w in $(seq 1 5); do
|
||
(
|
||
end=$((SECONDS + MIXED_DURATION))
|
||
ok=0
|
||
while [[ $SECONDS -lt $end ]]; do
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" send-message \
|
||
--queue-url "${BASE_URL}/queue/${MIXED_Q}" \
|
||
--message-body "mixed-send-${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
|
||
(
|
||
end=$((SECONDS + MIXED_DURATION))
|
||
recv=0
|
||
while [[ $SECONDS -lt $end ]]; do
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" receive-message \
|
||
--queue-url "${BASE_URL}/queue/${MIXED_Q}" \
|
||
--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 "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-message \
|
||
--queue-url "${BASE_URL}/queue/${MIXED_Q}" \
|
||
--receipt-handle "$handle" >/dev/null 2>&1
|
||
recv=$((recv + 1))
|
||
done <<< "$HANDLES"
|
||
done
|
||
echo "$recv" > "${TMPDIR_STRESS}/mixed_recv_${w}.result"
|
||
) &
|
||
done
|
||
|
||
# GetQueueAttributes (не должен падать при нагрузке)
|
||
for w in $(seq 1 2); do
|
||
(
|
||
end=$((SECONDS + MIXED_DURATION))
|
||
ok=0
|
||
fail=0
|
||
while [[ $SECONDS -lt $end ]]; do
|
||
R=$(sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" get-queue-attributes \
|
||
--queue-url "${BASE_URL}/queue/${MIXED_Q}" \
|
||
--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
|
||
|
||
echo " ⏳ Смешанная нагрузка ${MIXED_DURATION}s..."
|
||
wait
|
||
|
||
# Подсчёт
|
||
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
|
||
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"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# 9. CLEANUP
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "═══ 9. Cleanup ═══"
|
||
|
||
for i in $(seq 1 $TENANT_COUNT); do
|
||
sqs "${TENANT_AK[$i]}" "${TENANT_SK[$i]}" delete-queue \
|
||
--queue-url "${BASE_URL}/queue/${STRESS_Q}" >/dev/null 2>&1
|
||
done
|
||
sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-queue \
|
||
--queue-url "${BASE_URL}/queue/${BURST_Q}" >/dev/null 2>&1
|
||
sqs "${TENANT_AK[1]}" "${TENANT_SK[1]}" delete-queue \
|
||
--queue-url "${BASE_URL}/queue/${MIXED_Q}" >/dev/null 2>&1
|
||
echo " Очереди и тенанты очищены"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════
|
||
# ИТОГИ
|
||
# ═══════════════════════════════════════════════════════════
|
||
echo "╔══════════════════════════════════════════════════════════════╗"
|
||
echo "║ ИТОГО: ${PASS}/${TOTAL} ✅ ${FAIL}/${TOTAL} ❌ ║"
|
||
echo "╚══════════════════════════════════════════════════════════════╝"
|
||
|
||
if [[ $FAIL -gt 0 ]]; then
|
||
exit 1
|
||
fi
|