API Mindbox обрабатывает тысячи запросов в секунду, а мы публично гарантируем клиентам бесперебойный круглосуточный сервис. Чтобы выполнять свои обязательства, нам нужен непрерывный поток телеметрии, близкий к тому, что создают клиенты: со всеми деградациями, ошибками и распределением летенси.

На связи Дмитрий Рыбалка, SRE‑инженер Mindbox. В этой статье расскажу, как и почему мы используем k6, чтобы мониторить наш API.

k6 aka агент мониторинга: плотный поток телеметрии

Для внешнего мониторинга нашей сетевой инфраструктуры мы используем Blackbox Exporter. В целом со своими задачами он справляется: простой, понятный и покрывает большинство сценариев. Однако у него есть ограничение: новое значение метрики появляется только при запросе со стороны Prometheus, и обычно это происходит не чаще, чем раз в 15 секунд. При такой частоте запрос может отправиться в момент, когда простаивает CPU или когда очередь обработки на сервере пустая: деградация производительности уже произошла, и мониторинг ничего не зафиксировал.

Чтобы снизить дискретность, нам нужно отправлять от 2 до 5 запросов в секунду. Но если в Prometheus уменьшать интервал, нагрузка сразу же начинает неоправданно расти. Так что мы решили искать инструмент, который не увеличивал бы нагрузку и позволял бы отслеживать кратковременные всплески задержки, анализировать «хвосты» распределения и детализировать фазы сетевого запроса.

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

  1. Гибко задавать плотность измерений, не дожидаясь планового scrape.

  2. Запускать одновременную проверку нескольких endpoint.

  3. Изолировать друг от друга уже установленные и новые соединения.

  4. Отслеживать не только общую duration, но и отдельные этапы: DNS, connect, TLS, waiting.

  5. Преобразовывать сетевые ошибки в отдельные, легко анализируемые метрики.

  6. Выполнять проверки из разных локаций, чтобы оценить поведение сервиса в зависимости от site и region.

Стоит оговориться, что k6 не заменяет Blackbox Exporter и другие внешние системы мониторинга. Он более глубоко и детализированно мониторит API, что позволяет быстрее обнаруживать деградацию производительности и точно определять участок пути, на котором она возникает.

Сценарий мониторинга: конфигурация и параметры запуска

Разберем сценарий, с которым запускаем k6 для параллельной проверки доступности множества endpoint‑адресов. Привожу код полностью, потому что в статье буду на него ссылаться и детально разбирать.

import http from 'k6/http';
import { check } from 'k6';
import { textSummary } from './summary.js';

const RPS = Number(`${__ENV.RPS}`);
const timeUnit = 10;
const DISABLE_CONNECTION_REUSE = `${__ENV.DISABLE_CONNECTION_REUSE}` === 'true';

export const options = {
  noConnectionReuse: DISABLE_CONNECTION_REUSE,
  summaryTrendStats: ['avg', 'min', 'med', 'max', 'p(90)', 'p(95)', 'p(99)'],
  scenarios: {
    open_model: {
      executor: 'constant-arrival-rate',
      rate: timeUnit * RPS,
      timeUnit: `${timeUnit}s`,
      duration: '20m',
      preAllocatedVUs: 10,
      maxVUs: 10 + RPS,
    },
  },
};

const endpointEnv = `${__ENV.ENDPOINTS}`.split(',');

let endpoints = [];
for (let endpoint of endpointEnv) {
  endpoints.push(['GET', endpoint, null, { timeout: '15s' }]);
}

export default function () {
  const responses = http.batch(endpoints);
  responses.forEach(function (response) {
    check(response, {
      'status in range 200-399': (r) => r.status >= 200 && r.status <= 399,
    });
  });
}

export function handleSummary(data) {
  return {
    'stdout': textSummary(data, { indent: ' ', enableColors: false }),
  };
}

Основная функция — batch для пачки endpoint. Список конечных точек передается в сценарий через переменную окружения. Для каждого endpoint‑адреса формируется HTTP‑запрос, и все они выполняются одновременно с помощью http.batch().

Проверка охватывает не один, а сразу несколько endpoint. Это полезно в случаях, когда:

  • есть несколько url или endpoint в рамках одного сервиса,

  • существуют разные точки входа (ingress) маршрутизации трафика,

  • задействованы несколько региональных маршрутов,

  • одновременно используются endpoint продакшен и стейджа.

rate: задаем частоту. Вместо разовых запросов используем сценарий с контролируемой частотой выполнения. За счет этого:

  1. Получаем значимую выборку и перцентили p90, p95 и p99.

  2. Ловим кратковременные провалы, которые могли быть пропущены при редких проверках.

  3. Детально анализируем хвосты распределения задержек, что для API зачастую критичнее, чем среднее время ответа.

Чтобы чрезмерно не нагружать API синтетическими проверками, мы используем параметр rate. Он задается через переменную окружения RPS. Опытным путем выявили оптимальный диапазон rate: 2–5. После значения 6 мониторинг уже не давал ничего нового, но при этом излишне нагружал API.

executor и значение constant‑arrival‑rate: принуждаем следовать расписанию. В стандартном режиме k6 ждет завершения текущей итерации, прежде чем начать следующую. Это приводит к падению общего количества запросов при замедлении API. На практике клиенты обращаются к сервису не по расписанию и их действия не зависят от текущей производительности сервера. Чтобы имитировать реальное поведение пользователей, параметр rate следует использовать в сочетании с executor: 'constant‑arrival‑rate'. Это один из типов сценария в k6. С ним k6 выполняет тесты с заданной частотой, например 10 итераций в секунду. constant‑arrival‑rate отправляет новые запросы по графику, даже если предыдущие еще не завершены.

Помимо rate и executor есть еще два важных параметра настройки:

  1. timeUnit — единица времени, за которую запускается заданное rate. Например, 1s — секунда или 1m — минута.

  2. preAllocatedVUs — число виртуальных пользователей, подготовленных заранее для немедленного запуска. Помогает избежать накладных расходов в процессе выполнения.

noConnectionReuse: управляем повторным использованием соединений. Иногда важно понять, как ведет себя API для активного клиента, который уже подключен и продолжает слать запросы. Не менее полезно понимать, сколько стоит каждое новое соединение, чтобы выявлять проблемы, неочевидные для прогретого соединения, например медленный DNS или дорогой tls‑handshake. Для этого в k6 есть два режима работы, которые включаются с помощью параметра noConnectionReuse:

  1. В Keep‑Alive k6 повторно использует уже установленные соединения, чтобы оценить поведение API в условиях стабильной сессии с активным клиентом. Агент минимизирует выполнение некоторых сетевых этапов: DNS lookup, TCP connect, tls‑handshake. Это обеспечивает более стабильную и низкую задержку.

  2. В No Keep‑Alive k6 проходит полный сетевой цикл при каждом запросе. Это позволяет оценивать время, потраченное на каждый этап соединения.

Фрагмент сценария, который отвечает за включение и выключение режима:

const DISABLE_CONNECTION_REUSE = `${__ENV.DISABLE_CONNECTION_REUSE}` === 'true';

export const options = {
  noConnectionReuse: DISABLE_CONNECTION_REUSE,
};

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

Чтобы отключать внутренний DNS‑кеш при работе k6, можно использовать параметр запуска k6: ‑‑dns. Его возможные значения:

  1. ttl=0 отключает DNS‑кеш.

  2. select=random заставляет k6 равномерно распределять запросы между доступными вариантами IP. Полезен, если у имени хоста несколько IP‑адресов. 

  3. policy=preferIPv4 обеспечивает более стабильное сетевое поведение, если не требуется анализ IPv6.

Значения можно комбинировать, указывая через запятую.

Запускаем k6 с отключением DNS‑кеширования:

./k6 run \
 ... \
  --dns "ttl=0,select=random,policy=preferIPv4" \
  test.js

Отчет о работе k6: способ быстро оценить результаты

Итоговый отчет работы k6 зачастую воспринимают как дополнительный вывод в stdout. Однако в нашем случае отчет позволяет быстро оценить результаты локального запуска, сравнить распределения метрик между различными прогонами и убедиться, что проверка выполнена успешно.

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

Так выглядит стандартный вывод
HTTP
http_req_duration..............: avg=147.57ms min=134.98ms med=143.63ms max=168.03ms p(90)=163.12ms p(95)=165.58ms p(99)=167.54ms
  { expected_response:true }...: avg=147.57ms min=134.98ms med=143.63ms max=168.03ms p(90)=163.12ms p(95)=165.58ms p(99)=167.54ms
http_req_failed................: 0.00%  0 out of 4
http_reqs......................: 4      2.72493/s

EXECUTION
iteration_duration.............: avg=261.05ms min=243.98ms med=253.43ms max=293.38ms p(90)=283.61ms p(95)=288.49ms p(99)=292.4ms
iterations.....................: 4      2.72493/s
vus............................: 3      min=3      max=3
vus_max........................: 10     min=10     max=10

NETWORK
data_received..................: 1.4 MB 939 kB/s
data_sent......................: 13 kB  8.8 kB/s

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

import http from 'k6/http';
import { check } from 'k6';
import { textSummary } from './summary.js'; //подключаем

...
// используем
export function handleSummary(data) {
  return {
    'stdout': textSummary(data, { indent: ' ', enableColors: false }),
  };
}
Расширенный вывод выглядит вот так
checks.........................: 100.00% ✓ 78       ✗ 0
data_received..................: 22 MB   837 kB/s
data_sent......................: 197 kB  7.6 kB/s
dropped_iterations.............: 38      1.46353/s
http_req_blocked...............: avg=2.17s    min=93.45ms  med=2.53s    max=5.14s    p(90)=4.72s    p(95)=4.95s    p(99)=5.14s
http_req_connecting............: avg=36.06ms  min=23.36ms  med=36.29ms  max=77.97ms  p(90)=46.26ms  p(95)=46.99ms  p(99)=55.36ms
http_req_duration..............: avg=273ms    min=115.52ms med=311.35ms max=650.65ms p(90)=406.67ms p(95)=432.15ms p(99)=648.17ms
  { expected_response:true }...: avg=273ms    min=115.52ms med=311.35ms max=650.65ms p(90)=406.67ms p(95)=432.15ms p(99)=648.17ms
http_req_failed................: 0.00%   ✓ 0        ✗ 78
http_req_receiving.............: avg=175.33ms min=60.6ms   med=165.91ms max=398.92ms p(90)=291.66ms p(95)=335.85ms p(99)=375.34ms
http_req_sending...............: avg=578.44µs min=46.95µs  med=212.1µs  max=7.15ms   p(90)=1.2ms    p(95)=1.58ms   p(99)=5.42ms
http_req_tls_handshaking.......: avg=39.35ms  min=25.44ms  med=38.16ms  max=82.48ms  p(90)=47.55ms  p(95)=60.77ms  p(99)=71.39ms
http_req_waiting...............: avg=97.08ms  min=46.58ms  med=61.05ms  max=569.19ms p(90)=156.55ms p(95)=297.08ms p(99)=564.46ms
http_reqs......................: 78      3.004088/s
iteration_duration.............: avg=2.44s    min=209.29ms med=2.82s    max=5.61s    p(90)=4.97s    p(95)=5.3s     p(99)=5.55s
iterations.....................: 78      3.004088/s
vus............................: 9       min=1      max=15

В отличие от стандартного вывода, расширенный помогает ответить на ряд вопросов:

  1. Соблюдается ли заданная частота проверок?

  2. Где конкретно возникает задержка: в фазах blocked, connecting, tls_handshaking или waiting?

  3. Были ли пропущенные итерации?

  4. Насколько достоверны показатели p95 и p99?

Метрики: фазовый анализ запросов

Основная метрика, которую репортит k6, — http_req_duration. Это общее время, которое понадобилось агенту на выполнение запроса. Метрика глобальная и дает только общее представление о состоянии API. По одному лишь значению http_req_duration невозможно понять, на каком этапе запроса есть проблемы. Чтобы получить более детальную картину, мы стараемся собирать данные для каждой фазы запроса. Например, по увеличению http_req_tls_handshaking можно понять, что CPU забился на ноде. А большой http_req_waiting означает, что бэкенд долго отвечает.

Основные метрики для анализа ответов k6

Метрика

Описание

Проблемы, с которыми может быть связана

http_req_duration

Общее время выполнения запроса

Общее замедление

http_req_waiting

Время ожидания ответа после отправки запроса

Проблемы на уровне приложения или вышестоящих upstream зависимостей

http_req_connecting

Время установки TCP-соединения

Проблемы в сети, с Ingress, балансировщиками, firewall, NAT или состоянием точки проверки

http_req_tls_handshaking

Время tls-handshake

Проблемы с защищенным соединением

http_req_blocked

Время ожидания доступного соединения

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

dropped_iterations

Пропущенные итерации мониторинга

Агент не справляется с заданной частотой, возможно снижение качества измерений

http_req_failed

Стандартные для go‑стека код и описание сетевых ошибок

Ошибки в сети

Метрика http_req_failed охватывает широкий спектр проблем, связанных с сетью. Чаще всего мы сталкиваемся с такими:

  • явные DNS‑ошибки,

  • DNS timeout,

  • network timeout,

  • connection refused,

  • connection reset by peer.

С http_req_failed есть проблема, с которой мы столкнулись, когда настраивали k6. Метрика может «залипать»: на графиках создается видимость длительного сбоя больше 5 минут. Нашли похожее issue с решением и проверили его у себя — данные стали выводиться корректно.

sum(sum_over_time(k6_http_req_failed_rate[1m])) by (url, site, error)
/sum(count_over_time(k6_http_req_failed_rate[1m])) by (url, site, error) > 0

Метрики DNS‑запросов. Как бы мы ни настраивали k6, нам не удавалось получить детализированные метрики DNS‑запросов. Чтобы исправить ситуацию, мы применили расширение xk6-dns. Несмотря на его плюсы, метрики по процессу DNS‑резолва мы так и не получили:

  1. Не удалось запустить проверку перед HTTP‑запросом в том же режиме параллельного выполнения.

  2. Расширение xk6-dns версии 1.0.1 не регистрировало корректные метрики: их значение всегда оставалось равным 0 даже при отключенном кеше.

В итоге ориентируемся исключительно на сетевые ошибки, связанные с DNS, и следим за обновлениями расширения.

Метрика expected_response. В лейблах метрик k6 при выводе можно увидеть строку:

{ expected_response:true }...: avg=...

Это не пользовательская метрика, а отфильтрованный срез значений http_req_duration, который включает только те запросы, которые k6 признал успешными. В нашем сценарии успешным считается ответ со статусом в диапазоне 200–399 и не превысивший 15 секунд ожидания:

check(res, {
      'status in range 200-399': (r) => r.status >= 200 && r.status <= 399,
    });

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

Теги: обогащение метрик

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

  • точкам, откуда происходил запуск агента; 

  • особенностям окружения;

  • режимам соединений;

  • производительности внутри одного региона и между регионами.

Чтобы обогатить метрики дополнительной информацией, при запуске k6 используем теги. У нас такой набор:

  • site,

  • site_region,

  • endpoint_region,

  • endpoint_environment,

  • same_region,

  • connection_reuse.

На основе тегированных метрик можно создавать информативные дашборды и правила для оповещений.

Запуск k6 с тегами:

./k6 run \
 --tag site=${SITE} \
 --tag site_region=${SITE_REGION} \
 --tag endpoint_region=${endpoint_REGION} \
 --tag endpoint_environment=${endpoint_ENVIRONMENT} \
 --tag same_region=${SAME_REGION} \
 ... \
  test.js

Деплой и подготовка к запуску агента k6

Мы запускаем k6 из контейнера. Для этого в нем нужно объявить минимально необходимый набор переменных окружения:

Переменная

Описание

Пример значения

SITE

Название точки или региона запуска

{название точки/региона, откуда запускаем}

ENDPOINTS

Список URL‑адресов API для мониторинга, в одной строке через запятую

https://example.com,https://api.test.com

RPS

Желаемое количество запросов в секунду

5

DISABLE_CONNECTION_REUSE

Управление повторным использованием соединений

false

K6_PROMETHEUS_RW_SERVER_URL

URL‑адрес сервера Prometheus для Remote Write

{url prometheus для remote write}

Подробнее о переменных для отправки данных в Prometheus — в документации.

Финальный скрипт для запуска k6:

#!/bin/sh
set -ex

while true; do
    ./k6 run \
        --tag site=${SITE} \
        --tag прочие_теги=${из_значение} \
        --tag DISABLE_CONNECTION_REUSE=${DISABLE_CONNECTION_REUSE} \
        --env DISABLE_CONNECTION_REUSE=${DISABLE_CONNECTION_REUSE} \
        --env RPS=${RPS} \
        --env ENDPOINTS=${ENDPOINTS} \
        --dns "ttl=0,select=random,policy=preferIPv4" \
        --no-usage-report --no-color \
	  -o experimental-prometheus-rw \
        test.js
done

Разбор инцидентов: k6 на боевом дежурстве

Исключаем проблемы на стороне агента. У нас произошел инцидент, который мы поначалу приняли за деградацию удаленного сервиса. На дашборде высокого уровня начал расти процент ошибок. График показывал, что API недоступен. Одновременно регистрировались следующие типы отказов:

  • dial: connection refused;

  • dial: i/o timeout;

  • read: connection reset by peer.

Чтобы разобраться в причинах, использовали отладочный дашборд агента, который отображает:

  • количество пропущенных итераций (Dropped iterations),

  • загрузку виртуальных пользователей (VU),

  • интенсивность пропущенных итераций (Dropped iterations rate),

  • накопление сетевых ошибок по типам,

  • фактический RPS.

Рост Dropped iterations сигнализировал, что агент перестал справляться с заданной нагрузкой и остальные показатели «поехали» именно поэтому.

График показывает рост доли ошибок, типы сетевых отказов и состояние агента
График показывает рост доли ошибок, типы сетевых отказов и состояние агента
Отладочный дашборд агента
Отладочный дашборд агента

Подтверждаем проблему на стороне endpoint с помощью трех агентов. Мы запускаем агента k6 в трех разных локациях. Это помогает оперативно выявлять причины проблем. Когда агенты из разных точек одновременно сигнализируют о сбое, это означает, что проблема на стороне endpoint: в его Ingress, балансировщике, сетевом маршруте или среде размещения. А если о деградации сигнализирует только один из них, значит, проблема на стороне агента.

Через измерение error per site на дашборде видно, что ошибки для одного и того же endpoint поступают из нескольких точек наблюдения. Для мониторинга такой сигнал ценнее, чем одиночная проверка: мы можем быстрее разобраться в причинах возможной деградации.

Один и тот же endpoint деградирует сразу из нескольких точек проверки — проблема локализуется на стороне endpoint или его ближайшего сетевого окружения
Один и тот же endpoint деградирует сразу из нескольких точек проверки — проблема локализуется на стороне endpoint или его ближайшего сетевого окружения

Выявляем причину деградации на стороне endpoint по фазам запроса. Чтобы определить источник задержки, мы анализируем метрики для конкретного endpoint: 

  • duration,

  • waiting,

  • connecting,

  • tls_handshaking,

  • blocked,

  • sending,

  • receiving.

Рост waiting указывает на проблему, связанную с приложением, бэкендом или upstream‑сервисами. Рост connecting, tls_handshaking и blocked чаще всего означает проблемы в сети между агентом и endpoint.

Фазовый разбор задержки по одному endpoint: где именно формируется время на пути запроса
Фазовый разбор задержки по одному endpoint: где именно формируется время на пути запроса

Выделяем DNS‑инциденты из общего потока. Однажды прямо с утра дежурному прилетела пачка сообщений, что DNS‑записи резолвятся с ошибками. Сначала это было похоже на локальную проблему с резолвером или с отдельным узлом, на котором шли тесты. Быстрая проверка показала, что запись отдается, IP резолвится, а значит, проблема не на проде, не затрагивает клиентов и можно разбираться с ней, не торопясь.

Оказалось, нам просто «повезло»: быстрый тест попал на резолвер, где запись еще была в кеше и отдавалась корректно. Но когда сделали DNS‑трассировку, мы обнаружили, что в части случаев запись вообще не отдается. И значит, проблема на стороне внешнего DNS‑хостинга, а не на стороне нашей инфраструктуры. 

Мы в тот момент еще внедряли k6: алерты были отключены, но метрики уже собирались. И на дашбордах было видно, что проверки падают с разных площадок и ломаются не в приложении или внутренней сети, а именно на стадии DNS. 

В итоге благодаря k6 мы смогли вовремя разглядеть глобальную проблему в несерьезном, на первый взгляд, инциденте.

Сценарии, в которых k6 подходит для мониторинга API

k6 будет полезен, если нужно:

  1. Отслеживать не только среднее время ответа, но и «хвосты» распределения задержки: получать значения p95 и p99.

  2. Одновременно наблюдать за производительностью Production, Staging, резервного маршрута, а также внешних и внутренних адресов.

  3. Отдельно анализировать время, затраченное на этапы DNS, Connect, TLS.

  4. Быстро определять, является ли проблема локальной или она проявляется сразу из нескольких географических точек.

  5. Проверять временные затраты на установку нового соединения, когда невозможно отследить проблему на прогретых сессиях.

k6 показывает, как ведет себя сервис с точки зрения конечного клиента, и с его помощью наша SRE‑команда быстрее обнаруживает деградацию в API. Мы можем точнее локализовать источник проблемы и проанализировать сетевой путь запроса на уровне, который недоступен для стандартных внешних систем мониторинга.

Комментарии (2)


  1. DmitryStavtsev
    28.05.2026 13:45

    Очень интересный и не банальный материал и подход, прочёл с большим удовольствием


  1. plus887
    28.05.2026 13:45

    Спасибо! классная тема на самом деле. раскрыто хорошо, в закладки добавил ;)