Привет, Хабр, это снова Валентина, которая отвечает за качество low-code платформы Eftech.Factory в компании Effective Technologies. Представляю вторую статью из серии публикаций о наших практиках нагрузочного тестирования (НТ). Первую, про поиск оптимального процесса НТ, можно прочесть здесь. На этот раз я собираюсь поделиться рекомендациями по автоматизации рутины и отчётности.
Чтобы провести нагрузочное тестирование без стресса, надо позаботиться о сохранении своего ресурса — времени и нервов. Также мне кажется правильным освободить себя от рутинных и нудных дел, чтобы заниматься интересными и сложными задачами.
Чтобы достичь этих целей, я выработала для себя антистрессовый чек-лист из пяти пунктов:
-
Автоматизируй запуск замеров
-
Делегируй рутину боту
-
Экономь время на отчетах
-
Доверяй, но проверяй результат
-
Заботься о тестовых данных
По ним мы сегодня и пойдём:
Автоматизируй запуск замеров
Если вы новичок в НТ, вероятнее всего, вас устроит создание простых скриптов для скорейшего получения результата. Если замеры проводятся раз в год, то локальный запуск скрипта и черновые заметки о том, как он работает, вполне допустимы.
Напомню, что в моем проекте для НТ используется инструмент k6, который требует или чистого кода JavaScript, или библиотек-плагинов, написанных под k6.
Пример локального запуска подачи нагрузки и замеров на k6
Но если в вашей команде НТ — регулярная активность, и в анализе замеров участвует команда, то есть повод задуматься об автоматизации.
Напомню, что простейший перенос вашего скрипта в любой сервис CI/CD позволит:
-
не тратить мыслетопливо, вспоминая, как запустить скрипт и что подставить во входные параметры;
-
отказаться от тонны документации для передачи своих знаний о работе скрипта коллегам.
У нас запуск замеров НТ оформлен в CI/CD:
K6 поднимается в Docker-контейнере и используется на последующих шагах:
build-k6: stage: build rules: - when: always script: - |+ docker run --rm -u "$(id -u):$(id -g)" -v "${PWD}:/xk6" grafana/xk6 build v0.54.0 \ --with github.com/GhMartingit/xk6-mongo@v1.0.3 \ --with github.com/avitalique/xk6-file@v1.4.0 \ --with github.com/grafana/xk6-faker@v0.4.0 \ > docker.log 2>&1 cache: policy: push key: k6_binary paths: - ./k6 artifacts: paths: - "*.log"
До запуска замеров проверяем готовность контура для работы:
check: stage: check rules: - when: always script: - ./k6 run checkStand.js -e configFile=config/preparation.json -e host="$STAGE.$PROJECT.$DOMEN" -e dbName="${STAGE}_${PROJECT}" cache: key: k6_binary policy: pull paths: - ./k6
Файл проверок также реализован с учётом особенностей k6:
import { expectedCountDocument, mongoClass, } from "../../methods/base.method.js"; import { Counter } from "k6/metrics"; export const CounterErrors = new Counter("Errors"); export const options = { iterations: 1, thresholds: { "Errors{case:user}": [ { threshold: "count<1", abortOnFail: true } ], "Errors{case:load_object_read}": [ { threshold: "count<1", abortOnFail: true }, ], "Errors{case:load_object_write}": [ { threshold: "count<1", abortOnFail: true }, ], }, }; export default function () { const userCount = mongoClass.count("user"); if (userCount < expectedCountUser || userCount === undefined) { CounterErrors.add(1, { case: "user" }); } ... const loadObject1ReadCount = mongoClass.count("load_object_read"); if (loadObject1ReadCount < expectedCountDocument) { CounterErrors.add(1, { case: "load_object_read" }); } ... mongoClass.deleteMany("load_object_write", {}); const loadObject1WriteCount = mongoClass.count("load_object_write"); if (loadObject1WriteCount > 0) { CounterErrors.add(1, { case: "load_object_write" }); } }
Скрипт проверяет достаточность данных — например, количество учётных записей для разнообразия авторизации или объём данных в коллекции MongoDB для замера чтения крупных реестров.
Кстати, достаточность — это не только про наличие, но и про отсутствие. Например, для скриптов, которые создают записи в базе, в нашем проекте обязательным условием является чистая коллекция до старта замеров.
Также в шаг проверки можно включить сверку конфигурации, параметров контура и т.д.
Функции k6 позволяют добавить обработку результатов для выполненных шагов скрипта.
Так если какая-либо из проверок «упадёт», то шаг pipeline также не выполнится, и замер не будет запущен.
Запуск НТ у нас конфигурируемый:
При запуске pipeline можно выбрать:
-
Сценарий нагрузки
-
Профиль нагрузки
variables: STAGE: value: "15-0-x-autotest" description: "Название контура" SCRIPT: description: "Сценарий нагрузки" value: "scripts/all" options: - "scripts/all" - "scripts/auth-api" - "scripts/featureN" - "scripts/websocket" CONFIG: description: "Профиль нагрузки" value: "config/smoke" options: - "config/smoke" - "config/rampRate" - "config/stability"
Профиль нагрузки определяет, как долго и какой поток нагрузки будет идти на контур.
-
smoke — этот профиль используется для проверки контура и работоспособности скриптов. Выполняется 5 повторов каждого скрипта.
-
rampRate — целевой профиль для замеров. Нагрузка подается в несколько этапов с постепенным увеличением потока.
-
stability — при выборе этого профиля нагрузка подается на протяжении нескольких часов (обычно 6—8 часов) с постоянным показателем RPS.
Сценарий нагрузки позволяет выбрать набор скриптов для подачи нагрузки.
-
all — этот сценарий запускает регрессионное НТ; будет выполнен каждый скрипт в репозитории.
-
auth-api — сценарий, при выборе которого запускаются только скрипты для подачи нагрузки на сервис авторизации.
-
feautureN — это пример; можно запустить НТ даже для отдельной функцинальности!
У Grafana Labs есть хороший пост, в котором разъясняются отличия между разными типами профилей (правда, в статье они обозначены как Types of load testing).
Уверена, что у вас возник закономерный вопрос: а как же определить, какая нагрузка стрессовая, а какая — нормальная?
И это чертовски хороший вопрос, ответа на который у меня нет)
Для каждого сервиса, функциональности или системы эти показатели индивидуальны. Они выясняются опытным путем через сопоставление поведения тестируемого объекта при определённых выделенных ресурсах и поэтапно растущей нагрузке.
Но вернёмся к коду)
Запуск замеров у нас обёрнут в bash-скрипт. Возможно, это излишне и является рудиментом. Ранее для запуска требовалась подготовка переменных, которыми не хотелось мусорить в gitlab-ci.yml.
run: stage: measure rules: - if: $CI_PIPELINE_SOURCE == "pipeline" - if: $CI_PIPELINE_SOURCE == "schedule" - if: $CI_PIPELINE_SOURCE == "push" when: never - when: on_success script: - npm i - start=$(date +%s) - echo run.sh -h $STAGE -p $PROJECT -s $SCRIPT -d $DOMEN -c $CONFIG - ./run.sh -h $STAGE -p $PROJECT -s $SCRIPT -d $DOMEN -c $CONFIG - end=$(date +%s) - echo $start > start - echo $end > end after_script: - dashboardLT="${URL_GRAFANA_K6_FOR_QA}&from=$(cat start)000&to=$(cat end)000" - dashboardServices="${URL_GRAFANA_FACTORY_SERVICES}&from=$(cat start)000&to=$(cat end)000" - node tools/scripts/notification.js $STAGE $CI_COMMIT_BRANCH $CI_TELEGRAM_CHAT $CI_TELEGRAM_TOKEN $CI_JOB_ID $SCRIPT "$dashboardLT" "$dashboardServices" $CONFIG cache: key: k6_binary policy: pull paths: - ./k6 artifacts: paths: - ./report expire_in: 2 week name: ${STAGE}
И сам bash-скрипт…
#!/bin/bash while getopts h:p:s:d:c: flag do case "${flag}" in h) STAGE=${OPTARG} ;; p) PROJECT=${OPTARG} ;; s) SCRIPT=${OPTARG} ;; d) DOMEN=${OPTARG} ;; c) CONFIG=${OPTARG} ;; *) echo "Не корректный ключ. Проверьте введенные ключи $OPTARG";; esac done [ -z "$STAGE" ] && STAGE="15-0-x-autotest" [ -z "$PROJECT" ] && PROJECT="factory" [ -z "$SCRIPT" ] && SCRIPT="scripts/all" [ -z "$DOMEN" ] && DOMEN="lowcode" [ -z "$CONFIG" ] && CONFIG="config/smoke" if [[ $SCRIPT == "scripts/all" ]]; then folderPath=$(find scripts/* -type d ) else folderPath="$SCRIPT" fi for path in $folderPath; do for script in "$path"/*.js; do ./k6 run "$script" -e configFile="$CONFIG" -e host="$STAGE.$PROJECT.$DOMEN" -e dbName="${STAGE}_${PROJECT}" --insecure-skip-tls-verify -o experimental-prometheus-rw echo "$script", "$?" >>exitCode.txt done done
В зависимости от значения, переданного для сценария нагрузки, будут последовательно запущены скрипты из указанной папки.
После выполнения каждого скрипта в файл exitCode.txt записывается код выполнения.
Мы фиксируем как время старта, так и время окончания общего замера — эти показатели подставляются в ссылку Grafana, которую бот отправит после завершения замеров.
Про бота и использование файла exitCode.txt я подробно расскажу ниже.
Какими могут быть следующие шаги развития автозапуска и конфигурирования?
-
Встраивание запуска замеров при средней нагрузке в регулярный запуск проверки релиза.
-
Проведение НТ при динамическом масштабировании.
-
Перспективы тестирования выносливости, стресса и восстановления системы или продукта.
Делегируй рутину боту
Как не дёргаться, проверяя, завершились ли замеры НТ?
Нотификация нам в помощь! В Интернете много документации и примеров по реализации ботов для различных мессенджеров.
Рассмотрим реализацию нотификации в Telegram. Обязательным условием является предварительная подготовка бота.
В силу использования k6, нам потребуется или чистый код на Javascript, или библиотека-плагин, написанная под k6.
Поэтому у нас несколько вариантов:
-
реализовать нотификацию через API Telegram;
-
использовать плагин k6 — xk6-telegram.
Второй вариант кажется проще, начнём с него.
На странице документации к k6 есть раздел с расширениями.
Расширения создаются и поддерживаются как разработчиками k6, так и сообществом вокруг проекта.
xk6-telegram является одним из таких расширений. Оно разработано сообществом и упоминается в списке официально предлагаемых от k6, но его поддержка не гарантируется. Тем не менее, для старта можно использовать и его)
За основу кода берем пример из документации:
import http from "k6/http"; import telegram from "k6/x/telegram"; const conn = telegram.connect(`${__ENV.TOKEN}`, false); const chatID = 123456789; const environment = 'load_stand'; const link = 'https://grafana.com/'; export default function () { http.get('http://test.k6.io'); } export function teardown() { const body = `<b>Load testing</b> ${environment} \r\n`+ `<b>Dashboard</b>: <a href="${link}">K6 Result</a>`; telegram.send(conn, chatID, body); }
Что мы получаем?
Инструмент k6 выполняет код в скрипте последовательно: setup (пред-подготовка), function (основной код), teardown (пост-подготовка).
Как только замеры завершатся, с ошибкой или без неё, на финальном этапе (teardown) будет отправлено сообщение по указанному chatID.
Код отправки сообщения можно унифицировать для всех скриптов и вынести в отдельный наследуемый метод.
Для моего проекта простого уведомления о завершении расчетов мало.
Я бы хотела получать минимально необходимую информацию о выполненном замере.
Чтобы это стало возможным, мне потребовалось реализовать логику обработки результатов и сбор данных. А отправка уведомления реализована через вызов API Telegram.
В нашем проекте нотификация вынесена на уровень всего запуска замеров.
В .gitlab-ci.yml в блоке after_script вызывается
node tools/scripts/notification.js args
tools/scripts/notification.js содержит всю логику по подготовке и отправке сообщения боту.
Полный текст кода вы можете просмотреть по ссылке.
На вход скрипту передаются данные, объявленные в job pipeline:
-
название контура, на котором запускались замеры (у нас для каждого релиза готовится свой контур — для сценария, когда требуется сделать замеры в текущем и прошлом релизах);
-
ветка со скриптами, которые выполнялись;
-
название профиля нагрузки;
-
jobID (чтобы отправить в сообщении ссылку на артефакты Gitlab).
Далее выполняется подготовка данных для сообщения.
Стоит отметить, что exitCode от выполнения k6 у нас записывается в отдельный файл.
Для каждого скрипта вызывается k6 run script.js, и результат записывается в файл exitCode.txt.
На этапе подготовки данных для сообщения файл обрабатывается, строится цветовая градация успешности выполнения. Это не обязательно, но позволяет сразу сориентироваться в результате.
Только после этого вызывается отправка сообщения.
Экономь время на отчётах
Итак, у нас есть автоматизированный запуск в 2 клика, и бот своевременно уведомляет нас о завершении замеров. Это значит, что следующий этап — автоматизация отчётности.
Какой бы сервис для подачи нагрузки вы ни выбрали, в нём почти всегда есть стандартный отчёт с минимально необходимыми метриками.
В k6 вывод результатов выглядит так:
Такого среза достаточно, чтобы сделать выводы о замере и спланировать следующие действия.
А теперь представим, что необходимо сделать несколько разноплановых замеров и свести их результаты в удобно читаемый отчёт для руководства.
Failure story: раньше я, засучив рукава, погружалась в океан цифр и сводила их вручную. И хорошенько выгорала на этом!
Антиcтресс рекомендация: не поленитесь настроить простую интеграцию Prometheus-Grafana.
В нашем проекте k6 собирает метрики нагрузки, передаёт в Prometheus, а Grafana на основе этих данных строит графики и сводки таблиц.
В итоге мы получаем единое хранилище результатов и динамическую визуализацию:
-
Контроль показателей машины с тестируемым сервисом:
-
Показатели состояния тестируемого сервиса:
-
Метрики нагрузки:
Если вы только стартуете в НТ или присматриваетесь к сервисам нагрузки, то платные Enterprise-решения вам не доступны. Но какие же у них интересные возможности!
Например, Grafana Cloud предлагает обработать результаты замеров и показать их в сравнении между несколькими итерациями.
Когда очень хочется, но дорого, единственный выход — создать свой «велосипед».
Наши DevOps подготовили свой Grafana дашборд для анализа метрик нагрузки, в том числе с возможностью сравнения между несколькими замерами:
Разберёмся, как это работает.
Представьте себе ситуацию, когда НТ выполнялось в несколько итераций в течение ночи.
Нагрузка росла этапами — и так для каждого из скриптов.
{ "summaryTrendStats": ["avg", "p(90)", "p(95)", "p(99)", "count"], "scenarios": { "rampRate": { "executor": "ramping-arrival-rate", "maxVUs": 400, "preAllocatedVUs": 1, "timeUnit": "1s", "stages": [ { "target": 5, "duration": "1m" }, { "target": 5, "duration": "5m" }, { "target": 10, "duration": "1m" }, { "target": 10, "duration": "5m" }, { "target": 20, "duration": "1m" }, { "target": 20, "duration": "5m" }, { "target": 40, "duration": "1m" }, { "target": 40, "duration": "5m" } ] } } }
Что делает такой профиль?
-
stage0: минутный рост нагрузки до 5 RPS
-
stage1: стабильная нагрузка в 5 RPS
-
stage2: минутный рост нагрузки до 10 RPS
-
stage3: стабильная нагрузка в 10 RPS
-
stage4: минутный рост нагрузки до 20 RPS
-
stage5: стабильная нагрузка в 20 RPS
-
stage6: минутный рост нагрузки до 40 RPS
-
stage7: стабильная нагрузка в 40 RPS
С утра вы садитесь за анализ результатов.
Общая сводка позволяет сделать предварительные выводы, но для заключения о качестве нужны более точные сравнения.
Идеальным было бы точечное сравнение показаний по каждому сценарию.
Например, вот такой сводной таблицей:
Но и такое решение не идеально, ведь нагрузка подавалась этапами с возрастанием потока!
Такой у нас получается визуализация выполнения скрипта «Login» с градацией по этапам нагрузки:
Код плитки: https://disk.yandex.ru/d/j80WalY5BAqZ8g
Мне как создателю скриптов понятно, что при росте нагрузки до 20 RPS (stage4) начинается деградация в работе авторизации.
А если заглянуть ещё на уровень глубже, то можно узнать, что проблема конкретно в запросе logout.
Но есть одно «но»…
Отчетом пользуюсь не я одна, и надо, чтобы графики читались легко и не вызывали вопросов.
Поэтому думаем, как сравнить графики на основе подаваемой нагрузки в RPS.
У нас начинает получаться что-то такое:
Выводы те же: до определенной нагрузки API сервиса справляется с потоком, а после начинается «расколбас».
Далее можно настроить такой же график для каждого API внутри сценария или оставить вывод в таблице.
Код плиток в обеих вариациях можно найти по ссылке.
Доверяй, но проверяй результат
Теперь нас не отвлекает механика замера, а наглядный отчёт позволяет быстрее проанализировать результаты. А значит, можно заняться проблемой качества кода в скриптах нагрузки.
Failure story
Мне достаточно вспомнить этот эпический провал, а вы представьте ситуацию.
Допустим, есть задача — протестировать скорость ответа API для запроса авторизации в сравнении между двумя релизами.
Ниже приведён код. У нас в скрипте всего 1 запрос, который выполняется для различных учётных записей:
import http from "k6/http"; import { scenario } from "k6/execution"; … export default function (loginArray) { const body = { login: loginArray[scenario.iterationInTest % loginArray.length], password: commonPassword, }; http.post(`${host}/api/auth/login`, body); }
Результат прошлого релиза у нас уже был, а замеры на новой версии выдают потрясающий результат. Ускорение в 2 раза!
Мы с командой празднуем победу, а я начинаю готовить отчет о проделанной работе… И тут замечаю по логам, что все 100% запросов вернули ошибку.
Больно, обидно, познавательно.
Теория НТ не рекомендует усложнять скрипты нагрузки генерацией данных и функциональными проверками. Но минимально необходимые проверки результата нужны!
Часто сервисы генерации нагрузки предоставляют функции для проверок данных и результатов, которые не отъедают ресурсы.
А после можно задуматься о выставлении порогов (критериев успеха или провала).
Например, если запрос выполняется дольше 200 мс, мы получим цветовую нотификацию о пересечении выставленной границы.
Также можно настроить остановку выполнения скрипта, если показатели вышли за заданный порог.
Согласитесь, удобнее получить сообщение от бота с предупреждением о неожиданном поведении сервиса, чем «красный» отчет после 2-х часов ожидания.
Как ещё можно развить проверки?
-
Комбинировать проверки и пороги (с возможностью остановки скрипта).
-
Настроить пороги для каждого из этапов нагрузки.
-
Настроить разные уровни реагирования (приемлемо / тревожно / неправильно).
Заботься о тестовых данных
Failure story
Представим ситуацию, когда вы по наитию решаете выполнить один и тот же скрипт несколько раз подряд. (Реальная история, которую я снова вспоминаю с красными щеками!)
Вы сводите результаты в одну таблицу и получаете радугу, как у меня на картинке:
Первая же мысль у нас с командой: «Допустили утечку ресурсов!»
Но, как оказалось, проблема крылась в другом)
При запуске регрессионного НТ запускались скрипты не только на чтение данных, но и на запись! Из-за этого коллекции в базе данных «пухли» с каждой итерацией.
Теория НТ настаивает на выполнении нескольких однотипных замеров друг за другом.
Какие же выводы мы сделали из этой глупейшей ошибки?
Перед стартом замеров рекомендуется:
-
проверять наличие, достаточность и соответствие ожидаемому объему данных
(в том числе — их отсутствие); -
подготовить генерацию однотипных данных.
Что ещё можно улучшить?
-
Автоматизировать проверки наличия настроек и других конфигураций данных.
-
Автоматически очищать отдельные коллекции или даже запускать сброс БД в исходное состояние.
-
Оценить возможность импорта БД перед замерами при потребности в большом срезе данных.
Послесловие
Повторю свою мысль из первой статьи: ошибаться — это нормально. Ошибки — это опыт, который учит нас расширять горизонты и становиться лучше.
Путь в НТ тернист, но жутко интересен! Проблем, с которыми вы можете столкнуться, гораздо больше, чем упомянуто в статье. Но это повод пересмотреть свой опыт и подумать об автоматизации.
Также хочу порекомендовать Телеграм-канал «QA — Load & Performance». С ним я познакомилась, когда готовилась к выступлению на конференции Heisenbug, и была приятно удивлена, найдя там отзывчивое сообщество специалистов в нагрузочном тестировании.
ссылка на оригинал статьи https://habr.com/ru/articles/856240/
Добавить комментарий