Стресс-тестирование: определение пределов нагрузки
Стресс-тест намеренно нагружает систему за пределы нормальной эксплуатации для нахождения точки отказа. Ответы на вопросы: при каком RPS начинают расти ошибки? Как система восстанавливается после перегрузки? Где узкое место — БД, CPU, память, сеть?
Методология
Шаг 1: Определить baseline. Запустить нормальную нагрузку (50–70% от ожидаемого пика) и зафиксировать метрики: p95 latency, error rate, CPU/memory.
Шаг 2: Ступенчатое увеличение. Повышать нагрузку шагами по 10–20% каждые 2–5 минут. Фиксировать точку, где начинают расти ошибки или latency.
Шаг 3: Найти breaking point. Продолжать до деградации (error rate > 5% или latency > 5x baseline).
Шаг 4: Восстановление. Снять нагрузку и наблюдать, как быстро система возвращается в норму.
k6 сценарий стресс-теста
// tests/stress/breaking-point.js
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate, Trend, Counter } from 'k6/metrics'
const errorRate = new Rate('errors')
const requestsPerSecond = new Counter('requests_per_second')
export const options = {
stages: [
// Разогрев до нормального трафика
{ duration: '2m', target: 50 },
{ duration: '3m', target: 50 }, // базовый уровень
// Ступенчатое нарастание
{ duration: '2m', target: 100 },
{ duration: '3m', target: 100 },
{ duration: '2m', target: 200 },
{ duration: '3m', target: 200 },
{ duration: '2m', target: 400 },
{ duration: '3m', target: 400 },
{ duration: '2m', target: 800 },
{ duration: '3m', target: 800 },
{ duration: '2m', target: 1600 },
{ duration: '3m', target: 1600 },
// Остывание и наблюдение за восстановлением
{ duration: '5m', target: 50 },
{ duration: '3m', target: 0 },
],
// Не прерывать тест при превышении порогов — нужно видеть полную картину
thresholds: {
http_req_duration: [
{ threshold: 'p(95)<2000', abortOnFail: false },
],
errors: [
{ threshold: 'rate<0.1', abortOnFail: false }
]
}
}
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
export default function() {
const responses = http.batch([
['GET', `${BASE_URL}/api/products?limit=20`],
['GET', `${BASE_URL}/api/categories`],
])
responses.forEach(r => {
check(r, { 'status 2xx': (r) => r.status >= 200 && r.status < 300 })
errorRate.add(r.status >= 400)
})
requestsPerSecond.add(2)
sleep(0.1)
}
export function handleSummary(data) {
// Найти точку деградации из собранных данных
const stages = analyzeStages(data)
return {
'stress-results.json': JSON.stringify(data, null, 2),
stdout: generateReport(stages)
}
}
function generateReport(stages) {
return `
=== STRESS TEST REPORT ===
Breaking Point Analysis:
${stages.map(s => ` VUs: ${s.vus} | p95: ${s.p95}ms | Errors: ${(s.errorRate*100).toFixed(1)}%`).join('\n')}
`
}
Мониторинг во время теста
Запустить параллельно сбор системных метрик:
#!/bin/bash
# scripts/monitor-stress-test.sh
TARGET_HOST="app-server-ip"
INTERVAL=10 # секунды
while true; do
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# CPU, Memory, Load Average
ssh $TARGET_HOST "
echo -n '$TIMESTAMP '
echo -n 'cpu:'; top -bn1 | grep 'Cpu(s)' | awk '{print \$2}'; echo -n ' '
echo -n 'mem:'; free | grep Mem | awk '{print \$3/\$2 * 100}'; echo -n ' '
echo -n 'load:'; cat /proc/loadavg | awk '{print \$1}'
echo -n 'conns:'; ss -s | grep -o 'estab [0-9]*' | awk '{print \$2}'
"
# PostgreSQL: активные запросы и блокировки
ssh $TARGET_HOST "
PGPASSWORD=pass psql -U app -d appdb -t -c \"
SELECT 'active_queries:', count(*) FROM pg_stat_activity
WHERE state = 'active' AND query NOT LIKE '%pg_stat%';
SELECT 'long_queries:', count(*) FROM pg_stat_activity
WHERE state = 'active' AND query_start < NOW() - interval '5 seconds';
SELECT 'locks:', count(*) FROM pg_locks WHERE NOT granted;
\"
"
sleep $INTERVAL
done | tee stress-monitor.log
Анализ результатов Prometheus + Grafana
# k6 с Prometheus Remote Write
k6 run \
-o experimental-prometheus-rw \
--env K6_PROMETHEUS_RW_SERVER_URL=http://prometheus:9090/api/v1/write \
--env K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM=true \
tests/stress/breaking-point.js
# Запросы в Grafana для анализа стресс-теста
# RPS в реальном времени
rate(k6_http_reqs_total[30s])
# Error rate по времени (найти момент деградации)
rate(k6_http_req_failed_total[30s]) / rate(k6_http_reqs_total[30s])
# p95 latency в реальном времени
histogram_quantile(0.95, rate(k6_http_req_duration_seconds_bucket[30s]))
# Корреляция: нагрузка vs latency vs errors
Идентификация узкого места
# analyze_stress_results.py
import json
import pandas as pd
def analyze_breaking_point(results_file):
with open(results_file) as f:
data = json.load(f)
# Извлечь временные ряды
metrics = data['metrics']
analysis = {
'max_rps_before_errors': find_max_sustainable_rps(metrics),
'error_threshold_rps': find_error_threshold(metrics),
'latency_degradation_point': find_latency_degradation(metrics),
'recovery_time_seconds': find_recovery_time(metrics),
}
print("=== Breaking Point Analysis ===")
print(f"Max sustainable RPS (< 1% errors): {analysis['max_rps_before_errors']}")
print(f"Error threshold RPS: {analysis['error_threshold_rps']}")
print(f"p95 > 1s at RPS: {analysis['latency_degradation_point']}")
print(f"Recovery time after load removal: {analysis['recovery_time_seconds']}s")
# Рекомендации
if analysis['max_rps_before_errors'] < 100:
print("\n[!] LOW capacity. Consider: DB connection pooling, caching, horizontal scaling")
elif analysis['recovery_time_seconds'] > 120:
print("\n[!] SLOW recovery. Consider: circuit breakers, graceful degradation")
return analysis
Типичные узкие места и диагностика
| Симптом | Вероятная причина | Диагностика |
|---|---|---|
| Latency растёт, CPU низкий | Блокировки БД или медленные запросы | pg_stat_activity, slow query log |
| CPU 100%, мало ошибок | Вычислительный bottleneck | top, профилировщик приложения |
ENOMEM ошибки |
Утечка памяти или OOM | free -m, /proc/meminfo |
| Connection refused | Исчерпан pool соединений | pgBouncer stats, netstat |
| 502 Bad Gateway | Worker processes перегружены | Nginx error log, worker_processes |
Срок выполнения
Стресс-тест с ступенчатым профилем нагрузки, мониторингом и анализом breaking point — 2–3 рабочих дня.







