feat: add full comparative benchmark + stress test v2

- benchmark_full.sh: 10-section comparative benchmark (shared-sqs vs Yandex MQ)
  All 17 SQS commands tested, latency, throughput, message sizes, batch, long poll
- stress_test_v2.sh: improved stress test
- Key findings: large payload (64KB+) degradation needs investigation
This commit is contained in:
Naeel
2026-04-11 20:13:00 +03:00
parent b181b7ef0b
commit 406ec69679
2 changed files with 1754 additions and 0 deletions
+966
View File
@@ -0,0 +1,966 @@
#!/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=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
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=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}")
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 + 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
echo " ⏳ Long-poll: consumer ждёт, producers отправляют (30s)..."
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 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 ""
echo " Начальная загрузка: 100 msg"
# 10 Отправителей
for w in $(seq 1 10); 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 10); 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 3); 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 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 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))
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