Files
sless/test_harbor_load.sh
T

190 lines
7.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# 2026-04-09
# Нагрузочный тест Harbor: параллельные curl-запросы, замер латентности,
# фиксация зависаний по таймауту. Позволяет воспроизвести и измерить
# intermittent-проблему с HTTP-протоколом на конкретном хосте.
set -uo pipefail
# ── Конфигурация ─────────────────────────────────────────────────────────────
SECRETS_FILE="${SECRETS_FILE:-secrets/pearlharbor_registry.txt}"
DURATION=${DURATION:-60} # секунд
CONCURRENCY=${CONCURRENCY:-10} # параллельных воркеров
TIMEOUT=${TIMEOUT:-8} # таймаут одного запроса (сек)
LOGDIR="${LOGDIR:-/tmp/harbor_load_$(date +%Y%m%d_%H%M%S)}"
# ── Читаем секреты ────────────────────────────────────────────────────────────
if [ ! -f "$SECRETS_FILE" ]; then
echo "ERROR: secrets file not found: $SECRETS_FILE" >&2
exit 1
fi
connection_url=$(grep -E '^connection_url=' "$SECRETS_FILE" | cut -d'=' -f2-)
admin_pass=$(grep -E '^admin_pass=' "$SECRETS_FILE" | cut -d'=' -f2-)
REGISTRY=$(echo "$connection_url" | sed -E 's~https?://~~' | sed -E 's~/$~~')
BASE_URL="https://$REGISTRY"
# ── Точки нагрузки (URL + принудительный протокол) ───────────────────────────
# Цель: нагрузить Harbor разными типами запросов и обоими протоколами,
# чтобы поймать и зафиксировать intermittent-зависания.
declare -a TARGETS=(
"h2 $BASE_URL/api/v2.0/ping"
"h1 $BASE_URL/api/v2.0/ping"
"h2 $BASE_URL/api/v2.0/projects"
"h1 $BASE_URL/api/v2.0/projects"
"h2 $BASE_URL/v2/"
"h1 $BASE_URL/v2/"
)
mkdir -p "$LOGDIR"
RESULT_LOG="$LOGDIR/results.csv"
echo "ts,worker,proto,url,http_code,time_total,timeout" > "$RESULT_LOG"
echo "=== Harbor Load Test ==="
echo "Registry : $BASE_URL"
echo "Duration : ${DURATION}s"
echo "Workers : $CONCURRENCY"
echo "Timeout : ${TIMEOUT}s per request"
echo "Log : $LOGDIR"
echo ""
END_TIME=$(( $(date +%s) + DURATION ))
# ── Один воркер ──────────────────────────────────────────────────────────────
harbor_worker() {
local wid="$1"
local target_idx=$(( wid % ${#TARGETS[@]} ))
local entry="${TARGETS[$target_idx]}"
local proto=$(echo "$entry" | awk '{print $1}')
local url=$(echo "$entry" | awk '{print $2}')
local proto_flag=""
if [ "$proto" = "h1" ]; then
proto_flag="--http1.1"
else
proto_flag="--http2"
fi
while [ "$(date +%s)" -lt "$END_TIME" ]; do
local ts
ts=$(date +%s%3N)
# Замеряем время ответа и HTTP-код
local result
result=$(curl -sk $proto_flag \
--max-time "$TIMEOUT" \
-u "admin:$admin_pass" \
-o /dev/null \
-w "%{http_code} %{time_total}" \
"$url" 2>/dev/null) || result="000 $TIMEOUT"
local http_code time_total timed_out
http_code=$(echo "$result" | awk '{print $1}')
time_total=$(echo "$result" | awk '{print $2}')
# Считаем тайм-аут если http_code=000 или время близко к лимиту
if [ "$http_code" = "000" ]; then
timed_out=1
else
timed_out=0
fi
echo "$ts,$wid,$proto,$url,$http_code,$time_total,$timed_out" >> "$RESULT_LOG"
done
}
export -f harbor_worker
export END_TIME TIMEOUT LOGDIR RESULT_LOG admin_pass
export -a TARGETS
# ── Запускаем воркеров параллельно ───────────────────────────────────────────
echo "Starting $CONCURRENCY workers for ${DURATION}s..."
PIDS=()
for i in $(seq 0 $(( CONCURRENCY - 1 ))); do
harbor_worker "$i" &
PIDS+=($!)
done
# ── Прогресс каждые 5 секунд ─────────────────────────────────────────────────
while [ "$(date +%s)" -lt "$END_TIME" ]; do
sleep 5
local_count=$(wc -l < "$RESULT_LOG")
local_timeouts=$(grep -c ",1$" "$RESULT_LOG" 2>/dev/null || echo 0)
elapsed=$(( DURATION - ( END_TIME - $(date +%s) ) ))
echo "[+${elapsed}s] requests: $(( local_count - 1 )), timeouts: $local_timeouts"
done
# ── Ждём завершения воркеров ─────────────────────────────────────────────────
for pid in "${PIDS[@]}"; do
wait "$pid" 2>/dev/null || true
done
echo ""
echo "=== Results ==="
# ── Статистика через python3 ─────────────────────────────────────────────────
python3 - "$RESULT_LOG" << 'PYEOF'
import sys, csv
from collections import defaultdict
log_file = sys.argv[1]
rows = []
with open(log_file) as f:
reader = csv.DictReader(f)
for r in reader:
rows.append(r)
total = len(rows)
timeouts = sum(1 for r in rows if r['timeout'] == '1')
success = sum(1 for r in rows if r['timeout'] == '0' and r['http_code'].startswith('2'))
times = [float(r['time_total']) for r in rows if r['timeout'] == '0']
times.sort()
def pct(lst, p):
if not lst: return 0
idx = int(len(lst) * p / 100)
return lst[min(idx, len(lst)-1)]
print(f"Total requests : {total}")
print(f"Success (2xx) : {success} ({100*success//total if total else 0}%)")
print(f"Timeouts : {timeouts} ({100*timeouts//total if total else 0}%)")
other = total - success - timeouts
print(f"Other errors : {other}")
if times:
print(f"Latency (ok) : min={min(times):.3f}s median={pct(times,50):.3f}s p95={pct(times,95):.3f}s max={max(times):.3f}s")
print()
print("--- By protocol ---")
by_proto = defaultdict(lambda: {'ok':0,'fail':0,'times':[]})
for r in rows:
p = r['proto']
if r['timeout'] == '1':
by_proto[p]['fail'] += 1
elif r['http_code'].startswith('2'):
by_proto[p]['ok'] += 1
by_proto[p]['times'].append(float(r['time_total']))
else:
by_proto[p]['fail'] += 1
for proto, d in sorted(by_proto.items()):
t = sorted(d['times'])
p95 = pct(t, 95) if t else 0
print(f" {proto}: ok={d['ok']} fail={d['fail']} p95={p95:.3f}s")
print()
print("--- By URL ---")
by_url = defaultdict(lambda: {'ok':0,'to':0})
for r in rows:
u = r['url']
if r['timeout'] == '1':
by_url[u]['to'] += 1
elif r['http_code'].startswith('2'):
by_url[u]['ok'] += 1
# else: counted as non-2xx
for url, d in sorted(by_url.items(), key=lambda x: x[0]):
print(f" {url}: ok={d['ok']} timeout={d['to']}")
PYEOF
echo ""
echo "Raw log: $RESULT_LOG"