SQL-запрос который выполняется за 30 мс на 100 строках может завалиться по таймауту на миллионе, а функциональные тесты это не поймают: они проверяют один сценарий за раз. Нагрузочное тестирование симулирует параллельный трафик который обнажает такие проблемы. Нагрузочное тестирование для нормального трафика, стресс-тестирование для поиска точки отказа, спайк-тестирование для внезапных всплесков и выдержка для выявления утечек памяти которые видны только через несколько часов. Гайд охватывает k6: инструмент на JavaScript который запускает нагрузочные тесты из CLI, возвращает ненулевой код при невыполнении порогов и встраивается в GitHub Actions.

Почему нагрузочное тестирование важно

Форма логина которая отвечает за 200 мс: нормально. За 5 секунд пользователь уходит. За 30 секунд под нагрузкой сервер падает.

Производительностные баги часто самые дорогие в исправлении: они требуют архитектурных изменений, не точечных правок. Найти их раньше принципиально важно.

Типичные проблемы производительности

  • SQL-запросы которые работают нормально на 100 строках, падают по таймауту на миллионе
  • API-эндпоинты которые справляются с 10 параллельными пользователями, ломаются на 100
  • Утечки памяти которые накапливаются часами
  • Сторонние интеграции которые становятся узким местом под нагрузкой

Виды нагрузочных тестов

Нагрузочное тестирование

Симулирует ожидаемый продакшн-трафик и проверяет что система укладывается в допустимые пределы.

Вопрос: справляется ли система с нормальным трафиком? Пример: в приложении 500 параллельных пользователей в рабочее время. Запускаем 500 виртуальных пользователей на 30 минут и измеряем время ответа.

Приемлемый результат: 95-й перцентиль времени ответа меньше 1 секунды, процент ошибок меньше 1%, и нет утечек памяти.

Стресс-тестирование

Нагружаем систему сверх лимитов чтобы найти точку отказа.

Вопрос: как система ведёт себя при перегрузке? Падает ли она управляемо? Пример: начинаем со 100 пользователей, добавляем по 100 каждую минуту пока система не сломается. Смотрим когда начинаются ошибки, как система падает и восстанавливается ли она.

Спайк-тестирование

Резкий скачок нагрузки и возврат к норме.

Вопрос: справляется ли система с внезапными всплесками трафика? Пример: нормальная нагрузка 100 пользователей. Резко прыгаем до 1000 на 2 минуты, затем возвращаемся к 100. Типичные причины всплесков: упоминание в соцсетях, статья в СМИ, флеш-продажа.

Выдержка

Продолжительная нагрузка на длительный период.

Вопрос: есть ли утечки памяти или деградация производительности со временем? Пример: 200 параллельных пользователей 8 часов. Мониторим использование памяти и динамику времени ответа.

k6: современный инструмент нагрузочного тестирования

k6: самый QA-friendly инструмент нагрузочного тестирования. JavaScript-синтаксис, запуск из CLI, интеграция с CI.

Установка

# macOS
brew install k6

# Windows (winget)
winget install k6

# Docker
docker pull grafana/k6

Первый нагрузочный тест

// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';

// Конфигурация теста
export const options = {
  vus: 50,           // Виртуальные пользователи
  duration: '30s',  // Длительность 30 секунд
};

export default function() {
  // Запрос
  const response = http.get('https://lab.becomeqa.com/api/products');
  
  // Проверки
  check(response, {
    'status is 200': (r) => r.status === 200,
    'response time < 500ms': (r) => r.timings.duration < 500,
    'has products': (r) => JSON.parse(r.body).length > 0,
  });
  
  // Пауза между итерациями (имитация реального пользователя)
  sleep(1);
}

Запуск:

k6 run load-test.js

Вывод:

✓ status is 200         100.00% ✓ 1500  ✗ 0
✓ response time < 500ms 95.33%  ✓ 1430  ✗ 70
✓ has products          100.00% ✓ 1500  ✗ 0

http_req_duration..............: avg=185ms min=45ms  med=160ms  max=1.2s    p(90)=350ms p(95)=480ms
http_reqs......................: 1500   49.91/s

Сценарии с постепенным наращиванием нагрузки

Реалистичнее чем сразу давать полную нагрузку:

export const options = {
  stages: [
    { duration: '2m', target: 100 },  // Наращиваем до 100 пользователей за 2 минуты
    { duration: '5m', target: 100 },  // Держим 100 пользователей 5 минут
    { duration: '2m', target: 200 },  // Наращиваем до 200
    { duration: '5m', target: 200 },  // Держим 200 пользователей
    { duration: '2m', target: 0 },    // Снижаем нагрузку
  ],
};

Тестирование API с аутентификацией

import http from 'k6/http';
import { check, group } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 50 },
    { duration: '3m', target: 50 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    'http_req_duration': ['p(95)<500'],  // 95% запросов быстрее 500 мс
    'http_req_failed': ['rate<0.01'],     // Менее 1% ошибок
  },
};

export function setup() {
  // Логинимся один раз, токен для всех виртуальных пользователей
  const res = http.post('https://api.myapp.com/auth/login', JSON.stringify({
    email: 'load-test@myapp.com',
    password: 'LoadTestPass1',
  }), { headers: { 'Content-Type': 'application/json' } });
  
  return { token: JSON.parse(res.body).token };
}

export default function(data) {
  const headers = {
    'Authorization': `Bearer ${data.token}`,
    'Content-Type': 'application/json',
  };
  
  group('List products', () => {
    const res = http.get('https://api.myapp.com/products', { headers });
    check(res, { 'products loaded': (r) => r.status === 200 });
  });
  
  group('Get product detail', () => {
    const res = http.get('https://api.myapp.com/products/1', { headers });
    check(res, { 'product detail loaded': (r) => r.status === 200 });
  });
  
  sleep(Math.random() * 3 + 1);  // Случайная пауза 1–4 секунды
}

Пороговые значения

Задаём критерии прохождения/провала:

export const options = {
  thresholds: {
    // 95% запросов должны завершиться быстрее 500 мс
    'http_req_duration': ['p(95)<500'],
    
    // Порог для конкретного эндпоинта
    'http_req_duration{name:products}': ['p(95)<300'],
    
    // Допустимо менее 0.1% ошибок
    'http_req_failed': ['rate<0.001'],
    
    // Порог кастомной метрики
    'checkout_duration': ['p(90)<2000'],
  },
};

Если пороги не выполнены, k6 завершается с ненулевым кодом: CI падает автоматически.

Что измерять

Перцентили времени ответа

  • p50 (медиана): 50% запросов быстрее этого значения
  • p90: 90% запросов быстрее этого значения
  • p95: 95% запросов быстрее этого значения
  • p99: медленный хвост, то что испытывает худший 1%
Процент ошибок: доля запросов которые завершились с ошибкой (5xx, таймауты) Пропускная способность: количество запросов в секунду которые обрабатывает система Параллельные пользователи: сколько пользователей активны одновременно Утилизация ресурсов: CPU, память, соединения с базой данных (измеряется на стороне сервера)

Интеграция с CI

# GitHub Actions
performance-test:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    
    - name: Install k6
      run: |
        curl https://github.com/grafana/k6/releases/download/v0.50.0/k6-v0.50.0-linux-amd64.tar.gz -L | tar xvz
        sudo mv k6-v0.50.0-linux-amd64/k6 /usr/local/bin
    
    - name: Run load test
      run: k6 run load-tests/api-load-test.js
      env:
        BASE_URL: ${{ secrets.STAGING_URL }}
    
    - name: Upload results
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: k6-results
        path: k6-results.json

Типичные проблемы производительности

Медленные SQL-запросы

Отслеживай время выполнения запросов под нагрузкой, проверяй на N+1 запросы (1 запрос на строку вместо одного общего) и ищи отсутствующие индексы.

Утечки памяти

Использование памяти растёт со временем без снижения. Видно в тестах на выдержку.

Исчерпание пула соединений

Соединения с базой данных заканчиваются при высокой параллельности. Типичная ошибка: «too many connections».

Узкие места в сторонних сервисах

Внешний API который тормозит под нагрузкой, или платёжный шлюз с ограничениями частоты запросов.

Отсутствующее кэширование

Одни и те же данные повторно тянутся из БД вместо кэша.

Итоговая таблица

| Тип теста | Цель | Длительность |

|-----------|------|--------------|

| Нагрузочный | Проверка производительности при нормальной нагрузке | 30 мин – 2 ч |

| Стресс | Найти точку отказа | До сбоя |

| Спайк | Обработка внезапных всплесков трафика | Короткие всплески |

| Выдержка | Обнаружение утечек памяти | Часы |

Основы k6

  • vus: виртуальные пользователи
  • duration: длительность запуска
  • stages: паттерны наращивания нагрузки
  • thresholds: критерии прохождения/провала
  • check(): проверки на уровне запроса

Нагрузочное тестирование отличается от функционального: разные инструменты, разные цели. Функциональные тесты проверяют корректность, нагрузочные проверяют скорость и надёжность под нагрузкой. Нужны и те и другие перед релизом.

→ See also: Нагрузочное тестирование с k6: первый нагрузочный тест QA-инженера | Пирамида тестирования: объяснение для QA-инженеров | API-тестирование 101: всё, что нужно знать QA-инженеру в 2026 году