API Mindbox обрабатывает тысячи запросов в секунду, а мы публично гарантируем клиентам бесперебойный круглосуточный сервис. Чтобы выполнять свои обязательства, нам нужен непрерывный поток телеметрии, близкий к тому, что создают клиенты: со всеми деградациями, ошибками и распределением летенси.
На связи Дмитрий Рыбалка, SRE‑инженер Mindbox. В этой статье расскажу, как и почему мы используем k6, чтобы мониторить наш API.
k6 aka агент мониторинга: плотный поток телеметрии
Для внешнего мониторинга нашей сетевой инфраструктуры мы используем Blackbox Exporter. В целом со своими задачами он справляется: простой, понятный и покрывает большинство сценариев. Однако у него есть ограничение: новое значение метрики появляется только при запросе со стороны Prometheus, и обычно это происходит не чаще, чем раз в 15 секунд. При такой частоте запрос может отправиться в момент, когда простаивает CPU или когда очередь обработки на сервере пустая: деградация производительности уже произошла, и мониторинг ничего не зафиксировал.
Чтобы снизить дискретность, нам нужно отправлять от 2 до 5 запросов в секунду. Но если в Prometheus уменьшать интервал, нагрузка сразу же начинает неоправданно расти. Так что мы решили искать инструмент, который не увеличивал бы нагрузку и позволял бы отслеживать кратковременные всплески задержки, анализировать «хвосты» распределения и детализировать фазы сетевого запроса.
Неожиданно оказалось, что этим требованиям отвечает k6, известный как инструмент для нагрузочного тестирования. Несмотря на это амплуа, мы попробовали использовать его как управляемого агента для мониторинга нашего API. В итоге обнаружили ряд преимуществ в сравнении с обычными инструментами мониторинга. k6 позволяет:
Гибко задавать плотность измерений, не дожидаясь планового scrape.
Запускать одновременную проверку нескольких endpoint.
Изолировать друг от друга уже установленные и новые соединения.
Отслеживать не только общую duration, но и отдельные этапы: DNS, connect, TLS, waiting.
Преобразовывать сетевые ошибки в отдельные, легко анализируемые метрики.
Выполнять проверки из разных локаций, чтобы оценить поведение сервиса в зависимости от 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: задаем частоту. Вместо разовых запросов используем сценарий с контролируемой частотой выполнения. За счет этого:
Получаем значимую выборку и перцентили p90, p95 и p99.
Ловим кратковременные провалы, которые могли быть пропущены при редких проверках.
Детально анализируем хвосты распределения задержек, что для 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 есть еще два важных параметра настройки:
timeUnit — единица времени, за которую запускается заданное rate. Например, 1s — секунда или 1m — минута.
preAllocatedVUs — число виртуальных пользователей, подготовленных заранее для немедленного запуска. Помогает избежать накладных расходов в процессе выполнения.
noConnectionReuse: управляем повторным использованием соединений. Иногда важно понять, как ведет себя API для активного клиента, который уже подключен и продолжает слать запросы. Не менее полезно понимать, сколько стоит каждое новое соединение, чтобы выявлять проблемы, неочевидные для прогретого соединения, например медленный DNS или дорогой tls‑handshake. Для этого в k6 есть два режима работы, которые включаются с помощью параметра noConnectionReuse:
В Keep‑Alive k6 повторно использует уже установленные соединения, чтобы оценить поведение API в условиях стабильной сессии с активным клиентом. Агент минимизирует выполнение некоторых сетевых этапов: DNS lookup, TCP connect, tls‑handshake. Это обеспечивает более стабильную и низкую задержку.
В 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. Его возможные значения:
ttl=0 отключает DNS‑кеш.
select=random заставляет k6 равномерно распределять запросы между доступными вариантами IP. Полезен, если у имени хоста несколько IP‑адресов.
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
В отличие от стандартного вывода, расширенный помогает ответить на ряд вопросов:
Соблюдается ли заданная частота проверок?
Где конкретно возникает задержка: в фазах blocked, connecting, tls_handshaking или waiting?
Были ли пропущенные итерации?
Насколько достоверны показатели 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‑резолва мы так и не получили:
Не удалось запустить проверку перед HTTP‑запросом в том же режиме параллельного выполнения.
Расширение 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:
duration,
waiting,
connecting,
tls_handshaking,
blocked,
sending,
receiving.
Рост waiting указывает на проблему, связанную с приложением, бэкендом или upstream‑сервисами. Рост connecting, tls_handshaking и blocked чаще всего означает проблемы в сети между агентом и endpoint.

Выделяем DNS‑инциденты из общего потока. Однажды прямо с утра дежурному прилетела пачка сообщений, что DNS‑записи резолвятся с ошибками. Сначала это было похоже на локальную проблему с резолвером или с отдельным узлом, на котором шли тесты. Быстрая проверка показала, что запись отдается, IP резолвится, а значит, проблема не на проде, не затрагивает клиентов и можно разбираться с ней, не торопясь.
Оказалось, нам просто «повезло»: быстрый тест попал на резолвер, где запись еще была в кеше и отдавалась корректно. Но когда сделали DNS‑трассировку, мы обнаружили, что в части случаев запись вообще не отдается. И значит, проблема на стороне внешнего DNS‑хостинга, а не на стороне нашей инфраструктуры.
Мы в тот момент еще внедряли k6: алерты были отключены, но метрики уже собирались. И на дашбордах было видно, что проверки падают с разных площадок и ломаются не в приложении или внутренней сети, а именно на стадии DNS.
В итоге благодаря k6 мы смогли вовремя разглядеть глобальную проблему в несерьезном, на первый взгляд, инциденте.
Сценарии, в которых k6 подходит для мониторинга API
k6 будет полезен, если нужно:
Отслеживать не только среднее время ответа, но и «хвосты» распределения задержки: получать значения p95 и p99.
Одновременно наблюдать за производительностью Production, Staging, резервного маршрута, а также внешних и внутренних адресов.
Отдельно анализировать время, затраченное на этапы DNS, Connect, TLS.
Быстро определять, является ли проблема локальной или она проявляется сразу из нескольких географических точек.
Проверять временные затраты на установку нового соединения, когда невозможно отследить проблему на прогретых сессиях.
k6 показывает, как ведет себя сервис с точки зрения конечного клиента, и с его помощью наша SRE‑команда быстрее обнаруживает деградацию в API. Мы можем точнее локализовать источник проблемы и проанализировать сетевой путь запроса на уровне, который недоступен для стандартных внешних систем мониторинга.
DmitryStavtsev
Очень интересный и не банальный материал и подход, прочёл с большим удовольствием