Что такое k6?

k6 — это современный open-source инструмент нагрузочного тестирования, созданный Grafana Labs. В отличие от GUI-подхода JMeter, k6 использует скрипты на JavaScript, которые вы пишете в редакторе кода и запускаете из командной строки. Это делает его естественным выбором для разработчиков и инженеров автоматизации, предпочитающих код конфигурации.

k6 написан на Go, что обеспечивает отличные характеристики производительности. Одна машина с k6 может симулировать тысячи виртуальных пользователей с низким потреблением ресурсов по сравнению с JMeter. Инструмент естественно интегрируется в CI/CD пайплайны, что делает его идеальным для shift-left подхода к тестированию производительности.

k6 vs JMeter: Когда использовать какой инструмент

Прежде чем погружаться в k6, полезно понять, где он превосходит JMeter.

Характеристикаk6JMeter
СкриптингJavaScript (код)GUI + XML
Потребление ресурсовНизкое (Go runtime)Высокое (Java/JVM)
Поддержка протоколовHTTP, WebSocket, gRPCHTTP, JDBC, FTP, SMTP, LDAP, JMS
Интеграция с CI/CDНативная (CLI-first)Требует плагинов
Распределённое тестированиеk6 Cloud или xk6-distributedВстроенный master/slave
Тестирование браузераМодуль k6 browserНе поддерживается
Кривая обученияЛегко, если знаете JSСредняя (на основе GUI)
СообществоБыстро растётОчень большое, зрелое

Выбирайте k6, когда: Ваша команда знает JavaScript, нужна интеграция с CI/CD, тестируете HTTP/gRPC/WebSocket API или хотите лёгкое локальное выполнение.

Выбирайте JMeter, когда: Нужна поддержка протоколов помимо HTTP (JDBC, FTP, JMS), команда предпочитает GUI или нужна зрелая экосистема с обширными плагинами.

Установка k6

# macOS
brew install k6

# Windows
choco install k6

# Linux (Debian/Ubuntu)
sudo gpg -k
sudo gpg --no-default-keyring --keyring /usr/share/keyrings/k6-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys C5AD17C747E3415A3642D57D77C6C491D6AC1D68
echo "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.list
sudo apt-get update
sudo apt-get install k6

# Docker
docker run --rm -i grafana/k6 run - <script.js

Проверьте установку:

k6 version

Ваш первый скрипт k6

Вот базовый скрипт k6, отправляющий HTTP GET-запросы:

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

export const options = {
  vus: 10,        // 10 виртуальных пользователей
  duration: '30s', // выполнять 30 секунд
};

export default function () {
  const res = http.get('https://test-api.k6.io/public/crocodiles/');

  check(res, {
    'статус 200': (r) => r.status === 200,
    'время ответа < 500мс': (r) => r.timings.duration < 500,
    'body содержит crocodiles': (r) => r.body.includes('crocodiles'),
  });

  sleep(1); // ждать 1 секунду между итерациями
}

Запуск:

k6 run script.js

Структура скрипта

Каждый скрипт k6 состоит из трёх ключевых частей:

  1. Imports: Загрузка модулей k6 (k6/http, k6, k6/metrics)
  2. Options: Настройка VUs, длительности, stages, thresholds
  3. Функция default: Код, который каждый VU выполняет в цикле

Функция default выполняется один раз за итерацию для каждого VU. При 10 VUs и sleep в 1 секунду вы получаете примерно 10 запросов в секунду.

Виртуальные пользователи (VUs) и итерации

Virtual User (VU) — это конкурентный поток выполнения. Каждый VU выполняет функцию default в цикле до истечения длительности теста или достижения числа итераций.

  • VUs: Количество одновременных пользователей
  • Iterations: Общее число выполнений функции default (по всем VUs)
  • Duration: Как долго выполняется тест

Можно указать либо iterations, либо duration, но не оба одновременно с фиксированным числом VUs.

// Фиксированные итерации: 100 итераций распределены между 10 VUs
export const options = {
  vus: 10,
  iterations: 100,
};

// На основе длительности: 10 VUs в течение 1 минуты
export const options = {
  vus: 10,
  duration: '1m',
};

Stages: Нарастание, Стабильная нагрузка, Снижение

Для реалистичных паттернов нагрузки используйте stages для изменения числа VUs во времени:

export const options = {
  stages: [
    { duration: '2m', target: 50 },   // нарастание до 50 VUs за 2 минуты
    { duration: '5m', target: 50 },   // удержание 50 VUs 5 минут
    { duration: '2m', target: 0 },    // снижение до 0 за 2 минуты
  ],
};

Это создаёт классический профиль нагрузочного теста: постепенное увеличение, устойчивая нагрузка и плавное снижение.

Thresholds: Критерии прохождения/провала

Thresholds определяют критерии успеха теста. При нарушении любого threshold k6 завершается с ненулевым кодом возврата — идеально для CI/CD пайплайнов.

export const options = {
  thresholds: {
    http_req_duration: ['p(95)<500'],     // 95% запросов менее 500мс
    http_req_failed: ['rate<0.01'],        // менее 1% ошибок
    http_reqs: ['rate>100'],               // минимум 100 запросов/секунду
    checks: ['rate>0.99'],                 // 99% checks проходят
  },
};

Популярные метрики для thresholds:

  • http_req_duration — Время ответа (p50, p90, p95, p99, avg, max)
  • http_req_failed — Процент неудачных запросов
  • http_reqs — Частота запросов (запросов в секунду)
  • checks — Процент пройденных проверок

Checks: Встроенные проверки

Checks проверяют данные ответа без остановки теста (в отличие от assertions в JMeter, которые помечают запросы как неудачные):

check(res, {
  'статус 200': (r) => r.status === 200,
  'body содержит ожидаемое поле': (r) => JSON.parse(r.body).hasOwnProperty('id'),
  'время ответа ОК': (r) => r.timings.duration < 300,
});

Checks записываются как метрики. Можно устанавливать thresholds на процент прохождения checks.

Работа с HTTP-методами

import http from 'k6/http';

// GET
const getRes = http.get('https://api.example.com/users');

// POST с JSON body
const payload = JSON.stringify({ name: 'Test User', email: 'test@example.com' });
const params = { headers: { 'Content-Type': 'application/json' } };
const postRes = http.post('https://api.example.com/users', payload, params);

// PUT
const putRes = http.put('https://api.example.com/users/1', payload, params);

// DELETE
const delRes = http.del('https://api.example.com/users/1');

// Batch-запросы (параллельные)
const responses = http.batch([
  ['GET', 'https://api.example.com/users'],
  ['GET', 'https://api.example.com/products'],
  ['GET', 'https://api.example.com/orders'],
]);

Упражнение: Многоэндпоинтный нагрузочный тест с k6

Напишите скрипт k6, который тестирует API интернет-магазина с аутентификацией, несколькими эндпоинтами, thresholds и checks.

Сценарий

Протестируйте API интернет-магазина со следующим потоком:

  1. Логин для получения токена аутентификации
  2. Просмотр каталога товаров
  3. Просмотр конкретного товара
  4. Добавление товара в корзину

Требования

  1. Используйте stages: нарастание до 25 VUs за 1 минуту, удержание 3 минуты, снижение за 1 минуту
  2. Установите thresholds: p(95) < 800мс, процент ошибок < 1%, прохождение checks > 99%
  3. Добавьте checks для кодов состояния и содержимого ответа
  4. Используйте токен аутентификации в последующих запросах
  5. Добавьте реалистичное время ожидания между запросами
Подсказка: Структура скрипта
import http from 'k6/http';
import { check, sleep, group } from 'k6';

export const options = {
  stages: [
    { duration: '1m', target: 25 },
    { duration: '3m', target: 25 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    // определите thresholds здесь
  },
};

export default function () {
  // Группа 1: Логин
  // Группа 2: Просмотр каталога
  // Группа 3: Просмотр товара
  // Группа 4: Добавление в корзину
}

Используйте group() для организации запросов в логические секции. Это даёт метрики по группам в результатах.

Решение: Полный скрипт k6
import http from 'k6/http';
import { check, sleep, group } from 'k6';

const BASE_URL = 'https://api.ecommerce.example.com';

export const options = {
  stages: [
    { duration: '1m', target: 25 },
    { duration: '3m', target: 25 },
    { duration: '1m', target: 0 },
  ],
  thresholds: {
    http_req_duration: ['p(95)<800'],
    http_req_failed: ['rate<0.01'],
    checks: ['rate>0.99'],
  },
};

export default function () {
  let token;

  // Шаг 1: Логин
  group('Логин', function () {
    const loginPayload = JSON.stringify({
      username: `user${__VU}@example.com`,
      password: 'testpass123',
    });

    const loginRes = http.post(`${BASE_URL}/api/auth/login`, loginPayload, {
      headers: { 'Content-Type': 'application/json' },
    });

    check(loginRes, {
      'логин статус 200': (r) => r.status === 200,
      'логин возвращает токен': (r) => JSON.parse(r.body).token !== undefined,
    });

    token = JSON.parse(loginRes.body).token;
  });

  sleep(1);

  const authHeaders = {
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
    },
  };

  // Шаг 2: Просмотр каталога
  let productId;
  group('Просмотр каталога', function () {
    const catalogRes = http.get(`${BASE_URL}/api/products`, authHeaders);

    check(catalogRes, {
      'каталог статус 200': (r) => r.status === 200,
      'каталог возвращает товары': (r) => JSON.parse(r.body).products.length > 0,
    });

    const products = JSON.parse(catalogRes.body).products;
    productId = products[Math.floor(Math.random() * products.length)].id;
  });

  sleep(2);

  // Шаг 3: Просмотр деталей товара
  group('Просмотр товара', function () {
    const productRes = http.get(`${BASE_URL}/api/products/${productId}`, authHeaders);

    check(productRes, {
      'товар статус 200': (r) => r.status === 200,
      'товар имеет название': (r) => JSON.parse(r.body).name !== undefined,
      'товар имеет цену': (r) => JSON.parse(r.body).price > 0,
    });
  });

  sleep(1);

  // Шаг 4: Добавление в корзину
  group('Добавление в корзину', function () {
    const cartPayload = JSON.stringify({
      productId: productId,
      quantity: 1,
    });

    const cartRes = http.post(`${BASE_URL}/api/cart`, cartPayload, authHeaders);

    check(cartRes, {
      'корзина статус 200 или 201': (r) => r.status === 200 || r.status === 201,
      'корзина подтверждает добавление': (r) => JSON.parse(r.body).success === true,
    });
  });

  sleep(1);
}

Запуск теста:

k6 run ecommerce-load-test.js

На что обращать внимание в результатах:

  • http_req_duration — p95 должен быть менее 800мс
  • http_req_failed — должен быть близок к 0%
  • checks — процент прохождения должен быть выше 99%
  • http_reqs — общая частота запросов
  • Метрики по группам показывают, какой эндпоинт самый медленный

Экспорт результатов для анализа:

# JSON-вывод
k6 run --out json=results.json ecommerce-load-test.js

# CSV-вывод
k6 run --out csv=results.csv ecommerce-load-test.js

# InfluxDB (для дашбордов Grafana)
k6 run --out influxdb=http://localhost:8086/k6 ecommerce-load-test.js

Профессиональные советы

  • Используйте __VU и __ITER: Встроенные переменные __VU (номер текущего VU) и __ITER (номер текущей итерации) помогают создавать уникальные данные для каждого пользователя без внешних CSV-файлов.
  • SharedArray для тестовых данных: Загружайте большие наборы данных один раз и используйте совместно между VUs с помощью SharedArray из k6/data. Это предотвращает загрузку собственной копии каждым VU.
  • Кастомные метрики: Создавайте пользовательские метрики с помощью Counter, Gauge, Rate и Trend из k6/metrics для отслеживания бизнес-специфичных KPI.
  • Scenarios для сложных паттернов: Используйте scenarios вместо stages, когда нужны несколько типов исполнителей (constant-vus, ramping-vus, per-vu-iterations, constant-arrival-rate), работающих одновременно.
  • Модуль k6 browser: Для нагрузочного тестирования на уровне браузера k6 теперь включает модуль браузера на базе Chromium, способный измерять Core Web Vitals под нагрузкой.