Нагрузочное тестирование сайта: ab, wrk, k6 и JMeter — как не положить прод и что измерять
15 мин чтения
Нагрузочное тестирование сайта: ab, wrk, k6 и JMeter — как не положить прод и что измерять

Нагрузочное тестирование сайта: ab, wrk, k6 и JMeter — как не положить прод и что измерять

Перед акцией или релизом хочется знать, сколько запросов в секунду выдержит сайт, прежде чем начнёт отдавать 502 и таймауты. Разбираем виды тестов, почему среднее время ответа врёт, как не попасть под собственный антиддос, чем отличаются ab, wrk, k6, JMeter, Locust и Яндекс.Танк, как читать их вывод и что чинить. Флаги сверены с документацией на сентябрь 2026 года, вывод ab, wrk и k6 получен реальными прогонами.

Нагрузочное тестирование сайта — контролируемая подача на сервер заданного числа запросов в секунду или одновременных пользователей с измерением времени ответа, доли ошибок и потребления ресурсов, чтобы найти предел, после которого сайт деградирует. Результат теста — не одно число «RPS», а кривая: как растут p95-латентность и ошибки с ростом нагрузки и где они срываются. Тест, который меряет только среднее время ответа, предел не покажет.

Чем нагрузочный тест отличается от стресс-, soak- и spike-теста

Разница — в профиле нагрузки и вопросе, на который отвечает тест: нагрузочный (average-load) проверяет ожидаемый трафик, стресс — трафик выше ожидаемого, soak — долгую работу, spike — резкий всплеск, breakpoint — плавный рост до отказа. Классификация — по руководству k6 «Load test types».

Вид теста Нагрузка Длительность Вопрос
Нагрузочный (average-load) Обычный пик продакшена 5–60 минут Держит ли сайт типичный трафик с приемлемой латентностью
Стресс Выше обычной: +50–100 % и больше 5–60 минут Насколько деградирует сайт и выживает ли
Soak (выносливость) Обычная Часы: 3, 8, 12, 24, до 72 Есть ли утечки памяти, рост очередей, переполнение дисков
Spike (всплеск) Очень высокая, без полки Минуты Переживёт ли сайт наплыв после рассылки и восстановится ли
Breakpoint Плавный рост до отказа Сколько потребуется Где предел и что ломается первым

Для первого теста перед акцией достаточно двух: average-load на ожидаемом пике и стресс на удвоенном; soak и spike — следующий шаг.

Какие метрики измерять и почему среднее время ответа врёт

Минимум — четыре метрики одновременно: RPS, латентность по перцентилям (p50, p95, p99), доля ошибок и насыщение ресурсов сервера. Среднее не подходит потому, что распределение времени ответа под нагрузкой несимметрично: тысячи быстрых ответов и десятки зависших дают «приличное» среднее, за которым прячется каждый сотый пользователь с секундным ожиданием.

Пример из прогона wrk ниже: среднее — 29,76 мс, медиана — 3,97 мс, p99 — 943 мс. Среднее в 7,5 раза больше медианы и в 32 раза меньше p99 — не описывает ни типичный, ни худший случай. Поэтому в k6 пороги задают на p(95), а JMeter выводит 90-й, 95-й и 99-й перцентили.

Метрика Где смотреть Признак предела
RPS Requests per second в ab, Requests/sec в wrk, http_reqs в k6 Перестаёт расти при росте нагрузки, затем падает
p50 / p95 / p99 Percentage served в ab, --latency в wrk, p(95) в k6 p95 растёт быстрее p50 — образуется очередь
Доля ошибок Failed / Non-2xx в ab, Socket errors в wrk, http_req_failed в k6 Устойчивый рост выше 0,1–1 %
CPU, память, load average top, vmstat, free -m, journalctl -k CPU около 100 %, LA выше числа ядер, swap, «Killed process»
Соединения и воркеры ss -s, ss -ltn, страница статуса PHP-FPM listen queue больше 0, max children reached

Метрики генератора и сервера снимают одновременно, иначе непонятно, почему вырос p95. Чем занят сервер до первого байта, разобрано в статье про TTFB и способы его снизить.

Как не положить продакшен и не попасть под антиддос

Нагрузочный тест с точки зрения сети неотличим от DDoS-атаки, поэтому первое правило — предупредить всех между вами и сервером: хостинг, CDN, сервис защиты. Второе — тестировать в согласованное окно, начиная с нагрузки, которую сайт заведомо держит, и наращивая её ступенями.

Внимание: тест через CDN или анти-DDoS измеряет не ваш сервер

Cloudflare, DDoS-Guard, Qrator и подобные сервисы считают тысячи однотипных запросов с одного IP атакой: включат капчу или JS-challenge, отбросят соединения — и вы получите цифры защиты, а не сайта. Справочник Cloudflare по симуляции DDoS-атак не требует согласования для собственных зон, но автоматическую митигацию это не отменяет. Практика: бить в origin по IP с заголовком Host, а адрес генератора внести в исключения у защиты и хостера.

Тестировать — с отдельной машины, иначе генератор конкурирует с сайтом за CPU, и с каналом шире трафика: 1 000 RPS по 200 КБ — уже 1,6 Гбит/с. Письма, платежи и запись заказов отключить. Останавливаться, как только ошибки превысили 1 % или p95 ушёл за порог.

Какой инструмент выбрать: ab, wrk, k6, JMeter, Locust или Яндекс.Танк

Для одной страницы хватает ab или wrk; для сценария «зашёл, поискал, положил в корзину» с порогами — k6, JMeter или Locust; для RPS-профилей с автостопом — Яндекс.Танк. Сравнение по документации на сентябрь 2026 года:

Инструмент Сценарии Протокол Распределённость Отчёты Порог входа
ab (ApacheBench 2.3) Нет: один URL, флаги HTTP/1.0, keep-alive только с -k Нет, один процесс Текст с перцентилями Минуты
wrk 4.x / wrk2 Lua-скрипты (-s) HTTP/1.1, keep-alive по умолчанию Нет; многопоточный (-t) Текст, --latency; в wrk2 HdrHistogram и -R Минуты
k6 2.3 (Grafana) JavaScript: stages, checks, thresholds HTTP/1.1, автоматически HTTP/2, WebSocket, gRPC Grafana Cloud k6, k6-operator Консоль, --out json/csv/influxdb Час: бинарник и базовый JS
JMeter 5.6.3 GUI-конструктор, JMX, любые протоколы HTTP/1.1; HTTP/2 через плагин Да: -r / -R, remote_hosts HTML-дашборд (-e -o) с APDEX День: Java 8+, тяжёлый GUI
Locust 2.46 Python-классы пользователей HTTP/1.1; другие протоколы своим клиентом Да: --master / --worker, воркер на ядро Веб-интерфейс на :8089, CSV Час, если знаете Python
Яндекс.Танк YAML и патроны; Phantom, Pandora, JMeter, BFG HTTP/1.x в Phantom; HTTP/2 в Pandora Нет встроенной Консоль, автостоп, выгрузка в Overload Часы: Linux, свои форматы

Главная ловушка ab — протокол. По документации Apache, ab «не реализует HTTP/1.x полностью», в логе сервера его запросы видны как «GET / HTTP/1.0»: без -k каждый запрос открывает новое TCP-соединение, и вы измеряете рукопожатия, а не приложение. Браузеры ходят по HTTP/1.1 и HTTP/2 с keep-alive, поэтому цифры ab без -k хуже реальности.

Как запустить ab и прочитать его вывод

Два обязательных флага — -n (запросов всего) и -c (одновременно); без них ab делает один запрос, который документация называет нерепрезентативным. Ниже — реальный прогон против локального python -m http.server.

ab -n 2000 -c 50 http://127.0.0.1:8080/
# -k keep-alive, -t 30 лимит секунд, -l терпеть разную длину,
# -H "Host: example.com" при тесте origin по IP

Concurrency Level:      50
Time taken for tests:   1.488 seconds
Complete requests:      2000
Failed requests:        0
Requests per second:    1344.10 [#/sec] (mean)
Time per request:       37.200 [ms] (mean)
Time per request:       0.744 [ms] (mean, across all concurrent requests)

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        0    2  39.2      0    1017
Processing:     0    4   2.0      4      25
Waiting:        0    4   1.8      3      24
Total:          1    6  39.4      4    1027

Percentage of the requests served within a certain time (ms)
  50%      4
  90%      6
  95%      8
  99%     12
 100%   1027 (longest request)

Читать нужно три места. «Time per request (mean)» 37,2 мс — столько ждал один виртуальный пользователь; строка «across all concurrent requests» — просто 1/RPS, её путают с латентностью. Перцентили: 99 % ответов уложились в 12 мс, один ждал 1 027 мс. Connect с max 1 017 мс: ровно секунда — начальный таймаут повтора SYN по RFC 6298, сервер не успел принять соединение. Уже на 50 соединениях без keep-alive виден первый предел — очередь accept.

Как запустить wrk и что означают его строки

wrk задаёт нагрузку временем: -t потоков, -c соединений всего, -d длительность, --latency включает перцентили. Он держит соединения открытыми и выдаёт сотни тысяч запросов в секунду с одной машины — начинайте с -c 10.

wrk -t2 -c50 -d10s --latency http://127.0.0.1:8080/

Running 10s test @ http://127.0.0.1:8080/
  2 threads and 50 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    29.76ms  152.86ms   1.67s    96.64%
    Req/Sec     0.87k   454.01     1.71k    59.43%
  Latency Distribution
     50%    3.97ms
     90%    6.06ms
     99%  943.00ms
  15250 requests in 10.01s, 3.39MB read
  Socket errors: connect 0, read 0, write 0, timeout 5
Requests/sec:   1522.74
Transfer/sec:    346.52KB

Stdev 152 мс в пять раз больше среднего: почти все ответы лежат около медианы, а редкие выбросы тянут хвост до 1,67 с. Пять таймаутов в Socket errors — ответы дольше 2 с (таймаут по умолчанию). Req/Sec в Thread Stats — на поток, Requests/sec внизу — суммарный.

Слабость wrk — coordinated omission: следующий запрос уходит только после ответа на предыдущий, в момент зависания сервера wrk перестаёт мерить, и p99 выходит оптимистичнее реальности. Форк wrk2 добавляет -R для постоянного RPS и считает задержку от момента, когда запрос должен был уйти по расписанию: wrk -t2 -c100 -d30s -R2000 — тест «2 000 запросов в секунду», а не «100 соединений».

Как написать сценарий k6 с порогами

k6 описывает тест JavaScript-файлом: stages задаёт профиль виртуальных пользователей, thresholds — критерии успеха, check — проверки ответа. Если порог не выполнен, k6 выходит с кодом 99 — тест в CI падает сам.

// load.js — average-load с заходом в стресс
import http from 'k6/http';
import { check, sleep } from 'k6';

export const options = {
  stages: [
    { duration: '30s', target: 20 },  // разгон до 20 VU
    { duration: '1m',  target: 20 },  // полка: обычный пик
    { duration: '30s', target: 60 },  // втрое выше обычного
    { duration: '20s', target: 0 },   // спад
  ],
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
    http_req_failed:   ['rate<0.01'],
  },
};

export default function () {
  const res = http.get('https://example.com/catalog/');
  check(res, { 'status is 200': (r) => r.status === 200 });
  sleep(1);
}
k6 run load.js
# пример вывода (сокращён)
  █ THRESHOLDS
    http_req_duration
    ✓ 'p(95)<500' p(95)=212.4ms
    ✗ 'p(99)<1000' p(99)=1.42s
    http_req_failed
    ✓ 'rate<0.01' rate=0.31%

  █ TOTAL RESULTS
    ✗ status is 200
      ↳  99% — ✓ 2851 / ✗ 9
    http_req_duration..: avg=148ms min=41ms med=97ms max=3.1s p(95)=212ms
    http_req_failed....: 0.31%  9 out of 2860
    http_reqs..........: 2860   20.4/s
ERRO[0141] thresholds on metrics 'http_req_duration' have been crossed
# код выхода 99

Разбор: p95 в норме, p99 сорвался вместе с девятью не-200 ответами на полке в 60 VU — обычный пик сайт держит, тройной — нет. http_req_duration — это sending + waiting + receiving без DNS и соединения; чистый TTFB — http_req_waiting, TCP и TLS — http_req_connecting и http_req_tls_handshaking. Для теста «сколько RPS», а не «сколько пользователей» есть executor ramping-arrival-rate — его документация k6 рекомендует для breakpoint-теста вместе с abortOnFail.

Как запускать JMeter, Locust и Яндекс.Танк без GUI

JMeter собирают в GUI, а запускают из консоли: руководство пишет прямо — «GUI mode should only be used for creating the test script, CLI mode must be used for load testing». Флаг -n включает консольный режим, -t указывает план, -l пишет результаты, -e -o строят HTML-дашборд с APDEX (пороги 500 и 1 500 мс) и перцентилями 90/95/99.

jmeter -n -t shop.jmx -l results.jtl -e -o report/
# -Jusers=50 -Jduration=600 передают свойства в план,
# -r запускает тест на серверах из remote_hosts
# пример вывода (строки summary сокращены)
Creating summariser <summary>
Created the tree successfully using shop.jmx
Starting standalone test @ 2026-09-25 14:00:00 MSK
summary +   612 in 00:00:30 =  20.4/s Avg: 184 Min: 41 Max: 1902 Err: 0
summary +   615 in 00:00:30 =  20.5/s Avg: 191 Min: 39 Max: 2210 Err: 2
summary =  1227 in 00:01:00 =  20.4/s Avg: 187 Min: 39 Max: 2210 Err: 2

Locust описывает пользователя классом Python (HttpUser, методы с @task) и поднимает веб-интерфейс на порту 8089; для CI есть --headless с -u, -r и -t. Из-за GIL один процесс занимает одно ядро, поэтому документация предписывает --master и по одному --worker на ядро.

Яндекс.Танк задаёт профиль в RPS и умеет останавливаться сам. Схема line(1, 100, 10m) разгоняет с 1 до 100 запросов в секунду за 10 минут, const(100, 5m) держит полку, step(10, 100, 10, 60) поднимает нагрузку по 10 RPS каждые 60 секунд. Секция autostop прерывает тест по http(5xx, 10%, 10s) — 10 % ответов 5xx за 10 секунд — или time(1s, 10s), когда среднее время ответа выше секунды 10 секунд подряд; результаты выгружаются в Overload.

Как читать результат: график RPS против p95 и точка перегиба

Предел сайта — не последняя ступень, на которой он ещё отвечал, а точка перегиба: нагрузка, после которой RPS перестаёт расти, а p95 растёт в разы. Рабочую ёмкость берут на 20–30 % ниже: на перегибе любая мелочь — бэкап, кроны, боты — уводит систему в ошибки.

Условный пример ступенчатого теста сайта на nginx и PHP-FPM с 8 воркерами, ступень — 3 минуты:

Соединений RPS p50 p95 Ошибки Что происходит
10 210 40 мс 70 мс 0 Линейный участок
50 820 55 мс 140 мс 0 Перегиб: p95 удвоился, CPU у PHP-FPM около 90 %
100 910 100 мс 380 мс 0,2 % Насыщение: вдвое больше соединений дали +11 % RPS
200 900 210 мс 1 400 мс 3,1 % Плато, растут 502 и 504: воркеры заняты
400 640 560 мс таймаут 18 % Коллапс: RPS падает, ресурсы уходят на очередь

Цифры проверяет закон Литтла: L = λW, число запросов в системе равно произведению интенсивности на среднее время обслуживания — для любой стабильной системы независимо от распределений. Ступень 50: 50 = 820 × W, W ≈ 61 мс — совпадает с латентностью. Ступень 200: 200 / 900 = 222 мс — совпадает с p50. Следствие: для 900 RPS при p50 в 50 мс в системе должно быть 45 одновременных запросов, а не 200 — остальные стоят в очереди. После перегиба больше соединений даёт не RPS, а очередь.

Что чинить по результатам: keep-alive, кэш, воркеры, лимиты nginx и БД

Тип ошибки указывает на узкое место: 502 — упал или недоступен upstream, 504 — upstream жив, но не успел за таймаут, 503 — лимит соединений или защитный отказ, обрывы без кода — очередь accept или лимит открытых файлов.

Симптом в тесте Узкое место Чем подтвердить Что чинить
RPS низкий, CPU свободен, Connect в ab заметен Нет keep-alive: каждый запрос — новое TCP/TLS-рукопожатие ab с -k и без отличаются в разы keepalive_timeout не 0 (по умолчанию 75s); keepalive к upstream
502 и 504, CPU у PHP/Python 100 % Мало воркеров приложения или запрос слишком тяжёлый В логе FPM «server reached pm.max_children setting» pm.max_children в пределах RAM, OPcache, кэш страниц и фрагментов
Обрывы, «worker_connections are not enough» в error.log Лимит nginx: 512 соединений на воркер, включая upstream ss -s растёт, ошибки в error.log worker_connections 4096, worker_rlimit_nofile, worker_processes auto
p95 растёт, CPU веб-сервера свободен База данных: медленные запросы, нет индексов, мал пул slow query log, SHOW PROCESSLIST, pg_stat_activity Индексы, кэш запросов, пул соединений, реплика на чтение
Секундные выбросы при свободных ресурсах Переполнен accept backlog: у listen в nginx по умолчанию 511 ListenOverflows в nstat -az растёт listen ... backlog=4096 и net.core.somaxconn не меньше
# /etc/nginx/nginx.conf — типичные правки после теста
worker_processes auto;
worker_rlimit_nofile 65535;

events {
    worker_connections 4096;   # по умолчанию 512
}

http {
    keepalive_timeout  75s;    # по умолчанию 75s; 0 отключает
    keepalive_requests 1000;   # по умолчанию 1000, до 1.19.10 — 100

    upstream app {
        server 127.0.0.1:9000;
        keepalive 32;          # кэш idle-соединений на воркер
    }

    server {
        listen 443 ssl backlog=4096;   # по умолчанию 511
        location / {
            proxy_pass http://app;
            proxy_http_version 1.1;    # нужно до nginx 1.29.7
            proxy_set_header Connection "";
        }
    }
}

После каждой правки тест повторяют с тем же профилем — иначе не отличить эффект от шума. Как разбирать 502 по логам nginx, описано в статье об ошибке 502 Bad Gateway; 504 отличается тем, что upstream жив, но не уложился в proxy_read_timeout.

Как связать нагрузочный тест с мониторингом до, во время и после

Генератор видит сайт из одной точки и только пока идёт тест; внешний мониторинг показывает, что видят пользователи из регионов, и продолжает смотреть после релиза. Практика: до теста снять базовую линию времени ответа по фазам (TCP, TLS, TTFB) из нескольких точек, во время теста наблюдать рост TTFB, после — оставить проверку с ожидаемым кодом ответа на реальный пик.

Как это выглядит в Пруфене

Пруфен опрашивает сайт из 20 точек мониторинга — 17 в России, а также Минск, Астана и Испания — и для каждого запуска сохраняет код ответа и время по фазам: во время теста видно, растёт ли TTFB для внешних клиентов и не отдаёт ли сайт 502 в регионах. HTTP-проверка с ожидаемым кодом 200 и интервалом от 2 минут откроет инцидент при росте ошибок после релиза и уведомит в Email, Telegram или Макс; частоту опроса поможет выбрать справка об интервалах проверок. Проверка «Скорость страницы» пригодится как замер до и после оптимизаций: она платная, но 150 ₽ при регистрации хватит на сравнение. Бесплатный план включает 5 проверок — начать можно с главной страницы Пруфена.

Как провести первое нагрузочное тестирование сайта

Порядок рассчитан на один вечер: нужна отдельная машина с ab или k6 и доступ к метрикам сервера.

  1. Определите целевую нагрузку: пиковый час из аналитики переведите в RPS (просмотры в час / 3 600) и умножьте на 2–3 — это цель стресс-теста.
  2. Согласуйте окно и адрес генератора с хостингом и провайдером защиты; тестируйте origin по IP с заголовком Host, на проде — в час минимального трафика.
  3. Снимите базовую линию: прогон на 5 соединениях 60 секунд, зафиксируйте p50, p95, RPS и ошибки.
  4. Откройте на сервере top, ss -s и логи ошибок nginx и приложения; включите внешние проверки времени ответа из нескольких точек.
  5. Наращивайте нагрузку ступенями по 2–3 минуты: 10, 50, 100, 200 соединений или соответствующие RPS в wrk2, k6 или Танке; записывайте RPS, p95 и ошибки на каждой ступени.
  6. Остановитесь, как только ошибки превысили 1 % или p95 вырос втрое от базовой линии: предыдущая ступень — перегиб, рабочая ёмкость на 20–30 % ниже.
  7. Определите узкое место по типу ошибки и метрикам сервера, внесите одну правку (keep-alive, воркеры, кэш, лимиты nginx, индекс) и повторите тот же профиль.

Частые вопросы

Сколько RPS должен выдерживать обычный сайт? Универсального числа нет: nginx отдаёт статику и закэшированные страницы десятками тысяч запросов в секунду, а один PHP-FPM-воркер тяжёлой CMS — единицы. Считайте от своего трафика: пиковый час, делённый на 3 600 и умноженный на 2–3. Для большинства сайтов малого бизнеса это десятки RPS, а не тысячи.

Чем стресс-тест отличается от нагрузочного, если оба подают трафик? Целью. Нагрузочный тест должен пройти: сайт держит ожидаемый пик, пороги по p95 и ошибкам выполнены. Стресс-тест подаёт нагрузку выше ожидаемой и заранее допускает деградацию — вопрос в том, насколько она плавная и восстанавливается ли сайт после снятия нагрузки. По руководству k6 стресс запускают только после успешных smoke- и average-load-тестов.

Почему ab показывает низкий RPS, а wrk на том же сервере — в разы больше? Из-за протокола: ab шлёт HTTP/1.0 и без -k открывает новое соединение на каждый запрос, wrk работает по HTTP/1.1 с постоянными соединениями и несколькими потоками. Разница — стоимость рукопожатий. Сравнивать нужно не инструменты, а ступени одного инструмента с одним профилем до и после правок.

Как понять, что во время теста сработала защита CDN, а не упал сайт? По коду и телу ответа: вместо 502 или 504 приходят 403, 429 или 503 со страницей челленджа, а время ответа становится подозрительно стабильным — защита отвечает быстрее приложения. Повторите запросы к origin по IP: если там всё в порядке, вы измеряли фильтр. Варианты 503 разобраны в статье об ошибке 503.

Была ли статья полезна?

Начните мониторинг прямо сейчас

Бесплатный тариф — до 5 проверок. 150 ₽ на баланс при регистрации.

Создать аккаунт