Интеграция нагрузочного тестирования на Grafana K6 в CI/CD

от автора

Обеспечение надежного функционирования системы при развертывании обновления системы требует запуска тестов разного уровня — от модульных тестов отдельных компонентов до интеграционных тестов, проверяющих в staging-окружении работу системы в целом. Но не менее важны для оценки готовности системы к большой кратковременной пиковой нагрузке (или злонамеренным атакам) выполнение нагрузочных тестов. В июле 2021 года компания Grafana Inc приобрела продукт k6, который изначально был ориентирован на запуск высокопроизводительных распределенных нагрузочных тестов, и это положительно повлияло на его дальнейшее развитие как встраиваемого инструмента для запуска тестов в облачных инфраструктурах или Kubernetes. В этой статье мы рассмотрим один из возможных сценариев использования k6 для тестирования сервиса в конвейере CI/CD.

Прежде всего отметим, что k6 может работать и как автономный инструмент тестирования (в виде выполняемого файла или docker-контейнера) и как управляемый кластер для организации распределенной нагрузки (например, с использованием k6-operator, который создает дополнительный тип ресурса в Kubernetes K6 для управления запуском необходимого количества процессов в кластере и определить для них контекст выполнения). Мы рассмотрим только вариант с использованием изолированного процесса, но при необходимости эти же тесты могут быть применены в распределенном сценарии использования.

K6 реализован на go и может быть установлен как через пакетный менеджер (homebrew, winget/choco, apt/dnf) или запущен из Docker-образа grafana/k6. Для описания теста используется сценарий на JavaScript (с поддержкой ES6), который выполняется в специальном окружении, предоставляющим доступ к управлению конфигурацией (через экспорт объекта options) и к описанию теста (экспорт функции default).

Для выполнения теста используются методы из модуля k6/http (get, post, put, patch, del), которые могут объединяться в группы (batch). Запрос может быть дополнен заголовками и содержанием. Результат может быть проверен через метод check с лямбда-функцией для проверки объекта ответа (например check(res, { 'status was 200': (r) => r.status == 200 });). Также можно создавать собственные метрики и изменять их значение при получении определенных состояний (например, подсчитывать ошибки), для этого в модуле k6/metrics есть реализации счетчика (Counter), скорости (Rate), серии значений с выделением минимального, максимального и текущего (последнего) значения (Gauge). Запросы могут выполняться в цикле, в том числе с разделением интервалом (через вызов sleep).

Кроме http запросов g6 поддерживает grpc (модуль k6/net/grpc), Web Sockets (k6/ws). При получении ответа можно выполнить разбор html (модуль k6/html). Также можно получать информацию о текущем тесте (через модуль k6/execution).

Кроме этого существует большое количество расширений, которые добавляют возможности для управления ресурсами инфраструктуры (например xk6-browser поможет организовать тестирование веб-сайтов с помощью headless-браузера). xk6-amqp управляет AMQP-брокером и позволяет создавать exchange/queue/binding и взаимодействовать с очередями, xk6-kubernetes для манипуляции ресурсами кластером Kubernetes и др.)

Попробуем разработать простое приложение на Python и сэмулировать в сборочном конвейере нагрузочный тест для проверки сохранения адекватного времени доступа при увеличении количества одновременных подключений. Для реализации сборочного конвейера будем использовать возможности Gitlab с использованием Docker Runner (но здесь может использоваться Github Actions, Jenkins и любой другой инструмент).

Создадим минимальное приложение для тестирования на Flask:

from flask import Flask  app = Flask(__name__)   @app.route("/") def hello_world():     return "Hello, World"   app.run(host='0.0.0.0')

и создадим Dockerfile:

FROM python RUN pip install flask WORKDIR /opt ADD main.py /opt CMD python main.py

Теперь подготовим конфигурацию для тестирования, для этого будем использовать образ контейнера grafana/g6. Создадим файл реализации теста:

import http from 'k6/http';  export default function () {   http.get('http://localhost:5000'); }

И запустим наш сервер. Для доступа к серверу из теста объединим два контейнера в одну сеть:

docker network create test docker build -t testserver . docker run -itd --network test testserver

И запустим нагрузочное тестирование, для этого укажен продолжительность выполнения теста (—duration) и количество виртуальных пользователей (—vus).

sudo docker run --network test -i --rm grafana/k6 run --vus 100 --duration 10s - <test.k6

Результатом выполнения будет отчет, включающий временные замеры по всем этапам http-подключения, для нас наиболее интересна продолжительность итерации:

running (10.1s), 000/100 VUs, 10908 complete and 0 interrupted iterations default ✓ [ 100% ] 100 VUs  10s       data_received..................: 2.0 MB 200 kB/s      data_sent......................: 884 kB 88 kB/s      http_req_blocked...............: avg=316.24µs min=99.84µs med=151.79µs max=40.04ms  p(90)=181.33µs p(95)=191.9µs       http_req_connecting............: avg=140.68µs min=62.51µs med=98.56µs  max=36.79ms  p(90)=118.12µs p(95)=125.86µs      http_req_duration..............: avg=91.65ms  min=1.87ms  med=91.17ms  max=110.26ms p(90)=94.28ms  p(95)=98.92ms         { expected_response:true }...: avg=91.65ms  min=1.87ms  med=91.17ms  max=110.26ms p(90)=94.28ms  p(95)=98.92ms       http_req_failed................: 0.00%  ✓ 0           ✗ 10908      http_req_receiving.............: avg=300.97µs min=36.12µs med=177.21µs max=7.01ms   p(90)=670.17µs p(95)=727.9µs       http_req_sending...............: avg=63.18µs  min=23.13µs med=40.73µs  max=30.66ms  p(90)=52.84µs  p(95)=58.23µs       http_req_tls_handshaking.......: avg=0s       min=0s      med=0s       max=0s       p(90)=0s       p(95)=0s            http_req_waiting...............: avg=91.29ms  min=1.32ms  med=90.83ms  max=109.35ms p(90)=93.87ms  p(95)=98.55ms       http_reqs......................: 10908  1081.901504/s      iteration_duration.............: avg=92.03ms  min=2.7ms   med=91.38ms  max=140.05ms p(90)=94.52ms  p(95)=99.51ms       iterations.....................: 10908  1081.901504/s      vus............................: 100    min=100       max=100      vus_max........................: 100    min=100       max=100

Можем увидеть, что при 100 пользователей потерь подключений не было (поскольку vus в среднем сохраняется 100), средняя продолжительность итерации 92.03 мс, медиана — 91.38 мс, 90-й перцентиль по времени 94.52 мс, 95-й перцентиль — 99.51 мс. Запустим теперь тест с 10000 пользователей.

http_req_failed................: 8.09%  ✓ 1149      ✗ 13041        iteration_duration.............: avg=9.73s    min=118.66ms med=2.24s    max=35.29s  p(90)=30s      p(95)=30.04s  vus............................: 1145   min=0       max=10000 vus_max........................: 10000  min=3532    max=10000

Можно увидеть, что в среднем обработалось только 1145 подключений (а в некоторые итерации все запросы отбивались, min vus = 0, http_req_failed 8%). Время обработки запросов и 90 и 95 перцентиль выше 30 секунда, медианное время 2.24 с. Кажется было бы хорошо остановить тест сразу, когда время ответа начинает превышать пороговое (например, 1 секунду) и сообщить об этом как о провале нагрузочного теста.

Полученные метрики могут накапливаться (—summary-trend-stats перечисляет метрики по которым будет анализироваться тренд), отправляться во внешние системы (в JSON, CSV, Prometheus, InfluxDB, Datadog, New Relic),

Добавим опции в тест и перенесем туда определение vus, duration (также может быть указан список stages для запуска многостадийного теста с указанной продолжительностью и количеством пользователей), а также добавим пороговые значения (thresholds) для остановки при превышении скорости появления ошибочных запросов. Также можно определить сценарий с определением executor для управления количеством пользователей, например ramping-vus для постепенного увеличения подключений (в этом случае startVUs определяет начальное значение и stages для определения промежуточных значений и продолжительности для их достижения). Для сложных сценариев может быть задан executor externally-controlled для программного управления интенсивностью запросов через cli или через REST API).

import http from 'k6/http';  export const options = {   scenarios: {     growing_scenario: {       executor: "ramping-vus",       startVUs: 100,       stages: [         { duration: '20s', target: 1000 },       ],     }   },   thresholds: {     http_req_failed: ['rate<0.005'],     http_req_duration: ['p(95)<500'],   }, };  export default function () {   http.get('http://testserver:5000'); }

Тест проверяет на возрастающем количестве подключений в течении 20 секунд от 100 до 1000 пользователей. Успешным будет считаться выполнение при менее 0.5% ошибок и при 95-м перцентиле менее 500 мс. При превышении пороговых значений будет возвращен ненулевой код возврата, который воспринимается CI/CD как ошибка выполнения шага сценария. Создадим теперь необходимые сценарии для сборки контейнера и автоматического выполнения нагрузочного тестирования и добавим функцию handleSummary(data) в тест для создания json-артефакта из результатов тестирования (и сохранения его в gitlab):

import http from 'k6/http';  export const options = {   scenarios: {     growing_scenario: {       executor: "ramping-vus",       startVUs: 100,       stages: [         { duration: '20s', target: 1000 },       ],     }   },   thresholds: {     http_req_failed: ['rate<0.005'],     http_req_duration: ['p(95)<500'],   }, };  export default function () {   http.get('http://testserver:5000'); }  export function handleSummary (data) {     return {       'stdout': textSummary(data, { indent: ' ', enableColors: true }),       './summary.json': JSON.stringify(data),     } }

И соответствующий .gitlab-ci.yml:

stages:   - build   - test  test:   services:     - name: "dmitriizolotov/testserver"       alias: testserver   stage: test   image:     name: grafana/k6     entrypoint: [""]   script:     - k6 run test.k6   artifacts:     paths:       - summary.json     expire_in: 30 days  build:   stage: build   image:     name: gcr.io/kaniko-project/executor:debug     entrypoint: [""]   script:     - mkdir -p /kaniko/.docker     - echo '{"auths":{"https://index.docker.io/v1/":{"auth":"..."}}}' >/kaniko/.docker/config.json     - >-       /kaniko/executor       --cache-dir=/cache       --context "${CI_PROJECT_DIR}"       --dockerfile "${CI_PROJECT_DIR}/Dockerfile"       --destination dmitriizolotov/testserver

Теперь нагрузочное тестирование будет использовать сервис из созданного на первом шаге контейнера и оценивать поведение под нарастающей нагрузкой. Для реалистичности gitlab-runner должен запускаться на staging-серверах, чтобы контейнер проверяемого под нагрузкой процесса работал в условиях, приближенных к production-окружению. Кроме прочего, при выполнении теста сохраняется json-артефакт, содержащий данные об итогах прохождения теста и он может использоваться в дальнейшем для анализа изменений значений по мере развития кода.

При необходимости распределенного выполнения теста сценарий будет немножко иным и будут использоваться возможности k6-operator и ресурс K6 для запуска распределенного теста на staging-кластере.

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

Как из инженера службы поддержки стать SRE? Об этом уже 7 июля расскажет мой коллега Анатолий Бурнашев на бесплатном уроке курса SRE практики и инструменты. Узнать подробнее об уроке.


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


Комментарии

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

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