ESPNBottom 10: Time not on Michigan's side in this onePunchIndependence: FG declares Thursday public holidayDaily MaverickFOUL PLAY: Manchester City’s ‘sham’ deals leave Premier League in uncharted territoryThe Jerusalem PostSohlberg sets Central Elections Committee hearing on Likud petition against Fly&Vote initiativeBollywood HungamaWho is Lalit Prabhakar? Meet the National Award-winning actor playing Ajmal Kasab in PrahaarInquirerRidon’s tip to defense: Don’t remove Poa, he’s the best you haveHong Kong Free PressEx-husband, in-laws accused of murdering Hong Kong model appear in courtGuardian SportSouth Africa v Australia: third men’s one-day international – liveBBC NewsGreggs to shut four factories and cut 740 jobsالشرقطائرة فلاي دبي.. إصابة الطيار ومساعده وتحقيقات لمعرفة ما جرى على متنهاVilaWeb[INTERACTIU] Què canvia el decret d’habitatge si sou llogaters o propietaris?La PresseLongueuil | Un conducteur de 17 ans percute une résidence en fuyant les policiers
The Daily Newsstand · Free, Always
Wednesday, September 30, 2026

xk6-sip: нагрузочное тестирование в CI — автоматизация с нуля до отчёта

Translate

Разберём, как настроить нагрузочное тестирование с нуля и встроить его в CI на примере xk6-sip — расширения нагрузочного инструмента k6 для VoIP/SIP-телефонии. На каждый push в GitHub Actions идут шесть функциональных звонковых сценариев и двухминутный нагрузочный прогон с порогами качества, а на странице прогона появляется сводка: каждый сценарий и каждый шаг, ключевые метрики нагрузки против порогов. В артефактах остаются JUnit-отчёты, WAV проваленных проверок звука и скриншот дашборда Grafana за окно теста.

Это четвёртая статья серии. В первой мы разобрали, как описывать звонки как код и как устроен движок xk6-sip, во второй — настроили мониторинг нагрузки на Prometheus и Grafana. Здесь объединяем их в CI-пайплайн на GitHub Actions, который проверяет АТС на каждом коммите.

Для VoIP/SIP-телефонии автоматизация тестирования в CI исторически давалась тяжело. Классический SIPp описывает сценарии в XML и не отдаёт ни метрики в Prometheus, ни JUnit-отчёт, поэтому в пайплайне его приходится обвязывать скриптами. В xk6-sip сценарий — обычный скрипт k6, а пороги, JUnit и экспорт метрик — стандартные возможности k6.

Нагрузочное тестирование часто живёт отдельно от разработки: раз в релиз инженер вручную запускает прогон, смотрит на графики и пишет отчёт. Деградация при этом обнаруживается через недели после коммита, который её принёс. Автоматизация нагрузочного тестирования в CI сокращает этот срок до минут: пороги производительности становятся quality gates, как юнит-тесты.

Всё ниже — из реального пайплайна репозитория xk6-sip: функциональные тесты занимают 40 секунд, нагрузка с мониторингом — 3,5 минуты, 401 звонок и 2 005 проверок за прогон.

В статье:

  1. Архитектура пайплайна: что запускается на раннере.

  2. Как настроить нагрузочное тестирование с нуля: три уровня проверок.

  3. Функциональные звонковые тесты на каждый push.

  4. Нагрузочный прогон в CI и пороги как quality gates.

  5. Отчёты: сводка прогона на странице GitHub, JUnit и дашборд за окно прогона.

  6. Грабли, на которые мы наступили.

  7. Что дальше: реальная АТС, запуски CI по расписанию и сравнение с базовой линией.

Архитектура пайплайна

На каждый push и pull request рядом с юнит-тестами, сборкой и линтером параллельно идут два job со звонками на обычных раннерах GitHub Actions, без внешних стендов: всё — и тестируемая АТС, и мониторинг — поднимается на самом раннере и исчезает вместе с ним.

Каждый push: звонковые сценарии и нагрузка с отчётом

Каждый push: звонковые сценарии и нагрузка с отчётом

Результат сборки решают коды выхода k6: проваленная проверка или порог делают job красным, а артефакты сохраняются в любом случае.

Как настроить нагрузочное тестирование с нуля

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

Уровень

Когда

Длится

Что ловит

Функциональные сценарии звонков

каждый push и PR

секунды

сломанные call flow: удержание, переводы, отказы, звук

Короткая нагрузка (smoke-performance)

каждый push и PR

минуты

грубую деградацию, утечки, рассинхрон сигнализации и медиа

Полноценная нагрузка: поиск предела, стабильность

по расписанию или перед релизом

часы

потолок CAPS, деградацию между версиями АТС

Первые два уровня живут в обычном CI на общих раннерах. Третий требует выделенного генератора и стенда, и о нём — в конце. Для старта нужны четыре вещи:

  1. Генератор нагрузки, который собирается и запускается одной командой. Здесь это k6 с расширением xk6-sip: xk6 build собирает один бинарник без внешних зависимостей.

  2. Тестируемая система внутри CI. Для расширения это тестовая АТС testpbx: регистратор и B2BUA на Go с digest-авторизацией, удержанием и переводами. Для настоящей АТС — её стенд или контейнер (Asterisk, FreeSWITCH).

  3. Пороги в скрипте. k6 проверяет их сам и выходит с ненулевым кодом, если порог нарушен. Это и есть quality gate: никакой отдельной проверки поверх не нужно.

  4. Мониторинг и отчёт. Prometheus и Grafana в Docker прямо на раннере и скрипт, который после прогона снимает дашборд за окно теста. Без отчёта красная сборка говорит «стало хуже», но не говорит, что и когда.

Функциональные звонковые тесты на каждый push

Шесть сценариев проходят за 40 секунд и покрывают основные call flow АТС. Каждый — один VU и одна итерация, потому что здесь проверяется логика, а не объём.

Сценарий

Что проверяет

basic-call.js

звонок, гудки, ответ, звук в обе стороны, DTMF, кто положил трубку

hold.js

удержание и возврат re-INVITE: звук пропадает и возвращается

blind-transfer.js

слепой перевод REFER

attended-transfer.js

сопровождаемый перевод с Replaces

audio-quality.js

каждая сторона чисто слышит фразу собеседника и не слышит себя

reject-cancel.js

отказ (486), отмена во время вызова (487), несуществующий номер (404)

Сценарий — это обычный скрипт k6, который читается как тест-кейс. Вот отказ и отмена целиком:

export default function () {
  // B занят
  let out = step('A calls B', A.call({ callee: B }));
  let inc = step('B gets the call', B.expectCall({ caller: A, timeout: '10s' }));
  inc.reject(486, 'Busy Here');
  step('A: busy call ended', out.expectDisconnected('10s'));
  step(`A gets 486 (got ${out.status()})`, out.status() === 486);

  // A кладёт трубку, пока у B звонит
  out = step('A calls B again', A.call({ callee: B }));
  inc = step('B gets the second call', B.expectCall({ caller: A, timeout: '10s' }));
  step('A hears ringing', out.expectRinging('10s'));
  out.hangup();
  step('B: call cancelled', inc.expectDisconnected('10s'));
  step(`B: ended with 487 (got ${inc.status()})`, inc.status() === 487);

  // номера не существует
  out = step('A calls 1999', A.call({ callee: '1999' }));
  step('A: call to unknown number ended', out.expectDisconnected('10s'));
  step(`A gets 404 (got ${out.status()})`, out.status() === 404);
}

Три принципа, на которых это держится:

  • step() останавливает сценарий на первом провале. Если B не получил звонок, проверять статус бессмысленно, и отчёт показывает одну настоящую причину вместо каскада вторичных ошибок.

  • Фактическое значение — в имени проверки. В JUnit и в итогах k6 видно «A gets 404 (got 480)», и лог открывать не нужно.

  • Порог checks: ['rate==1'] делает любую проваленную проверку падением k6 с ненулевым кодом, а handleSummary() пишет JUnit-отчёт для CI.

Job в GitHub Actions собирает k6 и testpbx, поднимает АТС и гоняет сценарии по очереди. Один упавший сценарий не останавливает остальные: в конце видно все сломанные сразу.

  functional:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-go@v7
        with:
          go-version: stable
      - name: build k6 and testpbx
        run: |
          go install go.k6.io/xk6@latest
          mkdir -p bin
          xk6 build v2.3.0 --with github.com/Dmitry-Fedotov-Dev/xk6-sip=. --output bin/k6
          go build -o bin/testpbx ./cmd/testpbx
      - name: run scenarios
        run: |
          ./bin/testpbx -addr 127.0.0.1:5070 -users 3 > testpbx.log 2>&1 &
          sleep 1
          mkdir -p reports
          cd examples/functional
          failed=0
          for s in basic-call hold blind-transfer attended-transfer audio-quality reject-cancel; do
            echo "::group::$s"
            if ! ../../bin/k6 run -q -e JUNIT="../../reports/$s.xml" -e RECORD_DIR=../../reports/recordings "$s.js"; then
              echo "::error title=functional::$s failed"
              failed=1
            fi
            echo "::endgroup::"
          done
          exit $failed
      - name: keep reports
        if: always()
        uses: actions/upload-artifact@v7
        with:
          name: functional-reports
          path: |
            reports/
            testpbx.log

В артефактах остаются JUnit по каждому сценарию, лог АТС и WAV-записи звонков, где провалилась проверка звука: их можно послушать.

Нагрузочный прогон в CI

На каждый push идёт двухминутная нагрузка: 20 виртуальных пользователей, каждый ведёт пару абонентов и звонит по кругу с разговором 5–6 секунд. Профиль намеренно маленький: задача этого уровня — поймать грубую деградацию и рассинхрон, а не найти потолок.

Каждый звонок проверяется полностью: B получил звонок с верным АОН, соединение установилось, звук идёт, B слышит фразу A без искажений (compareAudio()), звонок завершился. Над этим — пороги, которые и решают судьбу сборки:

export const options = {
  scenarios: { calls: { executor: 'constant-vus', vus: 20, duration: '2m' } },
  thresholds: {
    sip_call_success: ['rate>0.99'],       // звонки соединяются
    sip_call_setup_time: ['p(95)<500'],    // и быстро
    rtp_audio_heard: ['rate>0.99'],        // без one-way audio
    rtp_audio_score: ['p(95)>0.9'],        // и без искажений
  },
};

Результаты последнего прогона на общем раннере GitHub Actions:

Метрика

Значение

Порог

Звонков

401, в среднем 3,15 CAPS

—

Успешных

100%

> 99%

Setup time p95

1,05 мс

< 500 мс

Post-dial delay p95

0,90 мс

—

Первый ответ на INVITE p95

0,35 мс

—

Джиттер p95

0,52 мс

—

RTP-пакетов принято / потеряно

224 107 / 0

—

Оценка звука p95

0,993

> 0,9

Проверок

2 005 из 2 005

—

Сверка по закону Литтла L = λW: 20 VU, одна итерация — это установление звонка, 5,6 с разговора и 0,5 с паузы, около 6,3 с. Отсюда 20 / 6,3 ≈ 3,2 CAPS против измеренных 3,15: генератор дал ровно ту нагрузку, которую заказали.

Почему порог setup time — 500 мс при фактических 1 мс. У общих раннеров CI шумные соседи: виртуальная машина делит CPU с чужими сборками, и тайминги плавают в разы. Порог на уровне таймера SIP T1 = 500 мс ловит настоящую поломку (АТС отвечает так медленно, что клиент уже повторяет запрос) и не краснеет от случайных скачков. Точные сравнения миллисекунд — дело выделенного стенда. Правило: на общих раннерах пороги проверяют корректность и грубую деградацию, а не точные цифры.

В job нагрузки три шага: поднять мониторинг и АТС, прогнать k6, снять отчёт:

      - name: start monitoring and the test PBX
        run: |
          docker compose -f monitoring/docker-compose.yml --profile linux-host up -d
          ./bin/testpbx -addr 127.0.0.1:5070 -users 200 -csv examples/subscribers.csv > reports/testpbx.log 2>&1 &
          timeout 60 bash -c 'until curl -sf http://localhost:3001/api/health >/dev/null; do sleep 2; done'
      - name: load test
        env:
          K6_PROMETHEUS_RW_SERVER_URL: http://localhost:9091/api/v1/write
          K6_FEATURES: native-histograms
        run: |
          echo "START=$(date +%s)" >> "$GITHUB_ENV"
          ./bin/k6 run -o experimental-prometheus-rw --tag testid="$TESTID" --tag pbx_version=testpbx \
            -e VUS=20 -e DURATION=2m -e HOLD=5 -e AUDIO=1 -e SIP_METRICS_ADDR=0.0.0.0:6566 \
            --summary-export reports/summary.json examples/call.js | tee reports/k6.txt
      - name: dashboard report
        if: always()
        run: |
          sleep 15 # the last remote write and scrape
          bash monitoring/report.sh "$TESTID" $((START - 30)) $(($(date +%s) - 10)) reports/dashboard.png

TESTID равен ci-<номер сборки>: по нему прогон находится на дашборде, а если мониторинг общий для нескольких сборок, прогоны не смешиваются. Метрики теста k6 отдаёт через remote write, а метрики процесса k6 и машины Prometheus забирает сам; как это устроено, подробно разобрано в статье про мониторинг.

Отчёты: сводка прогона, JUnit и дашборд

Так выглядит зелёный прогон в GitHub Actions: пять job за 3,5 минуты, нагрузочный — самый долгий. Отчёты лежат в двух артефактах сборки: functional-reports и load-report.

Успешный прогон CI в GitHub Actions: test, build, functional, load, lint

Успешный прогон CI в GitHub Actions: test, build, functional, load, lint

Сводка на странице прогона

Первое, что видно после сборки, — не артефакты, а сводка прямо на странице прогона. Скачивать zip, чтобы узнать «что упало», — лишний шаг, и его никто не делает. GitHub Actions даёт для этого файл $GITHUB_STEP_SUMMARY: всё, что job записал туда в Markdown, показывается на вкладке Summary.

У обоих job со звонками последний шаг — summary с if: always(): он работает и на красной сборке, когда сводка нужнее всего. Короткий скрипт на Python без зависимостей, .github/scripts/summary.py, собирает её из того, что k6 уже написал: из JUnit-отчётов сценариев и из summary.json нагрузки.

Функциональные тесты — одна строка на сценарий: результат, число шагов и упавший шаг.

Под таблицей — шаги каждого сценария в свёрнутых блоках <details>. У прошедшего сценария блок свёрнут, у упавшего раскрыт сразу и показывает, что успело пройти до поломки и где именно сломалось. Вот как выглядит сводка, если задрать порог оценки звука до 0,999 (вывод скрипта на настоящем провале):

Scenario

Result

Steps

Failed step

basic-call

✅

9

audio-quality

❌

6 of 7 passed

B hears A's phrase clearly (score 0.993 >= 0.999)

Step

✅

A calls B

✅

B gets the call from A

✅

A connected

✅

B hears A

✅

A hears B

✅

B audio compared

❌

B hears A's phrase clearly (score 0.993 >= 0.999)

❌

threshold checks: rate==1

Звонок соединился, звук идёт в обе стороны, а фраза A дошла с оценкой 0,993 при требовании 0,999. Всё это видно без лога и без артефактов. В логе job печатается та же сводка, но с шагами только упавших сценариев, чтобы она не тонула в зелёных строках.

Код шага в job функциональных тестов — три строки:

      - name: summary
        if: always()
        run: |
          python3 .github/scripts/summary.py functional reports $SCENARIOS
          python3 .github/scripts/summary.py functional --all-steps reports $SCENARIOS > reports/summary.md
          cat reports/summary.md >> "$GITHUB_STEP_SUMMARY"

Первая строка печатает короткую сводку в лог, вторая сохраняет полную в артефакт, третья выводит её на страницу прогона.

Нагрузка. Здесь сводка отвечает на два вопроса сразу: прошли ли пороги и насколько далеко метрики от них. Скрипт берёт цифры из summary.json, который k6 пишет по флагу --summary-export, и ставит рядом с каждой метрикой её порог из скрипта. Заголовок — итог одной строкой: «✅ Load: 4 thresholds passed» или «❌ Load: 1 of 4 thresholds crossed».

В таблице два вида строк:

  • С порогом — quality gate сборки: успешные звонки, setup time p95, доля плеч, услышавших звук, оценка звука p95. Рядом ✅ или ❌.

  • Без порога — контекст, который объясняет красную строку: число звонков и CAPS, post-dial delay, первый ответ на INVITE, джиттер, принятые и потерянные RTP-пакеты, проверки. Если упал порог оценки звука, строка «RTP packets received / lost» сразу покажет, потери это или что-то другое.

Так сводка выглядит на последнем прогоне:

Первая строка — ещё и проверка самого теста: 3,18 CAPS сходятся с расчётными 3,2 по закону Литтла, значит генератор дал заказанную нагрузку. Если бы общий раннер захлебнулся, CAPS упал бы раньше, чем покраснели пороги. Любой порог, которого нет в списке метрик, скрипт добавит строкой сам: нарушенный порог не потеряется, даже если его добавили позже. Строка в конце сводки указывает на dashboard.png в артефакте: сводка говорит «что», дашборд — «когда и как».

Шаг в job нагрузки — такой же, с проверкой, что k6 вообще успел написать итоги:

      - name: summary
        if: always()
        run: |
          if [ -f reports/summary.json ]; then
            python3 .github/scripts/summary.py load reports/summary.json \
              "20 VUs for 2 minutes against the test PBX, audio compared on every call." > reports/summary.md
            cat reports/summary.md
            cat reports/summary.md >> "$GITHUB_STEP_SUMMARY"
          fi

JUnit: при чём он здесь

JUnit XML — формат отчёта о тестах, который понимает практически любая CI и система тест-менеджмента. Название осталось от Java-фреймворка, но сам формат давно общий: набор тестов (testsuite), в нём тест-кейсы (testcase), у упавшего — failure с причиной. Именно из JUnit скрипт строит сводку функциональных тестов, и именно его заберут другие системы, если ваш CI — не GitHub.

В нашем отчёте каждый step() сценария — отдельный тест-кейс, с фактическим значением в имени:

<testsuite name="reject-cancel" tests="14" failures="0">
  <testcase name="A calls B" classname="reject-cancel"/>
  <testcase name="B gets the call" classname="reject-cancel"/>
  <testcase name="A gets 486 (got 486)" classname="reject-cancel"/>
  ...
  <testcase name="A gets 404 (got 404)" classname="reject-cancel"/>
  <testcase name="threshold checks: rate==1" classname="reject-cancel"/>
</testsuite>

Ловушка стандартной функции. В первой версии handleSummary() звал jUnit() из официальной библиотеки k6-summary, и отчёты выглядели так:

<testsuite name="k6 thresholds" tests="1" failures="0">
  <testcase name="checks - rate==1" classname="Unnamed folder" />
</testsuite>

Один кейс на весь сценарий: jUnit() делает тест-кейсы из порогов, а не из проверок. Если бы такой отчёт упал, он сказал бы только «что-то не так». Заметили это не сразу: пока всё зелёное, файл никто не открывает. Теперь JUnit пишет сам lib.js, примерно 30 строк: обходит data.root_group.checks из итогов k6 в порядке выполнения и добавляет по кейсу на каждый порог.

Кто ещё покажет этот отчёт. Сводка — это наш вариант для GitHub. Другие системы читают тот же XML сами, и каждый шаг звонка там становится отдельным тестом:

Где

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

Что нужно

GitLab CI

вкладка Tests в пайплайне и виджет в merge request

artifacts: reports: junit: reports/*.xml

Jenkins

история каждого кейса по сборкам, график «прошло / упало»

плагин JUnit: junit 'reports/*.xml'

GitHub, с аннотациями в PR

упавшие шаги отдельным отчётом проверки

сторонний action (dorny/test-reporter и подобные)

TestRail, Xray, Zephyr

прогон в системе тест-менеджмента

импорт JUnit XML

Именно поэтому фактическое значение стоит писать в имя шага. В GitLab или TestRail лога k6 не будет, а строка «A gets 404 (got 480)» объясняет провал сама.

Дашборд за окно прогона

Пороги отвечают на вопрос «прошло или нет», а дашборд — на вопрос «что происходило». Но Prometheus и Grafana на раннере исчезают вместе с ним, поэтому дашборд нужно успеть сохранить. Это делает скрипт monitoring/report.sh: он открывает дашборд в headless Chrome с нужным testid и окном времени и снимает его целиком одним изображением.

# высота страницы — из разметки дашборда: клетка сетки Grafana 30 px + 8 px отступ
height=$(python3 -c 'import json,sys
d = json.load(open(sys.argv[1], encoding="utf-8"))
print(max(p["gridPos"]["y"] + p["gridPos"]["h"] for p in d["panels"]) * 38 + 80)' "$dashboard")

# kiosk — без меню; testid и from/to (в миллисекундах) выбирают прогон и его окно
url="$grafana/d/xk6-sip/xk6-sip?orgId=1&kiosk&var-testid=$testid&var-window=30s&from=${from}000&to=${to}000"

# --window-size: окно высотой во весь дашборд — Grafana рисует только видимые панели
# --run-all-compositor-stages-before-draw: снимок только полностью отрисованной страницы
# --virtual-time-budget=60000: до 60 с виртуального времени на запросы к Prometheus;
#   без него Chrome снимает сразу после загрузки, и панели оказываются пустыми
"$chrome" --headless=new --disable-gpu --hide-scrollbars --window-size="1600,$height" \
  --run-all-compositor-stages-before-draw --virtual-time-budget=60000 \
  --screenshot="$out" "$url"

Две детали, без которых скриншот получается пустым. Grafana дорисовывает панели лениво, только в видимой части окна, поэтому окно браузера должно быть высотой во весь дашборд. А --virtual-time-budget даёт запросам к Prometheus время завершиться до снимка. Окно времени берётся от момента запуска k6 минус 30 секунд до конца прогона, с паузой 15 секунд на последнюю отправку метрик.

Итог — картинка 1600 × 3956 со всеми 39 панелями в артефактах сборки. Разберём два блока из неё.

Обзор дашборда за прогон в CI

Обзор дашборда за прогон в CI

Верхний блок — 12 индикаторов за последнее окно: ASR и SEER 100%, one-way audio 0%, setup p95 1,1 мс, все звонки в SLA, ретрансмиссий нет. «Calls in progress 0» и CAPS 2,3 вместо 3,15 — не ошибка: плитки показывают самую свежую точку, а она приходится на завершение теста. Средние за весь прогон — в итогах k6.

Блок здоровья генератора за прогон в CI

Блок здоровья генератора за прогон в CI

Нижний блок отвечает на главный вопрос нагрузки на общем раннере: не упёрся ли сам тест. Dropped iterations — 0, длительность итерации ровная, 6–6,5 с. Процесс k6 занял около 6% одного ядра и 70 МБ памяти, куча Go идёт пилой без роста, горутин около 125. CPU машины после старта — несколько процентов; всплеск до 70% в начале — это запуск контейнеров и k6. Значит, цифрам сигнализации и звука можно верить.

Сеть машины при этом показывает килобайты, а сеть процесса k6 — 310 кБ/с RTP. Тестовая АТС работает на той же машине, и звонки идут через loopback, который не проходит через сетевую карту. Счётчики на сокетах xk6-sip видят его, а экспортёр машины — нет.

И целиком — так отчёт выглядит в артефактах сборки:

Полный отчёт: дашборд xk6-sip за прогон в CI

Полный отчёт: дашборд xk6-sip за прогон в CI

Грабли

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

Симптом

Причина

Исправление

Все шесть сценариев падают: REGISTER без ответа, 10 ретрансмиссий, Timer_B timed out

go build -o testpbx положил бинарник внутрь папки testpbx/ — это имя пакета АТС в репозитории. ./testpbx & молча не запустился, и АТС не было вообще

бинарники в bin/

open bin/k6: no such file or directory

bin/ в .gitignore, на чистом раннере её нет, а xk6 сам папку не создаёт

mkdir -p bin

monitoring/report.sh: Permission denied

скрипт закоммичен с Windows без бита исполняемости

git update-index --chmod=+x и вызов через bash

Панели процесса k6 пустые на Linux, хотя на Windows работают

Prometheus в Docker ходит к k6 через шлюз хоста, а эндпоинт на 127.0.0.1 с него не виден. Docker Desktop на Windows это скрывает

SIP_METRICS_ADDR=0.0.0.0:6566 на раннере

Панель доли ошибок с осью до 10 000%

если все значения нулевые, Grafana сама выбирает диапазон оси

мягкий максимум оси (axisSoftMax)

Первая строка — самая поучительная. Симптомы выглядели как сетевая проблема или ошибка SIP-стека, а причина нашлась в первой строке лога АТС, который мы сохраняем в артефактах: ./testpbx: Is a directory. Правило: логи тестируемой системы идут в артефакты с if: always() с первого дня. И стоит проверять, что система поднялась, прежде чем давать на неё нагрузку: для Grafana это уже сделано через /api/health, а для АТС пока стоит sleep 1.

Ещё одна тонкость относится не к CI, а к метрикам: нативные гистограммы Prometheus искажают величины, которые скапливаются у единицы. Оценка звука 0,993 превращалась на дашборде в 0,958. Подробно — в статье про проверку качества звука.

Что дальше

То, что описано выше, закрывает первые два уровня проверок. Дальше — то, что превращает CI с нагрузкой в полноценный процесс нагрузочного тестирования:

  1. Настоящая АТС вместо тестовой. Сценарии не меняются: абоненты передаются через -e REGISTRAR=... -e A_USER=... или CSV. АТС можно поднять контейнером прямо в CI (Asterisk, FreeSWITCH) или гонять тесты против стенда с self-hosted раннера в его сети.

  2. Длинные прогоны по расписанию. Ночной job (on: schedule) на выделенном генераторе: поиск предела открытой моделью нагрузки (ramping-arrival-rate ступенями) и soak-тесты на часы. Закрытая модель с фиксированным числом VU, как в CI, потолок не находит: замедление АТС само снижает CAPS.

  3. Сравнение с базовой линией. Сейчас пороги абсолютные. Следующий шаг — сравнивать summary.json с прогоном основной ветки и валить PR, если setup p95 или оценка звука ухудшились сверх допуска. На выделенном стенде с постоянным Prometheus то же делает дашборд: выбор нескольких pbx_version и сравнение версий на одном графике.

  4. Результат прямо в PR. Комментарий с таблицей ключевых метрик и превью отчёта, чтобы ревьюер видел влияние изменения на производительность, не скачивая артефакты.

Всё описанное лежит в репозитории github.com/Dmitry-Fedotov-Dev/xk6-sip: *

Предыдущие статьи серии:

Если вы тестируете свою АТС — Asterisk, FreeSWITCH, Kamailio, OpenSIPS или собственную, — попробуйте эти сценарии на ней: для старта достаточно двух абонентов и адреса регистратора. Результаты, падения и нехватающие сценарии жду в issues репозитория. А если подход пригодился — поставьте звезду на GitHub: так инструмент быстрее найдут те, кому он нужен.

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.