Триггернутые, или Как безболезненно встроить нагрузочное тестирование в ваш пайплайн

от автора

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

Меня зовут Саша, я работаю в команде тестирования Ozon Fintech. В прошлый раз я рассказывала о типах нагрузочного тестирования (НТ) и о том, как создавать пушки под свои нужды. Сегодня же научу запускать НТ по кнопочке в CI. Статья будет полезна тем, кто уже имеет наработки по НТ, но ещё не автоматизировал их или ищет способы запускать тесты не по крону.

Наша команда финтеха в последнее время сильно разрослась. Сервисов стало много, и тестировщиков стало не хватать. Иногда стали возникать ситуации, когда разработчики просят нагрузить новую версию сервиса, но у QA на это нет времени, быстро проверить не получается. 

Чтобы не зависеть от нас, команда разработки попросила создать простой инструмент, который бы позволил им самостоятельно проводить весь цикл нагрузочного тестирования из CI. Иными словами, они попросили добавить в CI кнопку, при нажатии на которую всё само работает. Вся подкапотная магия — автоматическая генерация свежих патронов, загрузка их на сервер, билд пушки, загрузка пушки на сервер, сборка актуального конфига, загрузка конфига на сервер, запуск всего этого добра из консольки на сервере — их не интересует.

Мы рассматривали три варианта реализации этой идеи:

  1. Сам репозиторий для нагрузки создавался как проект-библиотека для хранения шаблонов патронов и пушек: в нём не предусматривается работающий сервис, он нигде не поднимается и не крутится. Поэтому как раз первой мыслью было сделать из него сервис, добавить в него ручки, по которым всё будет генериться и стреляться. Но это оказалось слишком сложно в рамках текущего проекта.

  2. В тестируемый сервис добавить файл с методами, которые будут обращаться к нагрузочному репозиторию как к библиотеке, всё создавать и запускать тесты. Но тогда владельцам самого тестируемого сервиса надо будет за всем следить. Это расходится с принципом «Просто нажать на кнопку — и оно само будет работать».

  3. Создать триггеры для нагрузки, по которым на стороне нагрузочного репозитория с помощью простых команд будет выполняться алгоритм всей этой генерации и запуска нагрузки, а к тестируемому сервису этот триггер будет просто подключаться.

Да здравствуют триггеры!

В GitLab можно создавать многопроектные пайплайны, когда действие в одном пайплайне запускает пайплайн в другом проекте. Триггер — это тип джобы, которая вызывается в одном проекте, но запускает выполнение действий в другом. 

В нашем примере будет два сервиса: 

  1. Сервис, который мы тестим (в терминологии триггеров — upstream-сервис), назовём его SUT (system under test).

  2. Сервис НТ, который стреляет (он же downstream-сервис), назовём его LOAD

Так как много всего завязано на ветки гита, то в нашем примере у SUT ветка будет называться my-upstream-branch, а у LOADmy-downstream-branch. В рамках примера мы хотим, чтобы по триггеру из проекта SUT ветки my-upstream-branch запускался пайплайн в проекте LOAD ветки my-downstream-branch

Таким образом, для создания многопроектного пайплайна у нас есть три элемента:

  • тесты, которые мы гоняем (LOAD);

  • триггер, по которому они запускаются;

  • сервис, который использует этот триггер, чтобы запустить тесты (SUT).

В коде это выглядит немного иначе:

  1. Есть тесты, которые мы запускаем через обычный пайплайн, описанный в gitlab-ci.

  2. Есть файл, который содержит описание джобы с триггером.

  3. Есть gitlab-ci в тестовом сервисе, который наследует эту джобу.

Приступим! В LOAD создаём триггер в отдельном файле .trigger.yml:

.run_load_tests:   stage: build   allow_failure: true   trigger:      project: load     strategy: depend     branch: my-downstream-branch #downstream branch pipeline

Обязательно пушим изменения на сервер, иначе SUT не даст сделать изменения в себе. В SUT добавляем шаг, в котором наследуется джоба с триггером, в .gitlab-ci.yaml:

load tests:   extends: .run_load_tests   allow_failure: true  include:   - project: load     ref: my-downstream-branch #downstream branch trigger file     file: .trigger.yml 

и тоже пушим.

Важно: в SUT в include:ref прописывается ветка, из которой мы будем считывать настройки триггера; в LOAD в trigger:branch указывается название ветки, прогон которой будет активирован.

Мультивселенная

Еще раз: в .gitlab-ci.yml сервиса SUT указывается ветка, из которой брать условие запуска тестов, а в .trigger.yml сервиса LOAD — кодовая база для прогона.

Мультивселенная
Мультивселенная

Таким образом, у нас появляется кнопка для запуска LOAD-тестов в пайплайне.

Скрипт нагрузки: добавим логики

Кнопка есть, но никакие действия для downstream-проекта не прописаны. В описание джобы с триггером даже нельзя закинуть script для выполнения, ведь такого ключа нет в списке разрешённых.

В случае попытки добавить это ключевое слово вылезет ошибка

CI lint invalid: [jobs:lnl tests config contains unknown keys: trigger]

Что же тогда будет выполняться? 

Суть в том, что триггер запускает пайплайн целиком, а не конкретную задачу. Причём пайплайн ветки, заданной в описании триггера .run_load_tests:trigger:branch: my-downstream-branch.

Если в gitlab-ci нет никаких шагов, то вылезает такая ошибка:

Поэтому следующим шагом добавляем и сам .gitlab-ci.yml, если его ещё нет, и шаги в него, например:

any_job:   stage: generate   when: always   script:     - echo "hello world" 

В результате при использовании триггера срабатывает пайплайн из нужной ветки:

Дальше закидываем в .gitlab-ci.yml все стейджи и шаги, которые нам нужны для запуска пушки (полные файлы будут в конце статьи):

stages:   - generate   - upload   - config   - shoot  generate-ammo:   stage: generate   script:     - make generate-ammo service=sut  upload-ammo:   stage: upload   script:     - make upload-ammo service=sut  generate-config:   stage: config   script:     - make generate-config service=sut    shoot:   stage: shoot   script:     - make shoot service=sut

Кастомизация

Такой хардкод с названием сервиса service=sut подходит для одного тестового сервиса, но мы тут делаем универсальный инструмент. Один и тот же пайплайн хочется запускать как из сервиса sut, так и из сервиса another-sut, у которых под капотом будут разные патроны, разные пушки, да просто разные адреса и условия нагрузки. 

Для этого настройки пайплайна на стороне LOAD будем определять через переменные окружения. В триггер будем передавать CI-переменную ${CI_PROJECT_NAME}, на основании которой изменять настройки:

.trigger.yml  .run_load_tests:   stage: build   allow_failure: true   variables:     TEST_SERVICE: ${CI_PROJECT_NAME} #название upstream-сервиса   trigger:     project: load     strategy: depend     branch: my-downstream-branch 

А в .gitlab-ci.yml добавим ещё один шаг aim, на котором будем определять, куда стрелять:

.gitlab-ci.yml  stages:   - aim   - generate   - upload   - config   - shoot  load-tests:   extends: .load-tests  .load-tests:   stage: aim   rules:     - if: $TEST_SERVICE == "sut"       variables:         TARGET: "sut.service.stg:82"         MAX_RPS: "49"     - if: $TEST_SERVICE == "another-sut"       variables:         TARGET: "another-sut.service.stg:82"         MAX_RPS: "16"   script:     - echo "testing service $TEST_SERVICE"     - echo "TARGET=$TARGET" >> build.env #save TARGET for later stages     - echo "MAX_RPS=$MAX_RPS" >> build.env #save MAX_RPS for later stages         artifacts:     reports:       dotenv: build.env  generate-ammo:   stage: generate   script:     - make generate-ammo service=$TEST_SERVICE

Важно обратить внимание на необходимость артефактов и на переменные окружения

Переменной TEST_SERVICE присваивается значение ${CI_PROJECT_NAME} на уровне upstream-проекта; в downstream-проекте она является переменной окружения и доступна со своим значением из любого места в пайплайне. А вот переменные, которые мы определяем в .load-tests:rules:variables, являются локальными и доступны только в рамках шага, выполняемого в этот момент. Поэтому их надо записать в артефакт dotenv, а в последующих шагах этот артефакт считывать:  

.gitlab-ci.yml   script:     - echo "TARGET=$TARGET" >> build.env #save TARGET for later stages   artifacts:     reports:       dotenv: build.env  generate-ammo:   stage: generate   script:     - make generate-ammo service=$TEST_SERVICE addr=$TARGET rps=$MAX_RPS   artifacts:     reports:       dotenv: build.env

Какие ещё подводные камни могут встретиться?

Во-первых, повседневная разработка нагрузочного проекта. 

На текущий момент в пайплайне нет никаких ограничений на запуск. Грубо говоря, при разработке новой фичи на каждый коммит будет триггериться падающий пайп, потому что в шаге .load-tests нет правила для нашего репозитория. Значение $TEST_SERVICE в рамках запуска проекта LOAD равно пустой строке ””.

Каждый коммит будет сопровождаться вот таким безысходным зрелищем:

Поэтому можно добавить ещё правил для пайпа с нагрузкой — запускать его, только если сработал триггер.

Для этого в триггер добавляем ещё одну переменную LOAD_PIPE: «true»:

.trigger.yml  .run_load_tests:   stage: build   allow_failure: true   variables:     LOAD_PIPE: "true" #запускает именно тестовый пайп     LNL_TEST_SERVICE: ${CI_PROJECT_NAME}

А на шагах, которые мы хотим запускать по триггеру, добавляем условие

.gitlab-ci.yml:  generate-ammo:   rules:     - if: $LOAD_PIPE == "true"

Это позволяет добавить пайплайн для разработки. Ведь в нашем проекте НТ тоже должны быть тесты и линтер, которые повышают качество кода, и мы хотим их запускать на всех ветках, кроме мастера. Например:

.gitlab-ci.yml  test:   extends: .go   stage: build   script:     - go test ./...   except:      refs:       - master   allow_failure: false

Во-вторых, повседневная разработка тестируемого проекта.

НТ не требуется в каждом возможном коммите. В наших проектах мы решили его запускать уже после релиза на стейдж-окружение. То есть сначала код должен пройти юнит-тесты, функциональные тесты, ревью на merge request, интеграционные тесты — несколько проверок, позволяющие с большой уверенностью предположить, что изменения в коде сделаны нормально. Всё-таки НТ — ресурсозатратный процесс, с помощью которого хочется проверять более или менее готовый продукт. Поэтому в триггер мы добавили ограничение на тип веток: НТ может быть запущено только в релизных ветках на стадии пост-деплоя, то есть когда сервис уже раскатан, и только вручную:

.trigger.yml  .load_tests:   stage: post-deploy #upstream stage   when: manual   only: #запускать только в релизных ветках     refs:       - "/^release\\/.+$/"

В-третьих, права пользователей. 

Чтобы иметь возможность запустить downstream-джобу, надо иметь достаточно прав для запуска пайплайна в downstream-проекте. То есть людей, которые будут запускать пайплайн с тестами, необходимо добавить к себе в репозиторий с необходимыми правами.

В-четвёртых, можно запутаться в ветках: upstream, downstream… 

Напомню, что в изначальной концепции был акцент на простоте — пользователям тестируемого сервиса нужно было делать минимум телодвижений или минимум изменений в своём проекте. Поэтому на уровне тестируемого upstream-проекта мы просто добавляем шаг, который наследует триггер.

В проекте LOAD пишем весь код для функционирования пушки, добавляем его в мастер — и после этого считаем, что актуальный код для НТ лежит там и нигде больше.

Финал

Итак, финальные варианты файлов. 

В проекте LOAD:

.trigger.yml  .run_load_tests:   stage: post-deploy #upstream stage    when: manual #НТ запускается вручную   allow_failure: true # НТ не влияет на прохождение пайплайна тестируемого сервиса   variables:     LOAD_PIPE: "true" #флаг запуска именно тестового пайпа     TEST_SERVICE: ${CI_PROJECT_NAME} # название upstream-проекта     SOME_OTHER_VAR:        value: "some_data"       description: "другие данные из upstream-проекта"   only: # запускать только в релизных ветках     refs:       - "/^release\\/.+$/"   trigger: # вызов тестов из другого репозитория     project: load #имя тестового репозитория НТ     strategy: depend #upstream pipe ждёт, пока пройдут все downstream-процессы     branch: master # при триггере запускается пайплайн из этой downstream-ветки
.gitlab-ci.yml  include:   - project: 'my/project/that/allows/to/use/golang'     ref: '0.0.5'     file:       - '/templates/go/.go.gitlab-ci.yml'   - local: '/.universal-ci.yml'  variables:   GO_VERSION: "1.17"   LOAD_PIPE:     value: "false"     description: "true if you want to start a load test"  stages:   - build   - aim   - generate   - upload   - config   - shoot  linter:   extends: .go   stage: build   script:     - make lint   except: # запускать только в релизных ветках     refs:       - master   allow_failure: true  test:   extends: .go   stage: build   script:     - go test ./...   except: # запускать только в релизных ветках     refs:       - master   allow_failure: true   # AIM # описание джобы вынесено в /.universal-ci.yml для лучшей читаемости load-tests:   extends: .load-tests  # GENERATE generate-ammo:   stage: generate   script:     - make generate-ammo service=$TEST_SERVICE addr=$TARGET rps=$MAX_RPS   rules:     - if: $LOAD_PIPE == "true"   artifacts:     reports:       dotenv: build.env  # UPLOAD upload-ammo:   stage: upload   script:     - make upload-ammo service=$TEST_SERVICE addr=$TARGET rps=$MAX_RPS   rules:     - if: $LOAD_PIPE == "true"   artifacts:     reports:       dotenv: build.env  #CONFIG generate-config:   stage: config   script:     - make generate-config service=$TEST_SERVICE addr=$TARGET rps=$MAX_RPS     - make save-config-to-artifact   rules:     - if: $LOAD_PIPE == "true"   artifacts:     reports:       dotenv: build.env     paths:       - ./configs     expire_in: 1 day  #SHOOT shoot:   stage: shoot   script:     - make shoot service=$TEST_SERVICE config=./configs   rules:     - if: $LOAD_PIPE == "true"   artifacts:     paths:       - ./configs     expire_in: 1 day 

Описание джобы load-tests берётся из ‘/.universal-ci.yml’, который указан в include в начале файла.

.universal-ci.yml  .load-tests:   stage: aim   rules:     - if: $TEST_SERVICE == "sut" #определение переменных для проекта sut       variables:         TARGET: "sut.service.stg:82"         MAX_RPS: "49"     - if: $TEST_SERVICE == "another-sut" #определение переменных для проекта another-sut       variables:         TARGET: "another-sut.service.stg:82"         MAX_RPS: "16"   script:     - echo "testing service $TEST_SERVICE"     - echo "TARGET=$TARGET" >> build.env #save TARGET for later stages     - echo "MAX_RPS=$MAX_RPS" >> build.env #save MAX_RPS for later stages   artifacts:     reports:       dotenv: build.env 

В сервисе SUT мы дополняем только .gitlab-ci.yml:

include:   - project: load     ref: master #берётся версия триггера из мастера     file:       - .trigger.yml  LOAD tests:   extends: .run_load_tests    variables:     SOME_OTHER_VAR: "If you need it downstream"

Если нужно на уровне SUT переопределить, например, TARGET, то это можно просто добавить в описание джобы:

load tests:   extends: .run_load_tests    variables:     SOME_OTHER_VAR: "If you need it downstream"     TARGET: $GIBSON_TARGET #change it if you want to test custom release

На выходе получаем систему с таким устройством:

Архитектура
Архитектура

Как вы могли заметить, в .gitlab-ci.yml в шагах генерации патронов, конфигов и пушек скрипты создания самих сущностей указаны верхнеуровнево. Для того чтобы все эти генерации работали для всех проектов, нужно и пушки с генераторами патронов сделать универсальными. Обе эти вещи выходят за пределы сегодняшней темы, поэтому и описаны без подробностей.

Вывод

Триггеры позволяют быстро автоматизировать процесс добавления функционала НТ в конечные сервисы. Регулярные обстрелы помогают определить, не уменьшилась ли пропускная способность сервиса, не ломаются ли интеграции с соседними сервисами при длительном взаимодействии, насколько оптимально работает железо. При таком встраивании НТ в жизненный цикл сервисов удаётся сэкономить время на генерации патронов и запуске вручную, а также тестировать сервис сразу после выкатки, а не ждать ночных стрельб по крону (если они вообще есть).

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

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

Надеюсь, моя статья облегчит вам жизнь и поможет быстрее настроить окружение для НТ, автоматизировать запуск тестов и улучшить качество ваших проектов. Stay tuned!


ссылка на оригинал статьи https://habr.com/ru/company/ozontech/blog/669518/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *