#!/bin/bash # compare_sqs.sh — Сравнительный тест двух SQS реализаций # Created: 2026-04-10 # # Сравниваем: # A) sqs-operator — https://sqs.kube5s.ru/sqs/{tenant} (1 тенант = 1 namespace в k8s) # B) shared-sqs — https://qu.kube5s.ru (Single-process, multitenancy через API) # # Метрики: # - Время создания тенанта # - Время CRUD очереди (create/send/receive/delete) # - Время удаления тенанта # - Ресурсы pods после N тенантов (kubectl top) # # Запуск: # bash compare_sqs.sh [TENANTS] # TENANTS=10 bash compare_sqs.sh # TENANTS=20 bash compare_sqs.sh # TENANTS=50 bash compare_sqs.sh # # Требования: # - kubectl с доступом к кластеру # - curl # - python3 # - aws CLI (для shared-sqs CRUD) # # Для sqs-operator: тенант создаётся через kubectl apply QueueService CR # Для shared-sqs: тенант создаётся через POST /admin/tenants set -uo pipefail # ════════════════════════════════════════════ # КОНФИГ # ════════════════════════════════════════════ # Количество тенантов — передаётся аргументом или переменной или по умолчанию 10 TENANTS="${1:-${TENANTS:-10}}" # sqs-operator OP_HOST="https://sqs.kube5s.ru" OP_CR_NAMESPACE="sqs-operator-system" # Namespace тенантов в sqs-operator: sless-fn-{tenant} OP_TENANT_NS_PREFIX="sless-fn" # Шаблон QueueService CR (apiVersion из реального кода) OP_CR_TEMPLATE='apiVersion: sqs.kube5s.ru/v1alpha1 kind: QueueService metadata: name: CRNAME namespace: sqs-operator-system spec: tenantId: TENANTID memoryMB: 64 storageMB: 512 persistence: false' # shared-sqs SS_HOST="https://qu.kube5s.ru" SS_ADMIN_TOKEN="${SS_ADMIN_TOKEN:-sqs-admin-7a7d8bd0c060a75c198d48680f34077a}" SS_REGION="us-east-1" SS_NAMESPACE="sless" # namespace где живёт shared-sqs pod (для kubectl top) # Имя пода shared-sqs в k8s (для ресурсов) SS_DEPLOY_LABEL="app=shared-sqs" OP_MANAGER_LABEL="control-plane=controller-manager" OP_TENANT_LABEL_PREFIX="sless.dev/tenant" # Таймаут ожидания QueueService Ready (секунд) OP_READY_TIMEOUT=120 # ════════════════════════════════════════════ # УТИЛИТЫ # ════════════════════════════════════════════ PASS=0; FAIL=0 RESULTS_FILE="/tmp/sqs_compare_$(date +%s).log" ok() { echo " ✅ $1"; PASS=$((PASS+1)); } fail() { echo " ❌ $1"; FAIL=$((FAIL+1)); } hdr() { echo ""; echo "══════════════════════════════════════════"; echo " $1"; echo "══════════════════════════════════════════"; } ts() { date '+%H:%M:%S'; } elapsed_ms() { # $1 = start seconds (с дробной частью от date +%s%3N) local start="$1" local end end=$(date +%s%3N) echo $(( end - start )) } # Запись результата в файл для итоговой таблицы record() { # $1=impl $2=phase $3=n_tenants $4=value_ms $5=label echo "$1,$2,$3,$4,$5" >> "$RESULTS_FILE" } # ─── sqs-operator API ─── op_sqs() { # $1=endpoint $2=ak $3=sk $4=query_params curl -sk --max-time 15 \ --aws-sigv4 "aws:amz:us-east-1:sqs" \ --user "$2:$3" \ "${1}/?${4}&Version=2012-11-05" } # Ожидать QueueService Phase=Ready op_wait_ready() { local crname="$1" max_sec="$2" local start start=$(date +%s) while true; do local phase phase=$(kubectl get queueservice "$crname" -n "$OP_CR_NAMESPACE" \ -o jsonpath='{.status.phase}' 2>/dev/null || echo "") [ "$phase" = "Ready" ] && return 0 local now now=$(date +%s) [ $(( now - start )) -ge "$max_sec" ] && return 1 sleep 2 done } # Проверить что QueueService CR API доступен (идемпотентно) op_check_api() { local tenantid="$1" local ep="${OP_HOST}/sqs/${tenantid}" # Для credentials нужен secret в k8s local ns="${OP_TENANT_NS_PREFIX}-${tenantid}" local ak sk ak=$(kubectl -n "$ns" get secret "sqs-creds-${tenantid}" \ -o jsonpath='{.data.accessKey}' 2>/dev/null | base64 -d 2>/dev/null || echo "") sk=$(kubectl -n "$ns" get secret "sqs-creds-${tenantid}" \ -o jsonpath='{.data.secretKey}' 2>/dev/null | base64 -d 2>/dev/null || echo "") echo "$ak:$sk:$ep" } # ─── shared-sqs Admin API ─── ss_admin() { local method="$1" path="$2" body="${3:-}" if [[ -n "$body" ]]; then curl -sf --max-time 15 \ -X "$method" \ -H "Authorization: Bearer $SS_ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d "$body" \ "${SS_HOST}${path}" 2>&1 else curl -sf --max-time 15 \ -X "$method" \ -H "Authorization: Bearer $SS_ADMIN_TOKEN" \ "${SS_HOST}${path}" 2>&1 fi } ss_sqs() { # $1=ak $2=sk $3=action_params curl -sk --max-time 15 \ -u "$1:$2" \ "${SS_HOST}/?${3}&Version=2012-11-05" 2>&1 } # ════════════════════════════════════════════ # ПРОВЕРКА ДОСТУПНОСТИ # ════════════════════════════════════════════ hdr "0. Проверка доступности (N=$TENANTS тенантов)" OP_HEALTH=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "${OP_HOST}/sqs/test001" 2>/dev/null || echo "0") SS_HEALTH=$(curl -sk --max-time 5 -o /dev/null -w "%{http_code}" "${SS_HOST}/health" 2>/dev/null || echo "0") echo " sqs-operator (${OP_HOST}): HTTP $OP_HEALTH" echo " shared-sqs (${SS_HOST}): HTTP $SS_HEALTH" # kubectl доступен? KUBECTL_OK=false if kubectl cluster-info &>/dev/null; then KUBECTL_OK=true echo " kubectl: OK ($(kubectl config current-context))" else echo " kubectl: НЕДОСТУПЕН — метрики ресурсов будут пропущены" fi # aws cli доступен? AWS_CLI_OK=false if command -v aws &>/dev/null; then AWS_CLI_OK=true echo " aws CLI: OK" else echo " aws CLI: НЕДОСТУПЕН — shared-sqs CRUD будет через curl" fi # ════════════════════════════════════════════ # ФАЗА 1 — Создание N тенантов # ════════════════════════════════════════════ hdr "1. Создание $TENANTS тенантов" # Массивы для хранения созданных тенантов declare -a OP_TENANTS=() # tenantId declare -a OP_CR_NAMES=() # имя CR declare -a SS_TENANT_IDS=() # UUID тенанта declare -a SS_AK_LIST=() # Access Keys declare -a SS_SK_LIST=() # Secret Keys echo "" echo " ── A) sqs-operator ──" echo " (каждый тенант = QueueService CR → новый namespace + deployment + secret)" echo "" OP_TOTAL_CREATE_MS=0 OP_CREATE_TIMES=() for i in $(seq 1 "$TENANTS"); do tid="cmp-op-$(printf '%03d' $i)-$$" crname="cmp-cr-$(printf '%03d' $i)-$$" # Применяем CR CR_YAML=$(echo "$OP_CR_TEMPLATE" | sed "s/CRNAME/$crname/g" | sed "s/TENANTID/$tid/g") t_start=$(date +%s%3N) echo "$CR_YAML" | kubectl apply -f - &>/dev/null apply_ok=$? if [[ $apply_ok -ne 0 ]]; then echo " ❌ Тенант #$i ($tid): kubectl apply failed" continue fi # Ждём Ready if op_wait_ready "$crname" "$OP_READY_TIMEOUT"; then t_ms=$(elapsed_ms "$t_start") OP_CREATE_TIMES+=("$t_ms") OP_TOTAL_CREATE_MS=$(( OP_TOTAL_CREATE_MS + t_ms )) printf " ✅ #%-3d %-40s %dms\n" "$i" "$tid" "$t_ms" record "sqs-operator" "tenant_create" "$i" "$t_ms" "$tid" else echo " ⚠️ #$i ($tid): timeout — не стал Ready за ${OP_READY_TIMEOUT}s" record "sqs-operator" "tenant_create_timeout" "$i" "$OP_READY_TIMEOUT\000" "$tid" fi OP_TENANTS+=("$tid") OP_CR_NAMES+=("$crname") done OP_CREATED=${#OP_TENANTS[@]} if [[ $OP_CREATED -gt 0 ]]; then OP_AVG_CREATE=$(( OP_TOTAL_CREATE_MS / OP_CREATED )) # Медиана OP_SORTED_TIMES=($(printf '%s\n' "${OP_CREATE_TIMES[@]}" | sort -n)) OP_MEDIAN_CREATE=${OP_SORTED_TIMES[$(( OP_CREATED / 2 ))]} printf "\n Итого sqs-operator: создано %d/%d avg=%dms median=%dms\n" \ "$OP_CREATED" "$TENANTS" "$OP_AVG_CREATE" "$OP_MEDIAN_CREATE" fi echo "" echo " ── B) shared-sqs ──" echo " (тенант = POST /admin/tenants, всё в памяти одного процесса)" echo "" SS_TOTAL_CREATE_MS=0 SS_CREATE_TIMES=() for i in $(seq 1 "$TENANTS"); do tname="cmp-ss-$(printf '%03d' $i)-$$" t_start=$(date +%s%3N) RESP=$(ss_admin POST /admin/tenants "{\"name\":\"${tname}\",\"max_queues\":50}" 2>&1) t_ms=$(elapsed_ms "$t_start") if echo "$RESP" | grep -q "access_key"; then ak=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['access_key'])" 2>/dev/null || echo "") sk=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['secret_key'])" 2>/dev/null || echo "") tid=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null || echo "") SS_CREATE_TIMES+=("$t_ms") SS_TOTAL_CREATE_MS=$(( SS_TOTAL_CREATE_MS + t_ms )) printf " ✅ #%-3d %-40s %dms (id=%s)\n" "$i" "$tname" "$t_ms" "${tid:0:12}..." record "shared-sqs" "tenant_create" "$i" "$t_ms" "$tname" SS_TENANT_IDS+=("$tid") SS_AK_LIST+=("$ak") SS_SK_LIST+=("$sk") else echo " ❌ #$i ($tname): ошибка: ${RESP:0:100}" record "shared-sqs" "tenant_create_error" "$i" "0" "$tname" fi done SS_CREATED=${#SS_TENANT_IDS[@]} if [[ $SS_CREATED -gt 0 ]]; then SS_AVG_CREATE=$(( SS_TOTAL_CREATE_MS / SS_CREATED )) SS_SORTED_TIMES=($(printf '%s\n' "${SS_CREATE_TIMES[@]}" | sort -n)) SS_MEDIAN_CREATE=${SS_SORTED_TIMES[$(( SS_CREATED / 2 ))]} printf "\n Итого shared-sqs: создано %d/%d avg=%dms median=%dms\n" \ "$SS_CREATED" "$TENANTS" "$SS_AVG_CREATE" "$SS_MEDIAN_CREATE" fi # ════════════════════════════════════════════ # ФАЗА 2 — CRUD очереди (первые 3 тенанта) # ════════════════════════════════════════════ hdr "2. CRUD очереди (send+receive+delete, первые 3 тенанта каждой реализации)" CRUD_LIMIT=3 echo "" echo " ── A) sqs-operator CRUD ──" echo "" OP_CRUD_TIMES=() for idx in $(seq 0 $(( CRUD_LIMIT - 1 ))); do [[ $idx -ge ${#OP_TENANTS[@]} ]] && break tid="${OP_TENANTS[$idx]}" crname="${OP_CR_NAMES[$idx]}" creds=$(op_check_api "$tid") ak=$(echo "$creds" | cut -d: -f1) sk=$(echo "$creds" | cut -d: -f2) ep=$(echo "$creds" | cut -d: -f3-) if [[ -z "$ak" || -z "$sk" ]]; then echo " ⚠️ #$idx skipped — нет credentials (тенант не Ready?)" continue fi qname="crud-q-$$-$idx" t_start=$(date +%s%3N) # CreateQueue R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${ep}/?Action=CreateQueue&QueueName=${qname}&Version=2012-11-05") QURL=$(echo "$R" | grep -oP "(?<=)[^<]+" || echo "") if [[ -z "$QURL" ]]; then echo " ❌ #$idx CreateQueue failed: ${R:0:80}" continue fi # SendMessage curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${ep}/?Action=SendMessage&QueueUrl=${QURL}&MessageBody=compare-test-${idx}&Version=2012-11-05" >/dev/null # ReceiveMessage R=$(curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${ep}/?Action=ReceiveMessage&QueueUrl=${QURL}&MaxNumberOfMessages=1&Version=2012-11-05") RECEIPT=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1 || echo "") # DeleteMessage if [[ -n "$RECEIPT" ]]; then RECEIPT_ENC=$(echo "$RECEIPT" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${ep}/?Action=DeleteMessage&QueueUrl=${QURL}&ReceiptHandle=${RECEIPT_ENC}&Version=2012-11-05" >/dev/null fi # DeleteQueue curl -sk --max-time 15 --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${ep}/?Action=DeleteQueue&QueueUrl=${QURL}&Version=2012-11-05" >/dev/null t_ms=$(elapsed_ms "$t_start") OP_CRUD_TIMES+=("$t_ms") printf " ✅ Tenant #%d %-36s CRUD %dms\n" "$idx" "$tid" "$t_ms" record "sqs-operator" "crud" "$idx" "$t_ms" "$tid" done echo "" echo " ── B) shared-sqs CRUD ──" echo "" SS_CRUD_TIMES=() for idx in $(seq 0 $(( CRUD_LIMIT - 1 ))); do [[ $idx -ge ${#SS_TENANT_IDS[@]} ]] && break tid="${SS_TENANT_IDS[$idx]}" ak="${SS_AK_LIST[$idx]}" sk="${SS_SK_LIST[$idx]}" qname="crud-q-$$-$idx" t_start=$(date +%s%3N) # CreateQueue — shared-sqs требует AWS SigV4, не Basic Auth R=$(curl -sk --max-time 15 \ --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${SS_HOST}/?Action=CreateQueue&QueueName=${qname}&Version=2012-11-05") QURL=$(echo "$R" | grep -oP "(?<=)[^<]+" || echo "") if [[ -z "$QURL" ]]; then echo " ❌ #$idx CreateQueue failed: ${R:0:80}" continue fi # SendMessage curl -sk --max-time 15 \ --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${SS_HOST}/?Action=SendMessage&QueueUrl=${QURL}&MessageBody=compare-test-${idx}&Version=2012-11-05" >/dev/null # ReceiveMessage R=$(curl -sk --max-time 15 \ --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${SS_HOST}/?Action=ReceiveMessage&QueueUrl=${QURL}&MaxNumberOfMessages=1&Version=2012-11-05") RECEIPT=$(echo "$R" | grep -oP "(?<=)[^<]+" | head -1 || echo "") # DeleteMessage if [[ -n "$RECEIPT" ]]; then RECEIPT_ENC=$(echo "$RECEIPT" | python3 -c "import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip()))") curl -sk --max-time 15 \ --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${SS_HOST}/?Action=DeleteMessage&QueueUrl=${QURL}&ReceiptHandle=${RECEIPT_ENC}&Version=2012-11-05" >/dev/null fi # DeleteQueue curl -sk --max-time 15 \ --aws-sigv4 "aws:amz:us-east-1:sqs" --user "$ak:$sk" \ "${SS_HOST}/?Action=DeleteQueue&QueueUrl=${QURL}&Version=2012-11-05" >/dev/null t_ms=$(elapsed_ms "$t_start") SS_CRUD_TIMES+=("$t_ms") printf " ✅ Tenant #%d %-36s CRUD %dms\n" "$idx" "${tid:0:20}..." "$t_ms" record "shared-sqs" "crud" "$idx" "$t_ms" "${tid:0:12}" done # ════════════════════════════════════════════ # ФАЗА 3 — Ресурсы (kubectl top) # ════════════════════════════════════════════ hdr "3. Ресурсы при $TENANTS тенантах (kubectl top)" if [[ "$KUBECTL_OK" == "true" ]]; then echo "" echo " ── A) sqs-operator pods ──" # Controller manager echo " -- Controller manager:" kubectl top pods -n "$OP_CR_NAMESPACE" -l "$OP_MANAGER_LABEL" 2>/dev/null || echo " (kubectl top недоступен — metrics-server?)" # Tenant pods — все ns с prefix sless-fn-cmp- echo " -- Tenant pods (cmp-* namespaces):" for tid in "${OP_TENANTS[@]}"; do ns="${OP_TENANT_NS_PREFIX}-${tid}" kubectl top pods -n "$ns" 2>/dev/null | grep -v "^NAME" | \ awk -v ns="$ns" '{printf " %-50s CPU=%-8s MEM=%s\n", ns"/"$1, $2, $3}' 2>/dev/null || true done # Суммарно: кол-во pods OP_NS_COUNT=$(kubectl get ns | grep -c "sless-fn-cmp-" 2>/dev/null || echo "?") echo " -- Итого namespace под тенанты: $OP_NS_COUNT" echo "" echo " ── B) shared-sqs pod ──" kubectl top pods -n "$SS_NAMESPACE" -l "$SS_DEPLOY_LABEL" 2>/dev/null || \ kubectl top pods -n "$SS_NAMESPACE" 2>/dev/null | grep -i "shared-sqs\|sqs" | \ awk '{printf " %-50s CPU=%-8s MEM=%s\n", $1, $2, $3}' || \ echo " (pod не найден в namespace $SS_NAMESPACE)" echo " -- Тенанты в памяти: $SS_CREATED (0 дополнительных pods)" else echo " kubectl недоступен — пропускаем" fi # ════════════════════════════════════════════ # ФАЗА 4 — Cleanup # ════════════════════════════════════════════ hdr "4. Cleanup" echo "" echo " ── A) sqs-operator ──" OP_DEL_TIMES=() for i in "${!OP_CR_NAMES[@]}"; do crname="${OP_CR_NAMES[$i]}" t_start=$(date +%s%3N) kubectl delete queueservice "$crname" -n "$OP_CR_NAMESPACE" --wait=false &>/dev/null t_ms=$(elapsed_ms "$t_start") OP_DEL_TIMES+=("$t_ms") record "sqs-operator" "tenant_delete" "$i" "$t_ms" "$crname" done echo " Удалено ${#OP_CR_NAMES[@]} CR (без ожидания термин.)" echo "" echo " ── B) shared-sqs ──" SS_DEL_TIMES=() for i in "${!SS_TENANT_IDS[@]}"; do tid="${SS_TENANT_IDS[$i]}" t_start=$(date +%s%3N) ss_admin DELETE "/admin/tenants/$tid" >/dev/null 2>&1 t_ms=$(elapsed_ms "$t_start") SS_DEL_TIMES+=("$t_ms") record "shared-sqs" "tenant_delete" "$i" "$t_ms" "${tid:0:12}" done if [[ ${#SS_DEL_TIMES[@]} -gt 0 ]]; then SS_AVG_DEL=$(python3 -c "t=[${SS_DEL_TIMES[*]}]; print(int(sum(t)/len(t)))" 2>/dev/null || echo "?") echo " Удалено ${#SS_DEL_TIMES[@]} тенантов avg=${SS_AVG_DEL}ms" fi # ════════════════════════════════════════════ # ИТОГОВАЯ ТАБЛИЦА # ════════════════════════════════════════════ hdr "ИТОГ — Сравнительная таблица (N=$TENANTS)" python3 - << PYEOF import csv, sys data = {} # Парсим results file try: with open("$RESULTS_FILE") as f: for row in csv.reader(f): if len(row) < 5: continue impl, phase, n, val_str, label = row try: val = int(val_str) except: continue key = (impl, phase) data.setdefault(key, []).append(val) except FileNotFoundError: print("Нет результатов") sys.exit(0) def stats(vals): if not vals: return "нет данных" vals_s = sorted(vals) n = len(vals_s) avg = int(sum(vals_s) / n) med = vals_s[n // 2] mn = vals_s[0] mx = vals_s[-1] return f"avg={avg}ms med={med}ms min={mn}ms max={mx}ms n={n}" print("") print(f"{'Метрика':<30} {'sqs-operator':<50} {'shared-sqs':<50}") print("-" * 130) phases = [ ("tenant_create", "Создание тенанта"), ("crud", "CRUD очереди (create+send+recv+del)"), ("tenant_delete", "Удаление тенанта"), ] for phase_key, phase_label in phases: op_vals = data.get(("sqs-operator", phase_key), []) ss_vals = data.get(("shared-sqs", phase_key), []) print(f" {phase_label:<28} {stats(op_vals):<50} {stats(ss_vals):<50}") print("") # Вывод победителя по скорости создания op_create = data.get(("sqs-operator", "tenant_create"), []) ss_create = data.get(("shared-sqs", "tenant_create"), []) if op_create and ss_create: op_avg = sum(op_create) / len(op_create) ss_avg = sum(ss_create) / len(ss_create) ratio = op_avg / ss_avg if ss_avg > 0 else float('inf') faster = "shared-sqs" if ss_avg < op_avg else "sqs-operator" print(f" Создание тенанта: {faster} быстрее в {ratio:.1f}x") print(f" sqs-operator: {op_avg:.0f}ms | shared-sqs: {ss_avg:.0f}ms") print("") print(f" Файл подробных данных: $RESULTS_FILE") PYEOF echo "" echo " Готово! $(date '+%Y-%m-%d %H:%M:%S')"