|
| 1 | +#!/bin/bash |
| 2 | +# Kubernetes load test for the recommendation API. |
| 3 | +# |
| 4 | +# Runs two scenarios using Locust and saves CSV results: |
| 5 | +# Scenario 1 — Baseline: 10 users, 60s (measure steady-state latency) |
| 6 | +# Scenario 2 — Stress: 50 users, 120s (trigger HPA, measure degradation) |
| 7 | +# |
| 8 | +# Prerequisites: |
| 9 | +# - Minikube running: minikube start --driver=docker |
| 10 | +# - Mounts active: minikube mount $(pwd)/outputs:/mnt/outputs |
| 11 | +# minikube mount $(pwd)/data/processed:/mnt/data/processed |
| 12 | +# - App deployed: kubectl apply -f k8s/deployment.yaml -f k8s/service.yaml |
| 13 | +# - HPA deployed: kubectl apply -f k8s/hpa.yaml |
| 14 | +# - metrics-server: minikube addons enable metrics-server |
| 15 | +# - locust installed: pip install locust |
| 16 | +# |
| 17 | +# Usage (from project root): |
| 18 | +# bash k8s/load_test.sh |
| 19 | + |
| 20 | +set -e |
| 21 | + |
| 22 | +RESULTS_DIR="k8s/results" |
| 23 | +mkdir -p "$RESULTS_DIR" |
| 24 | + |
| 25 | +# Get Minikube service URL |
| 26 | +# On macOS with Docker driver, `minikube service --url` blocks (it's a tunnel). |
| 27 | +# Accept the URL as an argument or prompt the user to provide it. |
| 28 | +if [ -n "$1" ]; then |
| 29 | + SERVICE_URL="$1" |
| 30 | +else |
| 31 | + echo "Usage: bash k8s/load_test.sh <SERVICE_URL>" |
| 32 | + echo "" |
| 33 | + echo "To get the URL, run this in a separate terminal and leave it open:" |
| 34 | + echo " minikube service recommendation-api --url" |
| 35 | + echo "" |
| 36 | + echo "Then pass the printed URL here, e.g.:" |
| 37 | + echo " bash k8s/load_test.sh http://127.0.0.1:12345" |
| 38 | + exit 1 |
| 39 | +fi |
| 40 | +echo "Service URL: $SERVICE_URL" |
| 41 | +echo "" |
| 42 | + |
| 43 | +# Verify service is healthy before running load tests |
| 44 | +echo "Verifying service health..." |
| 45 | +HEALTH=$(curl -s --max-time 5 "$SERVICE_URL/health" 2>/dev/null || echo "") |
| 46 | +if ! echo "$HEALTH" | grep -q '"model_loaded":true'; then |
| 47 | + echo "ERROR: Service is not healthy. Response: $HEALTH" |
| 48 | + exit 1 |
| 49 | +fi |
| 50 | +echo "Health check passed: $HEALTH" |
| 51 | +echo "" |
| 52 | + |
| 53 | +# ───────────────────────────────────────────────────────────── |
| 54 | +# Scenario 1 — Baseline (10 users, 60s) |
| 55 | +# ───────────────────────────────────────────────────────────── |
| 56 | +echo "========================================" |
| 57 | +echo "Scenario 1: Baseline (10 users, 60s)" |
| 58 | +echo "========================================" |
| 59 | + |
| 60 | +echo "Pod count before:" |
| 61 | +kubectl get pods -l app=recommendation-api --no-headers | wc -l | xargs echo " Pods:" |
| 62 | + |
| 63 | +locust -f k8s/locustfile.py \ |
| 64 | + --headless \ |
| 65 | + --users 10 \ |
| 66 | + --spawn-rate 2 \ |
| 67 | + --run-time 60s \ |
| 68 | + --host "$SERVICE_URL" \ |
| 69 | + --csv "$RESULTS_DIR/baseline" \ |
| 70 | + --only-summary \ |
| 71 | + 2>&1 | tail -20 |
| 72 | + |
| 73 | +echo "" |
| 74 | +echo "Baseline results saved to $RESULTS_DIR/baseline_stats.csv" |
| 75 | +echo "" |
| 76 | + |
| 77 | +# Parse and print key metrics |
| 78 | +python3 - "$RESULTS_DIR/baseline_stats.csv" << 'PYEOF' |
| 79 | +import sys, csv |
| 80 | +with open(sys.argv[1]) as f: |
| 81 | + rows = list(csv.DictReader(f)) |
| 82 | +for row in rows: |
| 83 | + if row.get("Name") == "/recommend": |
| 84 | + p50 = row.get("50%", "N/A") |
| 85 | + p95 = row.get("95%", "N/A") |
| 86 | + rps = row.get("Requests/s", "N/A") |
| 87 | + fail = row.get("Failure Count", "0") |
| 88 | + print(f" /recommend p50={p50}ms p95={p95}ms RPS={rps} failures={fail}") |
| 89 | +PYEOF |
| 90 | + |
| 91 | +# ───────────────────────────────────────────────────────────── |
| 92 | +# Scenario 2 — Stress (50 users, 120s) |
| 93 | +# ───────────────────────────────────────────────────────────── |
| 94 | +echo "" |
| 95 | +echo "========================================" |
| 96 | +echo "Scenario 2: Stress (50 users, 120s)" |
| 97 | +echo "========================================" |
| 98 | + |
| 99 | +echo "Pod count before stress test:" |
| 100 | +kubectl get pods -l app=recommendation-api --no-headers | wc -l | xargs echo " Pods:" |
| 101 | + |
| 102 | +echo "HPA state before stress test:" |
| 103 | +kubectl get hpa recommendation-api --no-headers 2>/dev/null || echo " HPA not deployed" |
| 104 | +echo "" |
| 105 | + |
| 106 | +locust -f k8s/locustfile.py \ |
| 107 | + --headless \ |
| 108 | + --users 50 \ |
| 109 | + --spawn-rate 5 \ |
| 110 | + --run-time 120s \ |
| 111 | + --host "$SERVICE_URL" \ |
| 112 | + --csv "$RESULTS_DIR/stress" \ |
| 113 | + --only-summary \ |
| 114 | + 2>&1 | tail -20 |
| 115 | + |
| 116 | +echo "" |
| 117 | +echo "Stress results saved to $RESULTS_DIR/stress_stats.csv" |
| 118 | +echo "" |
| 119 | + |
| 120 | +# Parse and print key metrics |
| 121 | +python3 - "$RESULTS_DIR/stress_stats.csv" << 'PYEOF' |
| 122 | +import sys, csv |
| 123 | +with open(sys.argv[1]) as f: |
| 124 | + rows = list(csv.DictReader(f)) |
| 125 | +for row in rows: |
| 126 | + if row.get("Name") == "/recommend": |
| 127 | + p50 = row.get("50%", "N/A") |
| 128 | + p95 = row.get("95%", "N/A") |
| 129 | + rps = row.get("Requests/s", "N/A") |
| 130 | + fail = row.get("Failure Count", "0") |
| 131 | + print(f" /recommend p50={p50}ms p95={p95}ms RPS={rps} failures={fail}") |
| 132 | +PYEOF |
| 133 | + |
| 134 | +echo "Pod count after stress test (HPA may have scaled up):" |
| 135 | +kubectl get pods -l app=recommendation-api |
| 136 | +echo "" |
| 137 | +echo "HPA state after stress test:" |
| 138 | +kubectl get hpa recommendation-api 2>/dev/null || echo " HPA not deployed" |
| 139 | + |
| 140 | +echo "" |
| 141 | +echo "========================================" |
| 142 | +echo "Load test complete." |
| 143 | +echo "Results: $RESULTS_DIR/baseline_stats.csv $RESULTS_DIR/stress_stats.csv" |
| 144 | +echo "========================================" |
0 commit comments