1. Введение
Нагрузочное тестирование. Многие считают что это больно и дорого, и если честно, не без причин. JMeter, которому нужна Java и отдельно потраченное время на интерфейс который не назвать интуитивным. Облачные сервисы с ценниками под сотни тысяч в год, что для пет-проекта просто несерьёзно. И локальные запуски с рабочего ноутбука через домашний Wi-Fi которые дают цифры не имеющие отношения к реальности, сеть плавает, p95 уходит куда-то, и ты сидишь гадаешь это сервер не держит или сосед кино скачал.
Перед каждым релизом вопрос один и тот же. Выдержит или ляжет.
Я покажу как сделать нагрузочное тестирование самостоятельно, без ДД (долго, дорого). Инструмент k6, VPS, сценарий на JavaScript, на всё про всё полчаса и стоимость сервера несколько рублей в час. Пройдём от установки до визуализации в Grafana Cloud, я объясню что значат цифры в выводе k6 и на какие метрики обращать внимание в первую очередь. Статья для тестировщиков, разработчиков, для всех у кого есть пет-проекты и для тех кто с нагрузкой ещё не сталкивался.
2. Что такое k6 и почему именно он
Выбор инструмента для нагрузочного тестирования это всегда компромисс между простотой, функциональностью и стоимостью. На рынке есть десятки решений, от тяжелых корпоративных платформ до легковесных скриптов на Python, и k6 занимает в этом спектре особое место.
k6 это инструмент для нагрузочного тестирования с открытым исходным кодом, который разрабатывает компания Grafana Labs. На момент написания статьи, май 2026 года, актуальной версией считается v2.0.0-rc1, она вышла в конце апреля. Инструмент активно развивается, но ключевая идея остаётся неизменной: тесты пишутся на JavaScript, а сам k6 работает на Go, что даёт высокую производительность при низком потреблении ресурсов.
Но хватит общих слов. Прежде чем ставить k6, давайте разберёмся, почему именно он, а не что-то другое. Я не хочу чтобы сложилось впечатление что это единственный инструмент на свете. Но для тех задач о которых эта статья, он подходит лучше всего. Сейчас покажу на сравнении.
Apache JMeter это ветеран рынка, ему больше двадцати лет. За это время он стал стандартом в корпоративной среде, особенно в банках и крупных компаниях. JMeter имеет графический интерфейс, который позволяет собирать тестовые сценарии перетаскиванием компонентов. Для простых тестов это удобно: добавил Thread Group, добавил HTTP Request, добавил Listener для просмотра результатов, запустил и смотришь графики.
Проблемы начинаются, когда тестов становится много и они усложняются. JMeter хранит сценарии в XML формате. XML не предназначен для ручного редактирования, особенно когда сценарий разрастается до тысяч строк. Версионирование таких файлов в Git превращается в боль: любые изменения порождают конфликты слияния, разбирать которые приходится вручную. Для решения этой проблемы сообщество создало инструменты вроде Taurus, которые позволяют писать тесты на YAML и транслировать их в JMeter XML, но это добавляет ещё один слой абстракции.
Другая проблема JMeter это управление ресурсами. JMeter работает на Java, и для серьёзных тестов требуется тщательная настройка JVM: размер кучи, сборщик мусора, параметры потоков. Один экземпляр JMeter на машине с 4 ГБ оперативной памяти с трудом держит 500-1000 виртуальных пользователей, после чего начинаются проблемы с производительностью самого генератора. Для масштабирования JMeter предлагает распределённый режим с мастером и агентами, что добавляет сложности в развёртывании.
Locust пошёл другим путём. Он предложил писать тесты на Python, что сразу сделало его привлекательным для разработчиков которые уже знают этот язык. Сценарий на Locust выглядит как обычный Python код: определяешь класс, наследующийся от HttpUser, добавляешь методы с декоратором @task, описываешь поведение пользователя. Выглядит чисто и понятно.
Однако у Locust есть архитектурное ограничение, GIL (Global Interpreter Lock) в Python. GIL не позволяет нескольким потокам выполнять Python код одновременно. Locust обходит это ограничение, запуская отдельные процессы для каждого воркера, но каждый процесс потребляет значительный объём памяти. На практике для генерации 10 000 виртуальных пользователей на Locust может потребоваться в 3-5 раз больше ресурсов, чем на k6.
Ладно, с конкурентами закончили. Вернёмся к k6.
Инструмент написан на Go. Честно говоря, когда я впервые это узнала, показалось странным выбором для инструмента тестирования. Но решение оказалось точным. Go не имеет GIL, многопоточность встроена на уровне языка через горутины. А горутина это не поток, она в десятки раз легче. Я проверяла на практике: один экземпляр k6 загружает все ядра и спокойно держит десятки тысяч запросов в секунду на довольно скромном оборудовании.
Сценарии k6 пишутся на JavaScript, который знаком практически каждому разработчику и тестировщику. Это снижает порог входа. Ещё k6 реализует подход Load-as-Code. Сценарии можно хранить в Git, версионировать, рефакторить и переиспользовать. Это особенно ценно в контексте CI/CD, где тесты становятся частью пайплайна и должны эволюционировать вместе с кодом приложения.
Вот ключевые возможности k6, которые понадобятся нам в этой статье. Настраиваемая генерация нагрузки, можно задать постоянное количество виртуальных пользователей или описать стадии роста и спада. Встроенные пороговые значения, thresholds. Это условия при которых тест считается проваленным, если метрика выходит за границу k6 завершается с ненулевым кодом, что позволяет встроить тест в CI/CD пайплайн. Тесты как код, сценарии на JavaScript можно хранить в репозитории, переиспользовать и версионировать. Поддержка нескольких протоколов: HTTP, WebSocket, gRPC и другие. Начиная с версии 2.0.0-rc1 модуль для работы с Redis был перенесён из k6/experimental/redis в k6/x/redis, благодаря механизму авторазрешения зависимостей k6 сам подгрузит нужный модуль при импорте. И ещё экспорт метрик, результаты можно выгрузить в JSON, CSV, InfluxDB или отправить в Grafana Cloud для визуализации.
Разумеется, у k6 есть и ограничения. Он не поддерживает распределённый запуск из коробки, как JMeter, и не имеет графического интерфейса для создания сценариев. Если вам нужно генерировать миллион запросов в секунду с географически распределённых точек, k6 сам по себе эту задачу не решит, потребуется обвязка вроде Kubernetes и k6 Operator. Но для 99 процентов случаев, с которыми сталкиваются небольшие команды и индивидуальные разработчики, возможностей k6 хватает с запасом.
3. Почему для тестов нужен VPS, а не локальный компьютер
Локальный компьютер для тестов не годится. Я это поняла не сразу. Сидела полночи, искала проблему в коде. А проблема сидела в роутере. Домашняя сеть через Wi-Fi просто нестабильна от природы. Джиттер, задержка скачет. Один и тот же тест из дома показал p95 320 миллисекунд, а с VPS в дата-центре 180. Почти вдвое. И всё из-за сети. Смешно и грустно одновременно.
Провайдер тоже не спасает. Тариф 100 мегабит, а вечером канал режется, потому что весь дом садится за видео. Тест в этот момент делит трафик с соседским стримингом, результаты недостоверны. Тут даже спорить не о чем, просто факт. Ресурсы ноутбука. Открыта IDE, браузер, мессенджеры, а k6 в это время ест процессор и генерит трафик. Система троттлит, вентиляторы шумят, цифры плывут. Знакомая картина?
VPS в дата-центре снимает все эти вопросы. Магистральный канал, полоса гарантирована. Сеть проектировали под пиковые нагрузки, джиттер минимален. IP выделенный, NAT не мешает. И сервер изолирован, на нём только генератор и никаких посторонних процессов.
Правило простое. VPS должен быть там же где и тестируемый сервер. Если сервер в Москве а VPS в Нидерландах, пинг добавляет 40-60 миллисекунд к каждому измерению. Вы тестируете не сервер, а скорость трансатлантического кабеля. Если разнесены, вычтите пинг при анализе. Лучше всего один дата-центр или один город.
4. Что потребуется для повторения
Для повторения нужно вот что.
VPS простой конфигурации. Одно или два ядра, 2 ГБ памяти, 20 ГБ диска, Ubuntu 22.04 или 24.04. Такая машина тянет 3-5 тысяч простых HTTP запросов в секунду. Для десятков тысяч пользователей берите пропорционально больше. SSH клиент. На Linux и macOS он встроен, на Windows PuTTY или OpenSSH в PowerShell. Базовое знакомство с терминалом. apt, gpg, nano. Если ставили пакеты через консоль, проблем не будет. Тестовый эндпоинт. Я использую публичный API quickpizza.grafana.com, Grafana Labs сделала его для демонстрации k6. Для своего сервиса замените на URL приложения в том же дата-центре что и VPS.
5. Шаг 1: Установка k6 на VPS
Подключаемся к серверу по SSH. В стандартных репозиториях Ubuntu пакет k6 отсутствует, поэтому необходимо добавить официальный репозиторий Grafana и импортировать GPG ключ. GPG ключ нужен для проверки подлинности пакетов: мы хотим быть уверены что устанавливаем k6 именно из репозитория Grafana, а не из подменённого источника. На минимальных сборках Ubuntu может не быть утилиты gnupg, которая нужна для импорта ключей. Установим её вместе с обновлением списка пакетов.
Последовательно выполняем команды:
sudo apt updatesudo apt install -y gnupgsudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D69echo "deb [signed-by=/usr/share/keyrings/k6-archive-keyring.gpg] https://dl.k6.io/deb stable main" | sudo tee /etc/apt/sources.list.d/k6.listsudo apt updatesudo apt install -y k6
Разберём что делает каждая команда. Первая команда обновляет индекс пакетов apt. Вторая устанавливает gnupg, если его ещё нет в системе. Третья команда импортирует GPG ключ Grafana: флаг --no-default-keyring указывает использовать отдельное хранилище ключей, --keyring задаёт путь к файлу, а --recv-keys получает ключ с указанным идентификатором с сервера ключей. Четвёртая команда добавляет репозиторий k6 в список источников apt, указывая ключ для проверки подписи. Пятая снова обновляет индекс, чтобы apt узнал о новом репозитории. Шестая устанавливает k6.
После завершения установки проверяем что k6 работает:
k6 version
На момент написания статьи актуальна версия v2.0.0-rc1. Если у вас установилась более свежая, что-то из команд может работать иначе. Сверьтесь с документацией, если что-то пошло не так.
6. Шаг 2: Пишем первый сценарий и разбираем его архитектуру
Создадим файл basic-test.js. Я использую редактор nano, вы можете взять любой другой:
nano basic-test.js
import http from 'k6/http';import { check, sleep } from 'k6';export const options = { vus: 10, duration: '30s', thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.01'], },};export default function () { const res = http.get('https://quickpizza.grafana.com'); check(res, { 'status is 200': (r) => r.status === 200, 'время ответа меньше 500 мс': (r) => r.timings.duration < 500, }); sleep(Math.random() * 3 + 1);}
Разберём этот сценарий построчно, как мы это делали бы с незнакомым кодом на код ревью. Понимание структуры сценария k6 важно, потому что на этой основе вы потом сможете писать собственные тесты любой сложности.
Строки import http from 'k6/http' и import { check, sleep } from 'k6' импортируют встроенные модули k6. Модуль http предоставляет функции для выполнения HTTP запросов: get, post, put, patch, del, options. Модуль check содержит функцию для проверок, которые не прерывают тест, но фиксируют процент успешных выполнений. Модуль sleep приостанавливает виртуального пользователя на случайное время, имитируя паузу между действиями. Пауза должна быть разной для каждого пользователя, иначе они пойдут синхронно и нагрузка станет неестественной.
Блок export const options это конфигурация теста. Параметр vus (virtual users) задаёт количество одновременно работающих виртуальных пользователей. В нашем случае 10 пользователей будут отправлять запросы параллельно. Параметр duration определяет длительность теста: 30 секунд. За эти 30 секунд общее количество запросов будет зависеть от случайных пауз, в среднем получится от 120 до 300 итераций.
Блок thresholds задаёт пороговые значения. Условие p(95)<500 означает что 95 процентов запросов должны выполняться быстрее 500 миллисекунд. Условие rate<0.01 ограничивает допустимую долю ошибок одним процентом. Если хотя бы одно условие не выполнится, k6 завершится с ненулевым кодом возврата, что пригодится при интеграции в CI/CD.
Функция export default function это точка входа. Она будет циклично вызываться каждым виртуальным пользователем в течение всего времени теста. Внутри функция http.get выполняет GET запрос к quickpizza.grafana.com. Функция check проверяет два условия: что статус ответа равен 200 и что время выполнения запроса меньше 500 миллисекунд. Каждая проверка возвращает булево значение, а результат накапливается в метрике checks. Функция sleep приостанавливает виртуального пользователя на случайное время от одной до четырёх секунд. Если все десять пользователей будут делать паузу ровно в секунду, они пойдут синхронно и нагрузка получится пилообразной, неестественной. Случайная пауза имитирует реальных людей, которые думают и кликают с разной скоростью.
Теперь дополним сценарий другими типами запросов, чтобы показать как k6 работает с разными HTTP методами. Добавим POST запрос с телом и заголовками, а также проверку JSON ответа:
import http from 'k6/http';import { check, sleep } from 'k6';export const options = { vus: 10, duration: '30s', thresholds: { http_req_duration: ['p(95)<500'], http_req_failed: ['rate<0.01'], },};export default function () { // GET запрос const resGet = http.get('https://quickpizza.grafana.com'); check(resGet, { 'GET status 200': (r) => r.status === 200, }); // POST запрос с телом и заголовками const payload = JSON.stringify({ name: 'Margherita', price: 9.99 }); const params = { headers: { 'Content-Type': 'application/json' }, }; const resPost = http.post('https://quickpizza.grafana.com/api/pizza', payload, params); check(resPost, { 'POST status 201': (r) => r.status === 201, 'тело ответа содержит id': (r) => r.json('id') !== undefined, }); sleep(Math.random() * 3 + 1);}
Здесь мы добавили POST запрос. Функция JSON.stringify преобразует объект JavaScript в строку JSON для отправки на сервер. Объект params задаёт заголовки запроса: Content-Type со значением application/json указывает серверу что мы отправляем JSON. После отправки запроса мы проверяем не только статус, но и содержимое ответа: метод r.json('id') парсит JSON ответ и извлекает значение поля id. Если поле существует, проверка проходит.
Это базовые строительные блоки из которых состоят сценарии k6. В реальных проектах вы будете комбинировать GET и POST запросы, добавлять проверки ответов, извлекать данные из ответов и передавать их между запросами.
7. Шаг 3: Запускаем тест и следим за загрузкой самого VPS
Перед запуском теста с большим количеством виртуальных пользователей необходимо увеличить системный лимит на количество одновременно открытых файлов. По умолчанию Ubuntu ограничивает этот показатель значением около 1024. Каждый виртуальный пользователь открывает сетевые соединения, и на 50 и более пользователях k6 может упереться в ошибку socket: too many open files. Увеличиваем лимит командой:
ulimit -n 65535
Это временное изменение, оно действует только в текущей сессии терминала. Чтобы сделать его постоянным, добавьте следующие строки в файл /etc/security/limits.conf:
* soft nofile 65535* hard nofile 65535
После этого потребуется перезайти в систему. Для наших целей временного лимита через ulimit достаточно.
Теперь запускаем базовый тест:
k6 run basic-test.js
В процессе выполнения в консоль будет выводиться прогресс: количество выполненных итераций, текущие значения метрик. По окончании появится итоговая сводка.
Перед запуском серьёзных тестов важно убедиться что сам VPS не стал узким местом. Проблема в том что если CPU генератора загружен на 100 процентов, он не может отправлять запросы с нужной скоростью, и вы измеряете не производительность тестируемого сервера, а производительность самого генератора.
Для мониторинга ресурсов я рекомендую btop, современный монитор ресурсов который по состоянию на 2026 год стал стандартом визуализации в терминале. Он показывает загрузку процессора, памяти, диска и сети в удобном графическом виде. Если btop не установлен, его можно поставить одной командой:
sudo apt install -y btop
Запустите btop в соседнем окне терминала и следите за загрузкой CPU во время теста. Если загрузка приближается к 100 процентам, генератор нагрузки достиг своего предела. В таком случае нужно либо упростить сценарий (уменьшить количество проверок, убрать парсинг JSON), либо перейти на более мощный тариф VPS.
Ориентир: одно виртуальное ядро CPU способно генерировать от трёх до пяти тысяч простых HTTP запросов в секунду. Если ваш скрипт делает сложные вычисления или обрабатывает большие объёмы данных ответа, эта цифра будет ниже. Например, если вы парсите JSON ответ размером в мегабайт, производительность может упасть до нескольких сотен запросов в секунду.
8. Шаг 4: Разбираем метрики и понимаем что они значат
Вывод k6 после теста содержит большое количество цифр. Я покажу на какие из них смотреть в первую очередь и объясню что они значат.
http_req_duration. Главное, с чего я всегда начинаю. Это полное время HTTP запроса: от отправки первого байта до получения последнего. Она включает в себя установку TCP соединения, TLS рукопожатие, отправку данных, ожидание ответа и получение тела ответа. Для большинства веб сервисов комфортным ориентиром считается когда 95-й перцентиль этой метрики не превышает 500 миллисекунд. Для внутренних API допустимы значения до одной секунды.
http_req_failed. Доля запросов завершившихся ошибкой. Тут должно быть строго ноль, любое отклонение повод лезть в логи. Ошибкой считается статус ответа не из семейства 2xx, таймаут соединения, ошибка DNS и тому подобное. Сервер мог начать отвечать 502 или 503, могли кончиться TCP порты на генераторе, мог глюкнуть DNS. Первым делом проверяйте логи, там почти всегда есть ответ.
checks. Процент успешных проверок из скрипта. С этой метрикой есть нюанс, о котором часто забывают. В отличие от http_req_failed, падение checks не остановит тест автоматически если это явно не настроено через thresholds. Целевой показатель сто процентов. Если ниже, значит сервер отдаёт не то что вы ожидали, например перестал возвращать поле id после деплоя. Я всегда добавляю пороговое условие: 'checks': ['rate>0.95'].
vus. Текущее количество активных виртуальных пользователей. При фиксированной нагрузке без стадий это значение постоянно. При использовании стадий оно меняется в соответствии с заданным профилем.
iterations. Общее количество итераций главной функции выполненных за время теста. Характеризует пропускную способность системы, чем больше итераций при фиксированном времени, тем больше запросов обработал сервер. Чтобы узнать запросы в секунду, разделите количество итераций на длительность теста в секундах.
http_req_waiting (также называют Server Processing Time или TTFB, Time To First Byte). Время от отправки запроса до получения первого байта ответа. Это время которое сервер тратит на обработку запроса. Бэкенд разработчикам эта метрика особенно важна: высокое значение http_req_waiting в сочетании с низкими значениями http_req_connecting и http_req_tls_handshaking означает что проблема на стороне приложения, а не в сети. Нужно профилировать код, смотреть запросы к базе данных, проверять внешние интеграции.
9. Важный блок: почему перцентили важнее среднего
Следующий раздел будет немного математическим. Без него нельзя, и я прошу потерпеть. Понимание перцентилей это то что отделяет настоящее нагрузочное тестирование от запуска скрипта и разглядывания циферок.
Среднее арифметическое, обманчивая метрика, и я сейчас покажу почему на конкретном примере. Это одно из самых частых заблуждений при работе с метриками производительности.
Предположим что 95 запросов из 100 обрабатываются за 100 миллисекунд, а 5 запросов зависают на 10 секунд. Среднее время составит: (95 умножить на 100 плюс 5 умножить на 10000) разделить на 100 равно 595 миллисекунд. Выглядит приемлемо: чуть больше полусекунды. Можно подумать что пользователи довольны.
Но 5 процентов пользователей ждали ответа 10 секунд. Если ваш сервис обрабатывает 10000 запросов в минуту, то 500 пользователей каждую минуту испытывают десятисекундную задержку. Они закроют вкладку и уйдут к конкурентам. Среднее арифметическое скрывает эту проблему. Просто игнорирует.
Теперь посмотрим на перцентили. 95-й перцентиль (p95), значение ниже которого находятся 95 процентов наблюдений. В нашем примере 95 самых быстрых запросов выполнились за 100 миллисекунд, поэтому p95 равен 100 миллисекундам. Эта метрика говорит что типичный пользовательский опыт хороший, но ничего не сообщает о проблемном хвосте. 99-й перцентиль (p99), значение ниже которого находятся 99 процентов наблюдений. В нашем примере 99 самых быстрых запросов всё ещё укладываются в 100 миллисекунд, а сотый, 10 секунд. p99 покажет те самые 10 секунд, сразу сигнализируя о проблеме для заметной доли пользователей.
Есть ещё p99.9. Он нужен не всегда. Если p99 уже показал проблему, p99.9 вряд ли скажет что-то новое. Но если у вас тысяча запросов и один из них завис на минуту, p99 этого выброса даже не заметит. А p99.9 заметит. Он вообще создан для таких ситуаций.
Именно поэтому в мониторинге производительности для критичных сервисов используют p99 или даже p99.9. p95 хорош как индикатор типичного опыта, но он может врать. Представьте что вы запустили тест на 1000 запросов и получили p95 равный 200 мс, а p99 равный 5000 мс. Вы смотрите на p95 и думаете: «Всё хорошо». Но p99 орёт: «Проблема!». Всегда смотрите на оба перцентиля вместе, это даёт объёмную картину производительности.
10. Шаг 5: Детализируем время запроса и находим узкие места
Метрика http_req_duration может быть разложена на составляющие. Это как разобрать двигатель чтобы понять какой узел работает с перебоями. Зная сколько времени занимает каждый этап, вы можете точно определить где проблема: в сети, в TLS, в приложении или в передаче данных.
Вот из чего складывается время запроса. http_req_blocked, время ожидания свободного TCP сокета. При малом количестве соединений обычно близко к нулю. Если значение высокое, возможно закончились TCP порты или клиент ждёт освобождения соединения в пуле. http_req_connecting, время установки TCP соединения. Включает тройное рукопожатие между клиентом и сервером: SYN, SYN-ACK, ACK. В норме занимает единицы миллисекунд в пределах одного дата-центра и до 10-20 мс при межконтинентальном соединении. http_req_tls_handshaking, время TLS рукопожатия в ходе которого клиент и сервер договариваются о шифровании. Включает несколько раундов обмена сообщениями и зависит от версии TLS: TLS 1.3 быстрее чем 1.2. http_req_sending, время отправки тела запроса. Для GET запросов обычно пренебрежимо мало. Для POST запросов с большим телом может быть значительным. http_req_waiting. Тот самый TTFB, который мы разбирали в метриках. Время когда сервер думает, а сеть уже всё отдала. http_req_receiving, время получения тела ответа. Зависит от размера ответа и пропускной способности канала.
Как интерпретировать. Если сумма http_req_connecting и http_req_tls_handshaking велика, проблема на сетевом уровне. Возможно сервер находится слишком далеко или есть проблемы с TCP соединениями. Проверьте пинг между VPS и тестируемым сервером, проверьте стабильность сети. Высокое значение http_req_waiting указывает на то что сервер долго обрабатывает запрос. Это самая частая проблема с которой я сталкиваюсь. Причины могут быть разными: медленный запрос к базе данных, блокировки, сборка мусора, неоптимальный алгоритм, внешний API который медленно отвечает. Здесь уже нужно профилировать код приложения. Высокое значение http_req_receiving при нормальном http_req_waiting говорит о том что сервер отдаёт слишком много данных. Проверьте не возвращаете ли вы лишние поля в ответе API. Возможно клиенту нужны только два поля а сервер отдаёт все пятьдесят.
11. Шаг 6: Усложняем сценарий и имитируем реальную нагрузку
Базовый тест с постоянным количеством виртуальных пользователей подходит для быстрой проверки, но не отражает реальную картину. В жизни трафик меняется: утром пользователи приходят на работу и открывают приложение, в обед наступает затишье, вечером новый всплеск.
Для моделирования таких изменений в k6 предусмотрен механизм стадий (stages). Стадии позволяют описать как меняется количество виртуальных пользователей во времени. Это мощный инструмент который превращает простой тест в имитацию реального поведения пользователей.
Создадим файл ramp-test.js:
import http from 'k6/http';import { check, sleep } from 'k6';export const options = { stages: [ { duration: '1m', target: 20 }, { duration: '3m', target: 20 }, { duration: '1m', target: 50 }, { duration: '3m', target: 50 }, { duration: '1m', target: 0 }, ], thresholds: { http_req_duration: ['p(95)<800', 'p(99)<1000'], http_req_failed: ['rate<0.01'], },};export default function () { const responses = http.batch([ 'https://quickpizza.grafana.com', 'https://quickpizza.grafana.com/api/pizza', ]); check(responses[0], { 'главная страница status 200': (r) => r.status === 200 }); check(responses[1], { 'API status 200': (r) => r.status === 200 }); sleep(Math.random() * 3 + 1);}
Перед запуском не забываем увеличить лимит открытых файлов если не сделали этого ранее. На 50 виртуальных пользователях Ubuntu может упереться в ограничение и k6 выдаст ошибку socket: too many open files:
ulimit -n 65535k6 run ramp-test.js
Разберём что изменилось по сравнению с базовым сценарием. Массив stages описывает пять стадий. Первая: duration одна минута, target 20, k6 плавно увеличит количество пользователей от нуля до 20 за одну минуту. Вторая: удержание 20 пользователей в течение трёх минут, имитация стабильной фазы когда мы снимаем основные метрики. Третья: рост до 50 пользователей за минуту, имитация пиковой нагрузки. Четвёртая: удержание 50 пользователей три минуты, замеряем поведение под пиком. Пятая: плавное снижение нагрузки до нуля.
В блоке thresholds появилось новое условие: p(99) меньше 1000. При высокой нагрузке p95 может оставаться в норме но хвост распределения (p99) будет расти. Это условие позволяет поймать такие ситуации.
Внутри функции вместо одиночного http.get используется http.batch. Обратите внимание на синтаксис: мы передаём массив URL строк. Это самый простой и корректный способ отправить несколько GET запросов параллельно, как это делает браузер загружая ресурсы страницы. Если нужно отправить запросы других типов (POST, PUT), используйте соответствующие функции внутри массива:
const responses = http.batch([ 'https://example.com/api/users', http.post('https://example.com/api/orders', payload, params), // можно комбинировать разные методы в одном вызове]);
Пауза задаётся случайной величиной, по тому же принципу что и в базовом сценарии. Случайная пауза создаёт реалистичный трафик.
Важное замечание по поводу облачных опций в версии 2.0.0-rc1. Если вы переносите старые скрипты на новую версию k6, убедитесь что в них нет блока ext.loadimpact. Этот синтаксис использовался для настройки облачного запуска и больше не поддерживается. Все настройки облачной среды теперь задаются в корне объекта options или через переменные окружения.
В процессе выполнения теста в консоли можно наблюдать как меняется значение vus: сначала растёт до 20, держится, потом растёт до 50, держится и падает до нуля. Это визуальный индикатор того как сервер справляется с возрастающей нагрузкой. Если во время роста нагрузки график vus начинает дёргаться или застывать, возможно сервер перестал успевать обрабатывать запросы.
12. Шаг 7: Настраиваем пороговые значения и встраиваем тест в CI/CD
Пороговые значения (thresholds) это механизм который превращает k6 из инструмента для ручного тестирования в автоматического стража производительности. Идея проста: вы задаёте критерии успешности теста и если они не выполняются k6 завершается с ненулевым кодом возврата. Система CI/CD видит этот код и блокирует развёртывание проблемной версии.
Вот несколько примеров пороговых условий которые можно комбинировать в зависимости от требований к сервису. p(95)<500, 95 процентов запросов быстрее 500 миллисекунд, стандартный порог для веб сервисов. p(99)<1000, 99 процентов запросов быстрее 1000 миллисекунд, ловит проблемы в длинном хвосте. rate<0.001, допустимая доля ошибок не более 0.1 процента, жёсткое требование для высоконагруженных систем. avg<300, среднее время ожидания ответа не более 300 миллисекунд.
Пороговые значения могут быть разными для разных окружений. На тестовом сервере обычно допускаются более высокие значения чем на проде. На staging значения должны быть близки к продакшену чтобы поймать проблемы до релиза. Рекомендую хранить пороги в отдельных файлах для каждого окружения и подключать их через переменные окружения или отдельные конфигурационные файлы.
Для встраивания в CI/CD пайплайн достаточно добавить вызов k6 в скрипт сборки. Как именно это сделать, зависит от вашей системы: Jenkins, GitLab CI, GitHub Actions, всё что угодно. Я покажу пример для GitLab CI, просто чтобы дать ориентир. Не переживайте если вы никогда не работали с CI/CD, сейчас важно понять общий принцип, а не синтаксис конкретного файла. Например для GitLab CI:
load_test: image: grafana/k6:latest script: - k6 run ramp-test.js allow_failure: false
Если тест не пройдёт пороговые значения, пайплайн упадёт и проблемный код не попадёт в продакшен. Параметр allow_failure: false важен: по умолчанию пайплайн может пропустить падение джобы, а нам нужно чтобы он блокировался.
По умолчанию k6 проверяет thresholds в конце теста. Это значит что если сервер лёг на первой минуте, тест всё равно будет гонять нагрузку оставшиеся восемь минут и только потом упадёт с ошибкой. Впустую потраченное время и деньги за VPS. Для CI/CD пайплайнов это можно улучшить. Добавьте в thresholds параметр abortOnFail: true, и k6 прервёт тест в ту же секунду как порог будет нарушен.
thresholds: { http_req_duration: [{ threshold: 'p(95)<800', abortOnFail: true }],}
k6 также поддерживает экспорт результатов в JSON, CSV, InfluxDB. Для регулярного использования в команде это основной способ работы, а не просто просмотр логов в консоли. Джоба пайплайна генерит артефакт, например results.json, а следующая джоба анализирует этот файл на предмет прохождения thresholds. Так вы не привязываетесь к stdout пайплайна, можете хранить историю прогонов и сравнивать результаты между релизами. Если p95 медленно но верно растёт от версии к версии, пора заняться оптимизацией даже если порог ещё не пробит. Для экспорта в JSON добавьте параметр при запуске:
k6 run --out json=results.json ramp-test.js
13. Шаг 8: Визуализируем результаты в Grafana Cloud
Консольный вывод даёт базовое представление о производительности, но для глубокого анализа удобнее визуализировать метрики в виде графиков. Человеческий глаз гораздо лучше воспринимает тренды на графике чем в таблицах с цифрами.
k6 имеет встроенную интеграцию с Grafana Cloud которая позволяет отправлять результаты теста в облако и просматривать их в веб интерфейсе. Grafana Cloud предоставляет бесплатный тариф. Для k6 он ограничен 500 виртуальными часами нагрузки в месяц (VUh). Для пет-проектов и разовых прогонов этого хватает с запасом.»
Для аутентификации проще всего использовать переменную окружения с API токеном. Это работает во всех версиях k6 и идеально подходит для CI/CD пайплайнов. Получите токен в Grafana Cloud в разделе Testing & synthetics → Performance → Settings и выполните:
export K6_CLOUD_TOKEN=<YOUR_API_TOKEN>
Если вы предпочитаете явную команду, в версии 2.0.0-rc1 используется k6 cloud login с обязательным указанием стека (организационной единицы в Grafana Cloud). Идентификатор стека виден в URL при заходе в него:
k6 cloud login --token <YOUR_API_TOKEN> --stack <STACK_SLUG>
После аутентификации запускаем тест локально на VPS и одновременно отправляем метрики в облако. В версии 2.0.0-rc1 обязательно указывать подкоманду run:
k6 cloud run --local-execution ramp-test.js
Ключ --local-execution указывает что сам тест выполняется на вашем сервере а в облако отправляются только метрики. Это экономит ресурсы облака и подходит для большинства случаев. Если запустить k6 cloud run без параметра --local-execution, скрипт загрузится в облако и будет выполнен на инфраструктуре Grafana. Такой вариант удобен когда ваш VPS не может обеспечить нужную нагрузку.
После завершения теста в терминале появится прямая ссылка на страницу с результатами. Перейдя по ней вы увидите графики: время ответа с разбивкой по перцентилям, количество запросов в секунду, ошибки, загрузка процессора генератора. Можно сравнить текущий прогон с предыдущими чтобы отследить динамику производительности. Если после последнего деплоя p95 вырос на 50 процентов, вы сразу это заметите.
14. Заключение и что дальше
Мы прошли путь от установки k6 на VPS до полноценного теста с имитацией реальной нагрузки и визуализацией результатов в Grafana Cloud. Если честно, когда я сама первый раз настраивала эту цепочку, была приятно удивлена. Никаких подводных камней, всё заработало с первого раза.
Теперь в арсенале есть инструмент который позволяет: проводить честное нагрузочное тестирование, результаты которого не зависят от домашнего интернета и загруженности ноутбука; получать конкретные метрики и находить узкие места до того как они приведут к падению продакшена; встроить тестирование в CI/CD пайплайн и автоматически блокировать проблемные релизы; делать всё это с минимальными финансовыми затратами, стоимость часа тестирования на бюджетном VPS исчисляется рублями.
При росте проекта и увеличении требований к нагрузочному тестированию стенд масштабируется. Поскольку k6 эффективно использует одно ядро процессора на экземпляр, первый шаг это перейти на VPS с более высокой тактовой частотой процессора. Это даст прирост производительности без изменения архитектуры тестов. Если одного сервера перестанет хватать, можно настроить распределённый запуск k6 с использованием k6 Operator в Kubernetes, это уже горизонтальное масштабирование при котором несколько экземпляров k6 работают параллельно каждый на своём ядре.
Регулярное нагрузочное тестирование перед релизами помогает избежать неприятных сюрпризов в проде и даёт уверенность в том что сервис выдержит наплыв пользователей. А сэкономленные на коммерческих инструментах средства можно направить на развитие продукта.
ссылка на оригинал статьи https://habr.com/ru/articles/1033472/