Нагрузочное тестирование с k6
Узнайте, как ваша система ведёт себя под трафиком, раньше пользователей.
За 30 секунд
Нагрузочное тестирование отправляет в систему реалистичный трафик, чтобы измерить скорость и найти её пределы. k6 — open-source инструмент нагрузочного тестирования от Grafana Labs: вы пишете тесты на JavaScript или TypeScript, запускаете их из командной строки и задаёте пороги прохождения, например «95% запросов быстрее 400 ms». Актуальная мажорная линейка — k6 2.x, и она естественно встраивается в CI-пайплайны.
Не в моём ежедневном стеке — я понимаю принципы и изучу детали, когда это понадобится проекту.
Почему это важно
- Функциональные тесты доказывают, что код работает для одного пользователя. Нагрузочные тесты доказывают, что он работает и для тысячи одновременно, — именно там проявляются пулы соединений, блокировки и медленные запросы.
- Они превращают планирование мощностей в цифры: сколько запросов в секунду выдерживает один сервер и где следующее узкое место.
- Команды запускают их перед запусками, кампаниями и крупными рефакторингами, чтобы распродажа или маркетинговая рассылка не превратились в аварию.
- Они дополняют unit- и интеграционные тесты (PHPUnit, Jest): те ловят неправильное поведение, а нагрузочные — медленное и хрупкое.
Как это работает
01Виды тестов производительности
Термин «нагрузочный тест» используют вольно, но каждый вид отвечает на свой вопрос. Smoke-тест даёт крошечную нагрузку, чтобы проверить, что скрипт и система работают. Тест средней нагрузки имитирует обычный трафик и подтверждает, что вы укладываетесь в целевые показатели. Стресс-тест выходит за пределы нормы, чтобы увидеть, как деградирует система. Spike-тест резко подскакивает, как после push-уведомления. Soak-тест (тест на выносливость) держит обычную нагрузку часами, чтобы найти утечки памяти и медленное истощение ресурсов. Breakpoint-тест наращивает нагрузку, пока что-нибудь не сломается, чтобы найти реальную ёмкость. Начните со smoke и средней нагрузки; остальные добавляйте, когда узнаете свою базовую линию.
- Пропускная способность: число запросов или итераций в секунду, которые выполняет система.
- Перцентили задержки: p95 означает, что 95% запросов были быстрее этого значения.
- Доля ошибок: процент неудачных запросов, часто первое, что ломается.
02Ваш первый скрипт k6
Скрипт k6 — это ES-модуль. Экспорт options описывает нагрузку и правила прохождения; функция по умолчанию — то, что каждый виртуальный пользователь (VU) выполняет в цикле. VU — это симулированный пользователь, который выполняет вашу функцию параллельно с остальными. check() проверяет ответы, не останавливая тест; thresholds превращают метрики в результат «прошёл/не прошёл», и k6 завершается с ненулевым кодом, если порог не пройден, — именно это ломает сборку в CI. sleep() добавляет время на размышление, чтобы VU вёл себя как реальный человек, а не как плотный цикл. k6 запускает файлы TypeScript напрямую, так что в больших наборах тестов можно использовать типы.
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
vus: 20,
duration: "2m",
thresholds: {
http_req_failed: ["rate<0.01"], // under 1% errors
http_req_duration: ["p(95)<400", "p(99)<1000"], // ms
},
};
export default function () {
const res = http.get("https://staging.example.com/api/products?page=1");
check(res, { "status is 200": (r) => r.status === 200 });
sleep(1);
}
03Сценарии и executors: открытая и закрытая модели
Сценарии позволяют точно формировать нагрузку и запускать несколько паттернов одновременно. Executors определяют, как именно. Executors закрытой модели (constant-vus, ramping-vus) держат занятым заданное число VU; если система замедляется, каждый VU делает меньше запросов, и это скрывает проблему. Executors открытой модели (constant-arrival-rate, ramping-arrival-rate) запускают новые итерации с фиксированной частотой независимо от времени ответа, что соответствует реальному интернет-трафику: пользователи продолжают приходить, даже когда сайт тормозит. Для тестов ёмкости API предпочитайте arrival-rate executors и давайте им достаточно preAllocatedVUs и maxVUs, чтобы выдерживать заданную частоту.
export const options = {
scenarios: {
browse: {
executor: "ramping-arrival-rate",
startRate: 10, timeUnit: "1s",
preAllocatedVUs: 50, maxVUs: 300,
stages: [
{ target: 100, duration: "3m" }, // ramp to 100 requests/s
{ target: 100, duration: "10m" }, // hold
{ target: 0, duration: "2m" }, // ramp down
],
},
},
thresholds: { "http_req_duration{scenario:browse}": ["p(95)<500"] },
};
04Реалистичные пользовательские сценарии
Реальные пользователи логинятся, просматривают, ищут и оформляют заказ, делая паузы между шагами. Моделируйте основные сценарии в реальных пропорциях из аналитики (например, 80% просмотр, 15% поиск, 5% checkout). Используйте group() для именования шагов, теги для разбивки метрик и SharedArray, чтобы загрузить тестовые данные один раз и разделить их между VU без копирования памяти. Там, где есть запись, генерируйте уникальные данные для каждого VU и итерации, чтобы не попадать в одну и ту же строку и не создавать искусственную конкуренцию за блокировки. Подготовку (создание тестового аккаунта, получение токена) делайте в setup(), который выполняется один раз, и передавайте результат в VU.
import http from "k6/http";
import { group, check } from "k6";
import { SharedArray } from "k6/data";
const products = new SharedArray("products", () => JSON.parse(open("./products.json")));
const BASE = __ENV.BASE_URL || "https://staging.example.com";
export function setup() {
const res = http.post(BASE + "/api/login", JSON.stringify({ email: "[email protected]", password: __ENV.TEST_PASSWORD }),
{ headers: { "Content-Type": "application/json" } });
return { token: res.json("token") };
}
export default function (data) {
const params = { headers: { Authorization: "Bearer " + data.token } };
group("product page", () => {
const p = products[Math.floor(Math.random() * products.length)];
check(http.get(BASE + "/api/products/" + p.slug, params), { "200": (r) => r.status === 200 });
});
}
05Запуск, чтение результатов и CI
Запускайте k6 run script.js локально или в контейнере. В конце k6 выводит сводку: частоту запросов, перцентили задержки, долю ошибок, объём переданных данных и то, пройден ли каждый порог. Смотрите на p95 и p99, а не на средние значения, потому что среднее скрывает медленный хвост, который замечают пользователи. Для трендов и корреляции с серверными метриками стримьте результаты в Prometheus (remote write) или Grafana Cloud k6 и смотрите их рядом с дашбордами CPU, базы данных и очередей. В CI запускайте короткий smoke-тест или тест средней нагрузки на каждый релиз против staging, а более длинные стресс- и soak-тесты держите по расписанию. Никогда не нагружайте продакшен или сторонний API без явного разрешения и плана.
# Local run with an environment variable
k6 run -e BASE_URL=https://staging.example.com script.js
# Docker, no local install
docker run --rm -i grafana/k6 run - <script.js
# Stream metrics to Prometheus remote write
K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9090/api/v1/write \
k6 run -o experimental-prometheus-rw script.js
Когда применять
- Перед запусками, распродажами и кампаниями, чтобы убедиться, что система укладывается в целевые показатели задержки и ошибок.
- После крупных изменений (новая версия фреймворка, миграция базы данных, слой кэширования), чтобы поймать регрессии производительности.
- Для планирования мощностей: выбора размеров инстансов, порогов автоскейлинга и лимитов пула соединений.
- Чтобы воспроизвести и исправить замедления продакшена в безопасном окружении.
Когда не стоит
- Как замена профилированию одного медленного запроса; сначала используйте APM-трейсы или профайлер.
- Против продакшена или сторонних сервисов без разрешения, временного окна и плана отката.
- На staging-окружении, настолько отличающемся от продакшена, что цифры ничего не значат.
Альтернативы
- Apache JMeter
Зрелый Java-инструмент с GUI и множеством протоколов; тяжелее, тест-планы на XML.
- Gatling
Тесты в коде на Java, Kotlin или Scala (и JavaScript), эффективный движок и хорошие отчёты.
- Locust
На Python, поведение пользователей описывается классами Python; удобно для Python-команд.
- Artillery
Тесты на YAML и JavaScript, на базе Node.js, с интеграцией Playwright для браузерной нагрузки.
Лучшие практики
- Сначала определите критерии успеха: целевую пропускную способность, задержки p95 и p99 и долю ошибок, записанные как thresholds.
- Используйте arrival-rate executors для тестов ёмкости, чтобы медленные ответы не скрывали проблемы.
- Моделируйте реальные сценарии и структуру трафика по аналитике, с временем на размышление и разнообразными тестовыми данными.
- Тестируйте в окружении, которое повторяет продакшен по размеру, конфигурации и объёму данных.
- Мониторьте тестируемую систему (CPU, память, базу данных, очереди) во время каждого прогона, а не только вывод k6.
- Запускайте короткий smoke-тест в CI на каждый релиз, а более тяжёлые тесты — по расписанию.
- Убедитесь, что сам генератор нагрузки не является узким местом: следите за его CPU и сетью.
Частые ошибки
- Смотреть на среднее время ответа вместо p95 и p99, что скрывает медленный опыт пользователей.
- Долбить один URL без времени на размышление и без разнообразия данных, так что кэши делают всё быстрым на вид.
- Нагрузочное тестирование с ноутбука через домашний Wi-Fi, когда измеряется сеть, а не сервер.
- Забыть очистить тестовые данные и оставить тысячи фейковых заказов или пользователей в общем окружении.
- Запуск против продакшена без предупреждения, который вызывает алерты, срабатывание rate limit или расходы.
Глубже
06Закон Литтла и расчёт размера теста
Закон Литтла связывает три числа: параллелизм = пропускная способность × время в системе. Если нужно 200 запросов в секунду, а запрос занимает 0,25 секунды, в системе одновременно находится около 50 запросов. Для arrival-rate тестов k6 нужное число VU ≈ частота × длительность итерации (включая sleep), поэтому сценарий длиной 2 секунды при 50 итерациях в секунду требует около 100 VU; задайте maxVUs с запасом. Тот же закон объясняет пределы сервера: пул PHP-FPM из 40 воркеров при запросах по 200 ms упирается примерно в 200 запросов в секунду, и всё, что выше, встаёт в очередь. Если задержка резко растёт, а пропускная способность выходит на плато, вы где-то упёрлись в лимит параллелизма: пул воркеров, соединения с базой данных или блокировку.
07Поиск узкого места во время теста
k6 говорит вам, что что-то медленное; серверные метрики говорят, почему. Следите по порядку: CPU приложения и насыщение воркеров, CPU базы данных, slow query log и соединения, cache hit ratio, глубина очередей и задержка внешних API. Применяйте метод USE (utilisation, saturation, errors) к каждому ресурсу. Тегируйте запросы в k6 по имени эндпоинта, чтобы URL с ID группировались (используйте тег name), и сопоставляйте самые медленные эндпоинты с трейсами в APM. Исправляйте по одному узкому месту за раз и перезапускайте тот же тест; следующее узкое место обычно проявляется сразу. Ведите журнал результатов с коммитом, конфигурацией и ключевыми цифрами для каждого прогона, чтобы сравнивать честно.
// Group dynamic URLs under one metric name
http.get(BASE + "/api/products/" + id, { tags: { name: "GET /api/products/:id" } });
// Custom metric for a business step
import { Trend } from "k6/metrics";
const checkoutTime = new Trend("checkout_duration", true);
checkoutTime.add(res.timings.duration);
08Coordinated omission и пределы генератора
Coordinated omission возникает, когда генератор нагрузки ждёт медленных ответов перед отправкой следующего запроса, поэтому отправляет меньше запросов именно тогда, когда системе тяжело, и измеренная задержка выглядит лучше реальной. Executors закрытой модели страдают от этого по своей природе. Arrival-rate executors избегают большей части проблемы, запуская итерации по расписанию, но если у них заканчиваются VU, k6 сообщает dropped_iterations, и это нужно считать сигналом провала. Также следите за генератором: если его CPU выше примерно 80%, тайминги становятся ненадёжными. Для больших тестов распределяйте нагрузку на несколько машин с помощью k6 Operator в Kubernetes или Grafana Cloud k6 и генерируйте нагрузку из того же региона, где находятся пользователи.
09Тестирование за пределами HTTP: WebSockets, gRPC и расширения
В k6 есть встроенные модули для HTTP/1.1 и HTTP/2, WebSockets, gRPC и браузера, а расширения (собираются через xk6, а в новых версиях официальные подключаются автоматически) добавляют протоколы вроде Kafka, SQL или Redis. Это позволяет тестировать части системы, до которых HTTP не дотягивается: WebSocket-чат с тысячами соединений, gRPC service mesh или пропускную способность консьюмера очереди. Относитесь к этим тестам так же, как к HTTP: реалистичная частота сообщений, пороги на задержку и ошибки, мониторинг на стороне сервера. Для событийных систем измеряйте сквозное время от публикации до эффекта, а не только вызов публикации.
Реальные ситуации
Подготовка к распродаже Black Friday
Проблема
Интернет-магазин ожидает на распродаже в десять раз больше обычного трафика. Команда не знает, выдержат ли текущие серверы, база данных и платёжный флоу.
Как решить
Возьмите из аналитики прошлогодний пик и структуру трафика, умножьте на ожидаемый коэффициент и определите пороги: p95 ниже 800 ms для страниц товаров, ниже 1,5 s для checkout, ошибки ниже 1%. Напишите сценарии просмотра, поиска, корзины и checkout с executor ramping-arrival-rate и заглушкой песочницы платёжного провайдера. Запускайте против staging-окружения продакшен-размера с копией анонимизированных данных. Исправьте первое узкое место (часто это запросы к базе данных или пулы соединений), перезапустите и повторяйте, пока цели не будут достигнуты с запасом 30%. Завершите spike-тестом и двухчасовым soak-тестом и согласуйте план масштабирования и отката.
Регрессия задержки после обновления
Проблема
После обновления фреймворка и PHP пользователи жалуются, что API стал медленнее, но в логах ошибок ничего очевидного нет.
Как решить
Напишите повторяемый тест средней нагрузки для десяти самых нагруженных эндпоинтов с фиксированными данными и постоянной частотой прихода. Запустите его против предыдущего и нового релиза на идентичной инфраструктуре по три раза и сравните p95 по каждому эндпоинту с помощью тегов name. Посмотрите на эндпоинты с наибольшим изменением и примените к ним APM-трейсы или профайлер. Частые причины — отключённый OPcache или кэширование, N+1-запросы, появившиеся из-за зависимости, или изменённые настройки соединений. После исправления добавьте в CI smoke-тест с порогами, чтобы следующая регрессия роняла пайплайн, а не доходила до пользователей.
Нагрузочный тест показывает отличные цифры, а продакшен — нет
Проблема
k6 показывает p95 80 ms при 500 запросах в секунду, но продакшен замедляется уже при 200 запросах в секунду.
Как решить
Скорее всего, тест нереалистичен. Проверьте, не бьют ли все VU в одни и те же несколько URL, которые остаются горячими в кэше, и не используют ли один аккаунт. Добавьте разнообразные тестовые данные из SharedArray, реальные пропорции трафика и аутентифицированные флоу. Сравните staging и продакшен: размер базы данных, индексы, конфигурацию кэша, CDN и число воркеров. Используйте arrival-rate executor и проверьте dropped_iterations. Включите фоновую нагрузку, которая есть в продакшене, например cron-задачи и воркеры очередей. Затем сравните задержку теста по эндпоинтам с продакшен-метриками: когда они совпадают при низкой нагрузке, модели можно доверять и при более высокой.
Шпаргалка
- Запустить тест
k6 run script.js- Переопределить VU и длительность
k6 run --vus 50 --duration 5m script.js- Передать переменную окружения
k6 run -e BASE_URL=https://staging.example.com script.js- Порог доли ошибок
http_req_failed: ["rate<0.01"]- Порог задержки
http_req_duration: ["p(95)<400", "p(99)<1000"]- Досрочная остановка при провале
{ threshold: "rate<0.05", abortOnFail: true }- Группировать динамические URL
http.get(url, { tags: { name: "GET /api/items/:id" } })- Экспорт сводки в JSON
k6 run --summary-export=summary.json script.js- Запуск в Docker
docker run --rm -i grafana/k6 run - <script.js
Вопросы и ответы
Q1Чем отличаются нагрузочное, стресс- и soak-тестирование?
Нагрузочный тест проверяет систему при ожидаемом трафике, например в обычный день или в запланированный пик, чтобы подтвердить, что она укладывается в целевые показатели задержки и ошибок. Стресс-тест выходит за пределы ожидаемого трафика, чтобы увидеть, как и где система деградирует и восстанавливается ли она, когда нагрузка спадает. Soak-тест держит обычную нагрузку часами, чтобы выявить медленные проблемы: утечки памяти, утечки соединений, растущие очереди или заполняющийся диск. Spike-тесты добавляют резкий скачок, а breakpoint-тесты наращивают нагрузку до отказа, чтобы найти максимальную ёмкость. Каждый отвечает на свой вопрос, поэтому я выбираю в зависимости от риска, который хочу снизить.
Q2Почему смотреть на p95 и p99, а не на среднее?
Среднее скрывает хвост. Если 90 запросов занимают 100 ms, а 10 — 3 секунды, среднее около 390 ms, что выглядит приемлемо, но каждый десятый пользователь ждёт три секунды. p95 говорит, что 95% запросов были быстрее этого значения; p99 показывает худший опыт, с которым всё ещё столкнётся большинство пользователей. Страница часто делает много запросов, поэтому вероятность, что просмотр страницы попадёт в медленный хвост, гораздо выше, чем для одного запроса. Поэтому целевые показатели и алерты должны быть на перцентилях вместе с долей ошибок, а среднее полезно только для расчётов мощностей.
Q3Что такое виртуальный пользователь в k6?
Виртуальный пользователь, или VU, — это независимый контекст выполнения, который в цикле запускает вашу функцию по умолчанию, как симулированный человек, пользующийся системой. У каждого VU свои переменные и cookie jar, и VU работают параллельно. Число VU — это не то же самое, что запросы в секунду: если каждая итерация вместе со временем на размышление занимает две секунды, 100 VU дают около 50 итераций в секунду. Именно поэтому существуют arrival-rate executors: вы задаёте частоту новых итераций, а k6 выделяет столько VU, сколько нужно. k6 эффективно выполняет VU на Go, поэтому одна машина может симулировать тысячи.
Q4Как использовать thresholds в CI?
Thresholds — это правила «прошёл/не прошёл» для метрик, например доля http_req_failed ниже 1% и p95 http_req_duration ниже 400 миллисекунд. Если любой порог не пройден, k6 завершается с ненулевым кодом, и CI-джоба падает так же, как сломанный unit-тест. Пороги можно ограничить тегами, например только сценарием checkout, и задать abortOnFail, чтобы досрочно остановить тест, если ошибки резко растут. В пайплайне я запускаю короткий тест против staging после деплоя с порогами на основе целевых показателей сервиса, а более длинные тесты держу в ночном расписании, чтобы основной пайплайн оставался быстрым.
Q5Как сделать нагрузочный тест реалистичным?
Начните с продакшен-данных: аналитика или access-логи показывают, какие эндпоинты используются, в каких пропорциях и с какой пиковой частотой. Моделируйте основные пользовательские сценарии с временем на размышление между шагами и используйте разнообразные тестовые данные — разные товары, поисковые запросы и пользователей, — чтобы кэши и база данных вели себя как в реальности. Используйте открытую arrival-rate модель, чтобы пользователи продолжали приходить, даже если система замедлится. Запускайте против окружения, близкого к продакшену по размеру и объёму данных, включите прогрев и мониторьте серверы во время теста. И наконец, сравните профиль задержек теста с продакшеном, чтобы проверить модель.
Q6Может ли k6 тестировать браузер и производительность фронтенда?
Да. Браузерный модуль k6 управляет настоящим браузером Chromium через API, похожий на Playwright, поэтому можно измерять Core Web Vitals, такие как LCP и INP, и тестировать пользовательские сценарии в браузере. Браузерные тесты гораздо тяжелее протокольных: один браузерный VU потребляет столько же ресурсов, сколько сотни HTTP VU. Распространённый гибридный подход — генерировать основную нагрузку лёгкими HTTP-сценариями и параллельно запускать несколько браузерных VU, чтобы измерить, что реальные пользователи видят под этой нагрузкой. Для чистой производительности фронтенда без нагрузки проще использовать Lighthouse или WebPageTest.
Вопросы уровня senior
S1Как бы вы встроили тестирование производительности в пайплайн доставки?
Я бы использовал уровни с разной стоимостью. На каждый merge — smoke-тест на одну-две минуты против эфемерного или staging-окружения с порогами на долю ошибок и p95 для ключевых эндпоинтов; он ловит сломанные скрипты и крупные регрессии. Каждую ночь — тест средней нагрузки на ожидаемом пике с фиксированным набором данных в окружении, похожем на продакшен; результаты сохраняются в Prometheus или Grafana Cloud k6 и сравниваются с предыдущими прогонами, с алертом на регрессии больше примерно 10%. Перед крупными запусками — стресс-, spike- и soak-тесты под наблюдением инженеров. Скрипты живут в репозитории рядом с кодом и ревьюятся как тесты. Компромисс — стоимость окружений против уверенности; эфемерные окружения помогают, но должны иметь реалистичный размер, иначе цифры врут.
S2Как превратить результаты нагрузочного теста в решения по мощностям и автоскейлингу?
Запустите ступенчатый arrival-rate тест и постройте графики пропускной способности, задержки p95 и загрузки ресурсов на каждой ступени. Излом кривой, где задержка резко растёт, а пропускная способность выходит на плато, — это практическая ёмкость одного инстанса; я планирую работу примерно на 60–70% от неё, чтобы поглощать пики и отказы. Определите, какой ресурс насыщается первым: если CPU, автоскейлинг по CPU; если соединения с базой данных, добавление инстансов приложения только ухудшит ситуацию, поэтому сначала исправьте пулинг или добавьте read-реплики. Проверьте сам автоскейлер spike-тестом, ведь масштабирование занимает минуты, и к тому времени пик может закончиться. И наконец, посчитайте стоимость тысячи запросов при выбранном размере — это делает компромисс видимым для бизнеса.
S3Чем отличается нагрузочное тестирование событийной системы или системы на очередях?
HTTP-ответ больше не означает конец работы. API может ответить 202 Accepted за 30 ms, а задача будет минутами лежать в очереди, поэтому задержка запроса сама по себе выглядит отлично, пока система отстаёт. Я измеряю сквозное время: помечаю каждое тестовое сообщение ID и временной меткой, а консьюмер или проба записывает время завершения, и во время теста отслеживаю лаг и глубину очереди. Пороги должны покрывать и API, и целевое время обработки, например 95% заказов обработаны в течение 30 секунд. Я также тестирую масштабирование консьюмеров и back-pressure: что происходит, когда downstream-сервис замедляется, и не усиливают ли повторы нагрузку. Soak-тесты здесь важнее, потому что бэклоги растут медленно.
Глоссарий
- Виртуальный пользователь (VU)
- Симулированный пользователь, который выполняет тестовую функцию в цикле параллельно с другими.
- Итерация
- Один полный прогон функции по умолчанию одним VU.
- Executor
- Компонент k6, который планирует VU и итерации в сценарии.
- Arrival rate
- Запуск новых итераций с фиксированной частотой независимо от времени ответа (открытая модель).
- Threshold
- Правило «прошёл/не прошёл» для метрики; если оно не выполнено, k6 завершается с ошибкой.
- Check
- Проверка ответа, которая учитывается в статистике, но не останавливает тест.
- p95 / p99
- Задержка, быстрее которой выполнились 95% или 99% запросов.
- Soak-тест
- Долгий тест при обычной нагрузке для поиска утечек и медленного истощения ресурсов.
- Coordinated omission
- Ошибка измерения, когда тестер замедляется вместе с системой и скрывает реальную задержку.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.