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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Обеспечение надежного функционирования системы при развертывании обновления системы требует запуска тестов разного уровня - от модульных тестов отдельных компонентов до интеграционных тестов, проверяющих в 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/


Интересные статьи

Интересные статьи

Как ускорить 1С БИТРИКС и снизить кол-во запросов к БД используя ядро D7.Пример выборки элементов IBlock с пользовательскими свойствами в один запрос.
Неделю назад я делал анонс про pgSCV новом экспортере метрик для PostgreSQL. После анонса мне ожидаемо стали писать читатели с намеком что неплохо бы и дашборды сделать. Эта задача есть у...
— «... ну вот опять, снова вернулась ко мне задача из тестирования, сколько можно уже?» — Вася зло прокомментировал появившееся уведомление о новом письме.Привет, меня зо...
В этой статье описываются операции по тестированию клиентской части приложения с помощью TestProject и pytest, а также способы выполнения тестов через GitHub Actions...
В интернет-магазинах, в том числе сделанных на готовых решениях 1C-Битрикс, часто неправильно реализован функционал быстрого заказа «Купить в 1 клик».