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

Ключевая мысль в том, что сам по себе сбор цифр о времени ответа ничего не решает. Цифры надо с чем-то сравнивать и на основе сравнения принимать решение пускать релиз дальше или нет. Именно это делают пороговые значения. Они задают критерии прохождения и провала по метрикам, которые команде важны, и при нарушении инструмент завершается с ненулевым кодом, что для любого пайплайна означает провал шага. Так нагрузочный тест получает право голоса в выкатке.

k6 это открытый инструмент от Grafana Labs, написанный на Go с движком JavaScript для скриптов, без зависимостей вроде виртуальной машины Java или среды Node, просто один бинарник. Версия 1.0 вышла в мае 2025 года. Дальше идёт разбор того, как устроены тест и его метрики, как пороги делают инструмент пригодным для пайплайна, как настроить стратегию провала и как связать всё с визуализацией в Grafana. Кода ровно столько, сколько нужно увидеть механику, и весь он в отдельных блоках с разбором вокруг.

Структура теста k6 и проверки как основа осмысленной нагрузки

Тест k6 это файл на JavaScript, описывающий, что делает виртуальный пользователь. В простейшем виде он шлёт запрос и проверяет ответ. Проверки фиксируют функциональную корректность под нагрузкой: вернулся ли ожидаемый код состояния, пришёл ли нужный ответ. Без проверок нагрузочный тест мерял бы лишь скорость, не убеждаясь, что под нагрузкой система отвечает правильно, а не быстро отдаёт ошибки.

import http from 'k6/http'
import { check, sleep } from 'k6'

export default function () {
  const res = http.get('https://api.example.com/products')
  check(res, {
    'код ответа 200': (r) => r.status === 200,
    'тело не пустое': (r) => r.body.length > 0,
  })
  sleep(1)
}

Пауза в конце имитирует время раздумий реального пользователя, который не дёргает сервер непрерывно, а делает паузы между действиями. Это важная деталь правдоподобия: тест без пауз создаёт неестественно плотный поток запросов и меряет систему в режиме, которого в жизни не бывает. Моделирование реалистичного поведения через паузы приближает нагрузку к боевой.

Запускается всё одной командой по имени файла. По умолчанию k6 крутит сценарий заданное число виртуальных пользователей в течение заданного времени и в конце выдаёт сводку метрик. Сами по себе метрики это пока лишь данные, и превращение их в вердикт начинается с порогов.

k6 run --vus 50 --duration 30s load-test.js

Пороговые значения как критерии прохождения и провала под цели уровня сервиса

Пороги это то, что превращает k6 из инструмента, просто собирающего данные о производительности, в инструмент, выносящий суждение о том, удовлетворяет ли приложение требованиям. Они задают критерии по метрикам, которые волнуют команду, и формулируются на языке целей уровня сервиса. Типичный набор порогов покрывает время ответа по процентилям, долю ошибок и долю успешных проверок.

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<500', 'p(99)<1000'],
    http_req_failed: ['rate<0.05'],
    checks: ['rate>=0.95'],
  },
}

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

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

Можно задавать пороги и на собственные метрики бизнеса: время логина, число оформленных заказов в секунду, доля ошибок оформления. Это привязывает технический тест к смыслу, понятному не только инженерам.

export const options = {
  thresholds: {
    login_duration: ['avg<300', 'p(95)<600'],
    checkout_errors: ['rate<0.01'],
  },
}

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

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

export const options = {
  thresholds: {
    http_req_duration: [{ threshold: 'p(95)<500', abortOnFail: true }],
  },
}

Отдельная важная тема это форма нагрузки. Не всякая нагрузка одинакова, и k6 различает несколько типов профилей. Дымовой тест с одним виртуальным пользователем проверяет, что сценарий вообще работает, и это лучший первый шаг: дымовой тест бесконечно лучше отсутствия теста вовсе. Нагрузочный тест моделирует ожидаемый поток. Тест на выносливость держит нагрузку долго, выявляя утечки и деградацию во времени. Стрессовый тест нагнетает поток выше ожидаемого, нащупывая потолок.

Тонкость, которую упускают новички, в выборе исполнителя нагрузки. Задавать число виртуальных пользователей это один способ, но для воспроизведения целевого числа запросов в секунду точнее работают исполнители на основе скорости поступления запросов, а не просто числа пользователей. Они держат заданный темп запросов независимо от того, как быстро отвечает система, что вернее моделирует реальный трафик. Моделировать стоит и смешанный профиль: одни пользователи просматривают каталог, другие дёргают программный интерфейс, третьи создают всплеск на логине, потому что в жизни нагрузка не однородна.

Встраивание в пайплайн и сдвиг тестирования производительности влево

Главная ценность k6 раскрывается, когда тест становится обязательным шагом пайплайна. Поскольку инструмент завершается ненулевым кодом при нарушении порогов, встраивание тривиально: пайплайн запускает тест, и если пороги нарушены, шаг падает, а выкатка останавливается. Это и есть автоматический заслон против просадок, ловящий регрессии производительности рано, до того как они доедут до продакшена.

# шаг пайплайна, падающий при нарушении порогов
- name: Нагрузочный тест k6
  run: k6 run load-test.js

Здесь срабатывает принцип сдвига влево. Падающие пороги останавливают слияние изменений, перенося контроль производительности к самому раннему моменту, к пулреквесту, а не к предрелизному стенду. Разумная стратегия начинается с малого: лёгкий дымовой тест на каждый пулреквест уже даёт раннюю обратную связь, а тяжёлые тесты на выносливость и стресс гоняют реже, перед крупными запусками или по расписанию. Скрипты держат версионированными рядом с кодом и относятся к ним как к тестовым артефактам, потому что тест производительности это такой же код, заслуживающий ревью и истории изменений.

Полезная практика это тегировать всё: сценарий, эндпоинт, версию, идентификатор коммита. Теги потом позволяют фильтровать и сравнивать метрики по срезам, например смотреть время ответа конкретного сценария отдельно от остальных. Без тегов метрики сливаются в общую кашу, по которой трудно понять, что именно деградировало.

Связка с Grafana и корреляция нагрузки с серверными данными

Цифры в терминале полезны для вердикта, но для понимания причин нужна визуализация, и тут k6 родом из экосистемы Grafana раскрывается полнее всего. Метрики теста транслируются в хранилище вроде Prometheus или InfluxDB, а Grafana строит по ним панели. Начиная с относительно недавних версий k6 ещё и несёт встроенную живую веб-панель прямо из коробки, так что реальное время видно без дополнительного обвеса.

# трансляция метрик в Prometheus для визуализации в Grafana
k6 run --out experimental-prometheus-rw load-test.js

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

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

Управление формой нагрузки через этапы и плавное нарастание

Отдельного разбора заслуживает то, как нагрузка меняется во времени в пределах одного теста. Резко обрушить на систему полную нагрузку с первой секунды нереалистично, потому что в жизни трафик нарастает постепенно. k6 позволяет описать этапы, на которых число виртуальных пользователей плавно растёт, держится на плато и затем спадает. Такой профиль гораздо вернее моделирует реальный день сервиса с утренним разгоном, дневным плато и вечерним спадом.

export const options = {
  stages: [
    { duration: '2m', target: 100 },
    { duration: '5m', target: 100 },
    { duration: '2m', target: 0 },
  ],
}

Этот профиль читается так: за две минуты нагрузка плавно поднимается до ста пользователей, пять минут держится на этом уровне, затем за две минуты спадает до нуля. Фаза нарастания важна не только для правдоподобия, но и для диагностики. Если система ломается именно в момент резкого роста, а на стабильном плато держится, это указывает на проблему с масштабированием или прогревом, а не с устойчивой ёмкостью. Разделение фаз помогает понять характер просадки.

Для стрессового теста этапы наращивают нагрузку ступенями выше ожидаемого максимума, чтобы нащупать точку, где система начинает деградировать. Для теста на выносливость, наоборот, держат умеренное плато очень долго, выявляя медленные утечки памяти и постепенное замедление, которые на коротком прогоне незаметны. Форма нагрузки это полноценный инструмент исследования, а не просто декорация, и осмысленный выбор этапов отвечает на разные вопросы о системе.

Стоит помнить, что генерация очень высокой нагрузки упирается в возможности одной машины. Когда нужен поток, превышающий то, что способен выдать один агент, прибегают к распределённому исполнению или облачному запуску, разносящему генерацию трафика по нескольким узлам. Это позволяет проверить поведение системы при экстремальных условиях, недостижимых с одного компьютера.

Практические ориентиры построения нагрузочного тестирования в пайплайне

Чтобы свести опыт к рабочим ориентирам, ниже единственный список этой статьи, расставленный по важности:

  1. Начинать с малого, с дымового теста на одном пользователе на каждый пулреквест, потому что минимальный тест несравнимо лучше его отсутствия;
  2. Задавать пороги по целям уровня сервиса через процентили и долю ошибок, а не по обманчивому среднему, и выравнивать их рано;
  3. Делать падающие пороги обязательным заслоном пайплайна, останавливающим слияние и выкатку при деградации, ради настоящего сдвига влево;
  4. Моделировать реалистичный смешанный профиль трафика и применять исполнителей на основе темпа запросов для точного воспроизведения нагрузки;
  5. Тегировать сценарии, эндпоинты, версии и коммиты, транслировать метрики в Grafana и коррелировать их с серверными данными для поиска причин;
  6. Обновлять тестовые данные, чтобы кеширование не приукрашивало результат, и держать тяжёлые тесты выносливости и стресса на отдельном расписании.

Эти шесть правил отделяют живую культуру тестирования производительности от ритуального прогона ради галочки. Производительность это не разовая фаза перед релизом, а привычка, встроенная в каждое изменение.

Итог укладывается в одну мысль. Нагрузочный тест, который только собирает цифры, бесполезен, потому что цифры сами по себе ничего не запрещают. Пороговые значения дают цифрам силу вердикта: они формулируют цели уровня сервиса, и при их нарушении k6 роняет шаг пайплайна ненулевым кодом, превращаясь в автоматический заслон против просадок. Встроенный в пайплайн на каждый пулреквест, начатый с малого и выровненный по процентилям, такой тест ловит регрессии рано и дёшево. А связка с Grafana и корреляция с серверными данными превращают его из выносящего приговор чёрного ящика в инструмент, показывающий, где именно система упёрлась в потолок. Команда, сделавшая нагрузочный тест привычкой каждого изменения, узнаёт о просадке от пайплайна. Команда, оставившая его на предрелизный стенд, узнаёт о ней от пользователей.