The Jerusalem PostIran responsible for antisemitic terror attacks in Liège, Antwerp earlier this year - reportRTP DesportoMundiais Triatlo. Maria Tomé 20.ª nas Elites, Cassandre Beaugrand campeãESPN DeportesPor qué los Warriors reciben una "B" por la extensión de StephESPNRussell's win won't kickstart title challenge, but Baku rewards his faith in trying seasonPunch2027: Osun APC rallies electorate for candidates, says current hardship temporaryVanguard‘In 2027, protect your votes,’ PDP tells supportersTagesschauBerliner SPD will mit Linken und Grünen sondierenGlobal NewsTrump rejects Iran’s proposal to reopen the Strait of HormuzWirtualna PolskaNegocjacje USA-Rosja. Dotyczą broni opartej na AISRF NewsIsraelische Komikerin – «Auch wenn die Dinge hoffnungslos sind: Aufgeben geht nicht»Premium TimesWe are building strong foundations for agricultural transformation in Jigawa – NamadiSCMP ChinaBeijing says US, China to pursue military crisis-communication pact
The Daily Newsstand · Free, Always
Saturday, September 26, 2026

xk6-sip: мониторинг нагрузочного тестирования VoIP/SIP-звонков

Translate

Разберём, как устроена observability в xk6-sip — расширении нагрузочного инструмента k6, которое позволяет автоматизировать нагрузочное и функциональное тестирование VoIP/SIP-звонков: откуда берутся метрики, как из них строятся панели и как по ним отличить деградацию АТС от деградации самого генератора.

Все графики и цифры от реального прогона нагрузочного теста: 100 одновременных звонков, 10 минут, 2000 звонков, 6,1 млн RTP-пакетов. Дашборд — 37 панелей в шести блоках; для каждого блока ниже есть источник данных, запрос, измеренное значение и признаки проблемы.

Границы статьи. Все метрики ниже — клиентские: их собирает xk6-sip на стороне генератора трафика, и они показывают АТС так, как её видят абоненты, плюс состояние самого генератора. Мониторинг серверной стороны — ресурсов АТС, её SIP-стека, медиасерверов и зависимостей — статья не затрагивает. Клиентские метрики отвечают на вопрос «что сломалось и когда»; на вопрос «почему» отвечает серверная сторона, о ней — в конце.

Профиль и ожидаемые значения

Прогон запускается двумя командами. Первая поднимает тестовую АТС testpbx на loopback и заводит в ней 200 абонентов из CSV. Вторая запускает k6 с расширением xk6-sip: метрики уходят в Prometheus через remote write, а теги testid и pbx_version помечают прогон, чтобы дашборд показывал только его. Что именно делает сценарий и на каком стенде он шёл — в таблице под командами.

testpbx -addr 127.0.0.1:5070 -users 200 -csv examples/subscribers.csv

K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9091/api/v1/write \
K6_FEATURES=native-histograms \
k6 run -o experimental-prometheus-rw \
  --tag testid=load-100x10m-2 --tag pbx_version=testpbx-0.4 \
  -e VUS=100 -e HOLD=30 -e DURATION=10m \
  -e SIP_METRICS_ADDR=127.0.0.1:6566 examples/call.js

Параметр

Значение

Сценарий

A → B, проверка АОН, isHeard() в обе стороны, разговор 30 с + U(0, 1) с, BYE от B

Исполнитель

constant-vus, 100 VU; один VU — два абонента и один звонок за раз

Абоненты

200, digest-авторизация на REGISTER (401) и INVITE (407), Expires 300 с

Медиа

G.711 μ-law, 20 мс, 50 pps на плечо

SUT

testpbx (регистратор + B2BUA) на том же хосте, loopback

Генератор

Windows 11, 16 ГБ RAM, без изоляции от других нагрузок

Перед чтением дашборда считаем ожидаемые значения. По закону Литтла L = λW: при L = 100 и W ≈ 31 с (разговор + установление + пауза итерации) получаем λ ≈ 3,2 CAPS и ≈ 1930 звонков за 10 минут. RTP: 100 × 2 плеча × 50 pps = 10 000 pps, при 172 байтах полезной нагрузки UDP (12 байт заголовок RTP + 160 байт G.711) это 1,72 МБ/с в каждую сторону. Регистрации: 200 на старте + два обновления за 10 минут = 600.

Если дашборд на чистом прогоне воспроизводит эти числа, конвейер метрик корректен, и его показаниям можно доверять в прогоне с деградацией.

Архитектура сбора метрик

Три источника данных с разной моделью доставки. Разделение принципиальное: метрики теста описывают SUT глазами абонента, метрики процесса и хоста — состояние генератора. Без второго и третьего источника джиттер от голодающего по CPU генератора неотличим от джиттера АТС.

Источник

Модель

Что содержит

Включение

Метрики теста

push, remote write

sip_*, rtp_* с тегами testid, pbx_version, scenario

-o experimental-prometheus-rw

Процесс k6

pull, /metrics

process_*, go_*, xk6sip_network_*, xk6sip_calls_in_progress

sip.options({ metricsAddr })

Хост генератора

pull

CPU, память, сетевые интерфейсы

windows_exporter / node-exporter

Метрики теста идут по push: тест живёт конечное время, и pull-модель потеряла бы хвост после последнего scrape. Процесс и хост — долгоживущие цели, для них стандартный scrape раз в 5 с. Метрики процесса и хоста не несут testid, корреляция с прогоном — по времени.

Почему native histograms и счётчики

При экспорте в Prometheus k6 по умолчанию отдаёт готовые перцентили trend-метрик и доли rate-метрик, накопленные с начала теста. Из них нельзя получить значение за окно: 10 минут деградации после часа нормы почти не сдвигают накопленный p95. Поэтому:

  • Времена экспортируются как native histograms (K6_FEATURES=native-histograms) и считаются за скользящее окно:

histogram_quantile(0.95, sum(rate(k6_sip_call_setup_time_seconds{testid=~"$testid"}[$window])))

Native histogram — один ряд на комбинацию меток с экспоненциальными бакетами: границы не подбираются заранее, разрешение подстраивается под данные, и деградация до секунд не упирается в последний бакет, как у классической гистограммы с le. Из неё rate() + histogram_quantile() дают перцентиль за любое окно, а гистограммы нескольких генераторов корректно суммируются — готовые p95 разных инстансов k6 сложить нельзя. Встречающаяся в старых руководствах переменная K6_PROMETHEUS_RW_TREND_AS_NATIVE_HISTOGRAM в k6 v2 устарела, включает гистограммы K6_FEATURES=native-histograms. Prometheus 3.x принимает их с флагами --web.enable-remote-write-receiver и --enable-feature=native-histograms; без второго дашборд покажет пустые панели времён.

  • Доли строятся по счётчикам расширения: sip_call_results{result, status}, sip_calls{phase}, rtp_legs{heard}. Rate-метрики sip_call_success и rtp_audio_heard остаются для порогов в скрипте и итогового отчёта.

Известное ограничение: remote write в k6 отправляет счётчик только при изменении. Для серий с редкими всплесками (REGISTER на старте и при обновлении) на окно попадает одна точка, и rate() возвращает пустоту. Такие панели построены на накопленных значениях.

Ниже — снимки дашборда за окно прогона 15:19–15:30, $window = 1 минута.

Обзор: 12 индикаторов

Во время прогона первым делом смотрят на 12 плиток верхнего блока. Каждая отвечает на один вопрос: идёт ли нагрузка как задумано, проходят ли звонки, слышен ли голос, укладывается ли АТС в SLA. Если значения совпадают с ожидаемыми из раздела выше, в нижние блоки можно не спускаться; если какая-то плитка отклонилась, она подскажет, куда смотреть дальше — в сигнализацию, медиа или генератор. В таблице ниже для каждого индикатора — как он считается и что показал этот прогон.

Верхний блок — система раннего предупреждения. Все доли и перцентили считаются за $window — это переменная дашборда Grafana, длина скользящего окна (в этом прогоне 1 минута), а не весь тест. То есть каждая точка на панели показывает последнюю минуту: CAPS — сколько вызовов в секунду завершилось за эту минуту, p95 — по звонкам этой минуты.

Индикатор

Что это простыми словами

Определение

Прогон

CAPS

Сколько звонков в секунду завершается — фактическая интенсивность нагрузки

sum(rate(k6_sip_call_results_total[$window]))

3,1 (1,8–4,9)

Calls in progress

Сколько разговоров идёт прямо сейчас

sip_calls{phase="answered"} − sip_calls{phase="ended"}

100

ASR

Доля звонков, на которые ответили

answered / все попытки

100%

SEER (RFC 6076)

Доля звонков, которые АТС отработала без сбоя: «занято», «не отвечает» и отказ абонента тоже считаются успехом сети

(200 + 480 + 486 + 600 + 603) / попытки без 3xx

100%

One-way audio

Доля сторон разговора, до которых не дошёл голос собеседника

rtp_legs{heard="false"} / rtp_legs

0%

Dropped iterations

Сколько запланированных звонков генератор не успел запустить

k6_dropped_iterations_total

0

Setup time p95

За сколько соединяется звонок — от набора до ответа абонента; 95% звонков укладываются в это время

INVITE → финальный ответ

2,9 мс

Setup within SLA

Доля звонков, соединившихся быстрее порога SLA

histogram_fraction(0, $sla_setup, …)

100% при SLA 300 мс

PDD within SLA

Доля звонков, где пауза между набором номера и звонком у вызываемого (post-dial delay) короче порога SLA

histogram_fraction(0, $sla_pdd, …)

100% при SLA 2 с

ALOC

Средняя длительность разговора

histogram_sum / histogram_count по sip_call_duration

30,6 с

First response p95

Как быстро АТС подтверждает, что получила звонок; 95% звонков укладываются в это время

INVITE → первый ответ транзакции

1,1 мс

Retransmissions

Сколько раз в секунду сообщение SIP пришлось отправить повторно, потому что АТС не ответила вовремя

sum(rate(k6_sip_retransmissions_total[$window]))

0

ASR против SEER. ASR зависит от профиля трафика: «занято» и «не ответил» снижают его без вины АТС. SEER из RFC 6076 считает отказ абонента успехом сети и падает только от 404, 5xx и таймаутов. В этом прогоне оба по 100%. На контрольном прогоне с 5% звонков на несуществующий номер и 5% «занято» получилось ASR 84% и SEER 92%: разница — это 486, 404 остался в SEER как отказ маршрутизации. Пара «ASR падает, SEER стоит» указывает на профиль абонентов, пара «падают оба» — на АТС.

ALOC совпадает с заданным в скрипте 30 + E[U(0,1)] = 30,5 с (+0,1 с на BYE). Падение ALOC при постоянной нагрузке — прямой признак того, что SUT рвёт сессии (session timers, лимиты каналов, сбои RTP-прокси).

CAPS скачет от 1,8 до 4,9 при среднем 3,1. Это свойство закрытой модели constant-vus: 100 VU стартовали одновременно и завершают звонки волнами, которые расплываются случайной добавкой к длительности. Для поиска пропускной способности нужна открытая модель (constant-arrival-rate), где CAPS задан и не зависит от скорости ответа SUT.

Сигнализация

Панель

Метрика и точка измерения

Прогон

Calls per second by final status / by class

sip_call_results{result, status}, финальный ответ на INVITE; классы через label_replace

только 200

Call setup time

sip_call_setup_time: отправка INVITE → 200 OK, включая цикл 407 + повтор с credentials

p50 1,9 / p95 2,9 мс

Post-dial delay

sip_post_dial_delay: INVITE → первый 18x

p95 2,5 мс

INVITE routing time

sip_invite_delivery_time: отправка INVITE устройством A → получение устройством B

p95 1,9 мс

INVITE first response time

sip_invite_first_response_time: INVITE → первый ответ транзакции, по сэмплу на каждую транзакцию (407 и 100 Trying)

p95 1,1 мс

SIP retransmissions

sip_retransmissions{method, kind, status}

0

Failed calls by status

increase(...[$__range]) по status

—

Routing time — метрика, которую не получить генератором с разнесёнными UAC и UAS: обе отметки времени снимаются одним процессом, без рассинхрона часов. Это чистое время работы маршрутизации SUT.

Первый ответ и ретрансмиссии

По RFC 3261 (17.1.1.2) клиентская INVITE-транзакция по UDP повторяет запрос по таймеру A: через T1 = 500 мс, затем 1, 2, 4 с — до первого ответа или до таймаута B = 64·T1. Поэтому панель первого ответа идёт с порогом 500 мс: пока p99 ниже T1, АТС успевает подтверждать транзакции; выше — каждый медленный ответ удваивает входящий поток INVITE. Порог на setup time тут не годится: в него входит время до ответа абонента, а ретрансмиссии останавливает уже 100 Trying.

Ретрансмиссии выполняет транзакционный слой sipgo, а расширение их наблюдает: SIP-сокет устройства обёрнут, и повторная запись сообщения с тем же Via branch, CSeq и стартовой строкой считается ретрансмиссией. Считаются и запросы (kind="request"), и финальные ответы, не подтверждённые ACK (kind="response", таймер G).

Сигнатура перегрузки

В чистом прогоне её нет, но именно для неё блок и собран. Типичная последовательность на SIP/UDP:

  1. Растёт p99 setup time при стабильном p50 — в SUT появляется очередь.

  2. p99 первого ответа подходит к 500 мс.

  3. Появляются ретрансмиссии INVITE: фактическая нагрузка на SUT становится выше заданной.

  4. Появляются 503 / 408, падает SEER.

Шаги 1–3 дают потолок АТС раньше, чем он станет виден по отказам. На шаге 4 система уже в режиме лавинообразной перегрузки, и снижение CAPS её сразу не выводит.

Медиа

Успешная сигнализация не гарантирует голос: NAT, ошибки в SDP или исчерпание портов медиасервера дают звонок с 200 OK и тишиной в одну сторону. Поэтому каждый звонок генерирует RTP в обе стороны, а каждое плечо по окончании звонка сообщает, слышало ли оно другую сторону.

Панель

Метрика

Прогон

One-way audio

rtp_legs{heard="false"} / rtp_legs, по плечам

0 из 4000

Jitter p95 by leg

rtp_jitter, оценка interarrival jitter по RFC 3550 (6.4.1), по direction

0,6 мс на обоих плечах

RTP packet loss

rtp_packets_lost / (received + lost); потери — по разрывам sequence number

0

RTP packets per second

rtp_packets_sent, rtp_packets_received

9673 pps

Сверка. Теоретический максимум — 10 000 pps. Из 31,25 с итерации медиа идёт около 30,6 с (ALOC), то есть ~98% времени, что даёт ~9800 pps; измерено 9673. За тест отправлено 6 119 611 пакетов, принято 6 119 571. Счётчик потерь при этом нулевой: 40 пакетов находились в полёте в момент BYE и пришли после закрытия потока, разрывов последовательности не было.

Фазовый сдвиг. RTP-метрики публикуются по завершении плеча, и медиа-панели отстают от сигнализации на ALOC: в этом прогоне на ~30 с. Коррелируя всплеск джиттера с событием в сигнализации или на генераторе, сдвигайте медиа назад на длительность разговора.

Регистрации и сценарий

  • Регистрации идут всплесками, поэтому панель накопительная: 200 → 400 → 600, по ступеньке на старт и на каждое обновление при Expires 300 с. Сколько 401 — столько же 200: каждая регистрация проходит digest-цикл. p95 ответа на REGISTER — около 1 мс.

  • Failed requests by method — 0 из 7200 запросов. 401 и 407 не считаются ошибками, иначе доля «ошибок» была бы 36% на ровном месте: 2600 challenge-ответов на 1200 REGISTER, 4000 INVITE и 2000 BYE.

  • Failed expectations — 0 из 8000 проверок. Это метрика уровня сценария: call — звонок не пришёл или пришёл с чужим АОН, heard — нет звука, transferred — не прошёл перевод. Успешный 200 OK с чужим АОН виден только здесь.

  • Calls ending by who hung up — все remote: метрика пишется с стороны A, а BYE отправляет B. Аномалия — не сам remote, а расхождение со сценарием: появление error / timeout или падение ALOC.

Генератор

Нагрузочный тест невалиден, если узким местом стал генератор. Для VoIP это особенно коварно: при нехватке CPU планировщик RTP отправляет пакеты с опозданием, и приёмная сторона фиксирует это как джиттер SUT. Блок генератора нужен, чтобы валидировать сам тест.

Метрики процесса отдаёт само расширение на metricsAddr: стандартные process- и Go-коллекторы client_golang плюс собственные счётчики трафика на уровне сокетов SIP и RTP.

Панель

Что показывает

Метрика

Прогон

Dropped iterations

Сколько звонков генератор не успел запустить по плану

k6_dropped_iterations_total

0

k6 process CPU

Насколько k6 загружает процессор

rate(process_cpu_seconds_total{job="k6"})

55% ядра (максимум 63%)

Goroutines

Число параллельных задач внутри k6; рост без роста нагрузки — утечка

go_goroutines

~530, стабильно

k6 process memory

Сколько памяти занимает k6

process_resident_memory_bytes, go_memstats_heap_inuse_bytes

RSS ~100 МБ, heap 38–61 МБ

k6 process network

Сколько трафика k6 отправляет и получает: голос (RTP) и сигнализация (SIP)

rate(xk6sip_network_bytes_total) по proto, direction

RTP 1,65–1,70 МБ/с, SIP ~12 КБ/с

Machine CPU / memory

Загрузка процессора и памяти всего компьютера-генератора

windows_exporter

11% CPU, 11,2 из 16 ГБ

Стабильность. Число горутин и RSS выходят на плато после разгона и не растут с числом завершённых звонков, а heap идёт пилой сборок мусора без тренда — утечек горутин и памяти на звонок нет. Для soak-тестов это первое, что нужно проверить.

Сверка трафика. Расчёт — 1,72 МБ/с полезной нагрузки UDP в каждую сторону, измерено 1,65–1,70. rtp in и rtp out совпадают, потому что оба плеча звонка обслуживает один процесс. SIP — меньше 1% трафика.

Loopback против NIC. SUT работал на 127.0.0.1, поэтому «Machine network» теста не видит: loopback-трафик не проходит через сетевой интерфейс, и экспортёр хоста его не считает. Счётчики на сокетах видят всё. При удалённой SUT обе панели совпадут с точностью до заголовков IP/UDP (28 байт на пакет, +16% для G.711 по 20 мс). Расхождение больше этого — сторонний трафик на генераторе.

CPU. 55% ядра на 200 RTP-потоков и 3 CAPS. Это выше, чем в изолированном бенчмарке расширения (62% на 1000 потоков на Linux), и расхождение объяснимо: Windows, SUT и генератор на одном хосте, плюс remote write и экспорт метрик самого процесса. Вывод — метрики производительности генератора нужно снимать на целевой платформе, а не переносить с бенчмарка.

Память хоста. Первая попытка этого прогона была прервана на 4,5 минуте из-за нехватки RAM на машине при RSS k6 около 100 МБ. Именно такой случай и разделяют панели процесса и хоста: процесс стабилен, хост у потолка. В продакшен-испытаниях генератор должен стоять на выделенном хосте.

Итоги

Конвейер метрик воспроизводит расчётные значения по всем осям:

Величина

Расчёт

Измерено

CAPS

L/W = 100 / 31 ≈ 3,2

3,1

Звонков за прогон

≈ 1930 + довершение итераций в graceful stop

2000

Звонков в разговоре

100

98,9 в среднем

ALOC

30,5 с

30,6 с

RTP pps

~9800 с учётом скважности

9673

RTP-трафик на сторону

1,72 МБ/с

1,65–1,70 МБ/с

REGISTER 200 OK

200 × 3

600

Отказы, потери, one-way audio, ретрансмиссии

0

0

Пороги сценария: sip_call_success 100% (порог > 99%), rtp_audio_heard 100% (> 99%), sip_call_setup_time p95 2,87 мс (< 500 мс).

Миллисекундные времена — свойство тестовой SUT на loopback, а не характеристика продакшен-АТС. Кроме того, на Windows гранулярность часов Go около 0,5 мс, поэтому субмиллисекундные перцентили здесь ориентировочные. Ценность прогона — в валидации инструментов измерения перед тестом настоящей системы.

Воспроизведение

git clone https://github.com/Dmitry-Fedotov-Dev/xk6-sip && cd xk6-sip
xk6 build v2.3.0 --with github.com/Dmitry-Fedotov-Dev/xk6-sip=. --output k6

docker compose -f monitoring/docker-compose.yml up -d        # Linux: --profile linux-host
go run ./cmd/testpbx -addr 127.0.0.1:5070 -users 200 -csv examples/subscribers.csv &

K6_PROMETHEUS_RW_SERVER_URL=http://localhost:9091/api/v1/write K6_FEATURES=native-histograms \
./k6 run -o experimental-prometheus-rw --tag testid=my-run --tag pbx_version=<версия> \
  -e VUS=100 -e HOLD=30 -e DURATION=10m -e SIP_METRICS_ADDR=127.0.0.1:6566 examples/call.js

Grafana — http://localhost:3001, Prometheus — http://localhost:9091. На Windows метрики хоста отдаёт windows_exporter с --collectors.enabled=cpu,memory,net,os,system --web.listen-address=127.0.0.1:9182. Для своей АТС достаточно CSV с её абонентами вместо сгенерированного testpbx.

Дашборд лежит в репозитории как JSON — monitoring/grafana/dashboards/xk6-sip.json — и подключается provisioning'ом автоматически. В существующую Grafana его можно импортировать как есть; нужен источник данных Prometheus с uid prometheus.

Вторая половина картины: серверная сторона

Всё, что выше, — взгляд снаружи. Генератор видит, что setup p99 вырос втрое, но не видит, почему. Причина живёт на АТС, и для неё нужен свой набор метрик, снятый в той же шкале времени и с той же меткой прогона:

Клиент (xk6-sip) видит

Что смотреть на АТС

Первый ответ на INVITE растёт к 500 мс, появляются ретрансмиссии

Очередь входящих SIP-сообщений и транзакций, загрузка потоков SIP-стека, потери UDP в буфере сокета

Setup p99 растёт при ровном p50

CPU по ядрам, время ответа БД и внешних сервисов маршрутизации, паузы GC

Звонков в разговоре меньше, чем answered − ended

Число активных каналов и диалогов, лимиты лицензий и транков

One-way audio при зелёном ASR

Сессии и порты RTP-прокси или медиасервера, NAT, использование транскодинга

Растёт доля remote среди завершивших

Session timers, таймауты RTP на медиасервере, рестарты процессов

503 и 480 под нагрузкой

Срабатывание защиты от перегрузки, пределы пула каналов, CPS-лимитеры

Правило одно: клиентская метрика говорит, что и когда сломалось, серверная — почему. Если оба набора лежат в одном Prometheus с общим testid и pbx_version, по одному клику видно, какой ресурс АТС упёрся в тот момент, когда на генераторе поплыла сигнализация, — и ёмкость новой версии снимается не как «держит 30 CAPS», а как «держит 30 CAPS, упирается в CPU SIP-стека».

Серверный мониторинг сильно зависит от конкретной АТС (Asterisk, FreeSWITCH, Kamailio/OpenSIPS, проприетарные B2BUA), поэтому здесь он только очерчен. Если вы строите нагрузочное тестирование своей телефонии и хотите связать метрики генератора с метриками сервера в один дашборд — пишите, это как раз то, чем я занимаюсь.

Следующий шаг

Закрытая модель с фиксированным числом VU проверяет устойчивость на известной нагрузке, но не находит потолок. По закону Литтла при фиксированном L = 100 рост W сам снижает λ: если АТС начнёт отвечать на INVITE за 2 с вместо 3 мс, VU дольше ждут, и CAPS падает — генератор подстраивается под скорость SUT. ASR и SEER при этом могут оставаться зелёными: звонков меньше, но проходят все.

Та же синхронизация искажает времена. Пока VU ждёт медленный звонок, он не начинает следующий, и звонки, которые пришлись бы на худший момент, просто не случаются и не попадают в перцентили. Это coordinated omission: p99 на графике лучше, чем увидели бы реальные абоненты, — они друг друга не ждут. На дашборде это видно как просадка CAPS при неизменном числе VU и calls in progress, равном числу VU.

Для поиска пропускной способности нужна открытая модель: ramping-arrival-rate ступенями с остановкой по порогу (abortOnFail). В ней CAPS задаёт генератор, и при деградации SUT растёт не пауза, а число одновременных звонков (L = λW) — пока не кончатся preAllocatedVUs (dropped iterations) или каналы АТС. Именно в таком прогоне сработают панели, которые здесь остались пустыми: коды отказов, первый ответ у T1, ретрансмиссии, падение SEER и ALOC.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.