Мы в Altenar разрабатываем программное обеспечение для беттинга. Как и для многих разработчиков для нас очень важна производительность, в связи с этим особое внимание мы уделяем нагрузочному тестированию. Под катом подробный рассказ про наш опыт от инженера по автоматизации тестирования Даниила Матафонова (QA Automation Engineer) daniilmatafonov.

Долгое время мы использовали JMeter. Но ни команда DevOps, ни .NET разработчики не проявляли горячего интереса к его интерфейсу. Прочитать тестовый скрипт было невозможно.

Так мы пришли к выводу, что нам нужно найти оптимальный рабочий инструмент, который бы удовлетворял наиболее важным для нас требованиям:

  • Гибкость
  • Масштабируемость
  • Простота создания тестового сценария на языке понятном представителям из разных департаментов
  • Поддержка gRPC

Анализ инструментов


Мы провели сравнительный анализ наиболее актуальных инструментов для нагрузочного тестирования. У каждого из них есть свои преимущества и недостатки.



k6 стал для нас оптимальным инструментом по следующим причинам:

  • Поддержка протоколов HTTP 1.1/1.2, gRPC
  • Кроссплатформенность
  • JavaScript — язык для реализации тестов
  • Возможность запуска тестов из облачной инфраструктуры k6 cloud.
  • Выгрузка статистических метрик в различные хранилища данных (Amazon CloudWatch, Apache Kafka, Cloud, CSV, Datadog, influxDB, JSON, New Relic, StatsD )
  • Активное community

Особенности k6


Сценарии

В k6 реализованы гибкие функции настройки и запуска тестов, называемые сценариями. Сценарии могут работать последовательно или параллельно, что очень упрощает разработку тестов и позволяет моделировать расширенные шаблоны выполнения, которые могут лучше имитировать реальный трафик.

Исполнители

Исполнители нужны для планирования и управления ресурсами Vus (потоки выполнения, именуемые виртуальными пользователями) и iterations - итераций прогонов нагрузочных тестов. Исполнитель выбирается в зависимости от типа трафика, который вы хотите смоделировать для тестирования сервисов.

В k6 представлены следующие типы исполнителей:

  • Shared iterations
  • Per VU iterations
  • Constant VUs
  • Ramping VUs
  • Constant Arrival Rate
  • Ramping Arrival Rate
  • Externally Controlled

Изначально k6 работает в агрессивном режиме и пытается отправить наибольшее количество запросов в единицу времени. Мы изменили это поведение, использовав исполнитель Constant VUs. Он позволяет генерировать нагрузку с фиксированным количеством Vus на старте и параметр minIteration Duration, задает лимит для итераций и ограничение по количеству запросов для каждого VUs. Более подробную информацию можно найти в документации, раздел Executors.

Метрики

В k6 используются built-in:

  • http_reqs
  • vus
  • iterations
  • iteration_duration
  • data_received
  • data_sent
  • checks

и custom метрики следующих типов:

  • Counter — кумулятивная метрика, суммирует добавленные значения.
  • Gauge — метрика, которая хранит минимальное, максимальное и последнее добавленные к ней значения.
  • Rate — метрика, которая отслеживает процент добавленных значений, отличных от нуля.
  • Trend — метрика, позволяющая вычислять статистику по добавленным значениям (min, max, average и процентили).

Кроме встроенных мы используем кастомные метрики типов Trend для хранения времени обработки запросов в миллисекундах и Rate — для хранения частоты возникновения ошибок API. Документация по metric types.

Тестирование


Выполним нагрузочное тестирование на примере метода одного из компонентов системы с использованием k6. Рассмотрим как настроить интеграцию с InfluxDB — одним из наиболее популярных хранилищ метрик, собрать нагрузочные метрики и отобразить их в Grafana.

Конфигурационный скрипт:

config.js

export let options = {
	httpDebug: 'full',
	minIterationDuration: '1m',
	scenarios: {
		main_scenario: {
			executor: 'constant-vus',
			vus: 4000,
			exec: 'frontendMain',
			duration: '5m',
			gracefulStop: '0s',
		}
	}
};

Содержит объявление объекта options с настроенным сценарием и исполнителем.

Скрипт компонента:

При реализации вызова любого метода API достаточно использовать набор обязательных входных параметров и функцию get() или post() в зависимости от типа http-запроса.

export function getAllSports(langIds) {
	const langId = getRndValueFromArray(langIds);
	const currentDate = new Date().toISOString();
	const weekAfterDate = addDays(currentDate, 7);
	let urlQuery = buildQuery({
		timezoneOffset: "-180",
		langId: langId,
		skinName: "betsonic",
		configId: `${__ENV.CONFIG}`,
		culture: "en-GB",
		countryCode: "RU",
		deviceType: "Desktop",
		numformat: "en",
		period: 'periodall',
		startDate: currentDate,
		endDate: weekAfterDate,
	});
	let resp = http.get(capiUrl + `/Sportsbook/GetAllSports?${urlQuery}`);
	return resp;
};

default функция:

export function frontendMain() {

	group("GetAllSports", function () {
		const allSportsResult = getAllSports(langIds);
		let allSportsCheckResult = check(allSportsResult, {
			"status is 200 OK": (allSportsResult) => allSportsResult.status === 200,
			"content-type is application/json": (allSportsResult) => allSportsResult.headers['Content-Type'] === "application/json; charset=utf-8",
		});
		errorRate.add(!allSportsCheckResult);
		let allSportsDuration = getTrend("GetAllSports");
		if (Object.keys(allSportsDuration).length !== 0) {
			allSportsDuration.add(allSportsResult.timings.duration);
		}
	});

В методе реализован непосредственно сценарий теста. Функция group используется для группировки кода по тестируемым методам. С помощью функции check реализуются проверки на статус-код ответа сервера и content-type.

Интеграция с InfluxDB


Для выгрузки данных в InfluxDB используется параметр -o influxdb. В качестве значения передается адрес хоста, порт, имя базы.

Сборка и запуск тестов

Собираем тесты с помощью системы сборки webpack.



Запускаем тест с помощью bash скрипта:



Через параметр -e передаются переменные среды. В тестах мы используем их для настройки тестового конфига, категорий событий, адресов тестируемых компонентов API.

Логи и метрики


Далее представлен фрагмент лога в процессе выполнения тестов.



По завершению работы в папке logs будет сформирован отчет по тестам, содержащий метрики и дополнительную статистику.

- GetAllSports

- GetAllSports

? status is 200 OK? content — type is application / json

GetAllSports...............: avg = 1383.930035 min = 6.675649 med = 1258.454661 max = 4541.098292 p(90) = 2674.549785 p(95) = 2930.894705

checks.....................: 100.00 % ?80000? 0
data_received..............: 77 MB 253 kB / s
data_sent..................: 12 MB 41 kB / s
errors.....................: 0.00 % ?0? 40000
group_duration.............: avg = 1.01 s min = 6.77 ms med = 750.99 ms max = 5.24 s p(90) = 2.2 s p(95) = 2.8 s
http_req_blocked...........: avg = 48.88 ms min = 188 ns med = 510 ns max = 749.13 ms p(90) = 34.35 ms p(95) = 439.23 ms
http_req_connecting........: avg = 6.7 ms min = 0 s med = 0 s max = 112.68 ms p(90) = 3.13 ms p(95) = 67.97 ms
http_req_duration..........: avg = 933.7 ms min = 6.27 ms med = 728.89 ms max = 4.54 s p(90) = 2.07 s p(95) = 2.67 s
http_req_receiving.........: avg = 3.72 ms min = 33.03 µs med = 188.14 µs max = 384.06 ms p(90) = 4.67 ms p(95) = 15.53 ms
http_req_sending...........: avg = 92.47 µs min = 19.12 µs med = 62.73 µs max = 94.61 ms p(90) = 122.85 µs p(95) = 157.34 µs
http_req_tls_handshaking...: avg = 35.66 ms min = 0 s med = 0 s max = 426.84 ms p(90) = 26.19 ms p(95) = 353.95 ms
http_req_waiting...........: avg = 929.88 ms min = 8.54 µs med = 727.75 ms max = 4.54 s p(90) = 2.06 s p(95) = 2.67 s
http_reqs..................: 20000 131.488876 / s
iteration_duration.........: avg = 2.03 s min = 17.19 ms med = 2.03 s max = 5.25 s p(90) = 3.51 s p(95) = 3.92 s
iterations.................: 20000 65.744438 / s
vus........................: 10 min = 10 max = 4000
vus_max....................: 4000 min = 4000 max = 4000

Проанализируем наиболее значимые показатели по компоненту:

GetAllSports...............: avg = 1383.930035 — среднее время выполнения метода в ms(миллисекундах).
errors.....................: 0.00 % — ошибок API не выявлено.
http_reqs..................: 20000 131.488876 / s — общее число выполненных запросов
vus........................: 10 min = 10 max = 4000 — количество vus.

Проанализируем наиболее значимые показатели по компоненту:

GetAllSports...............: avg=1383.930035 — среднее время выполнения метода в ms (миллисекундах).
errors.....................: 0.00% — ошибок API не выявлено.
http_reqs..................: 20000 131.488876/s — общее число выполненных запросов
vus........................: 10 min=10 max=4000 — количество vus.

Отчеты Grafana


Для более наглядного представления метрик мы создали Grafana board. На графиках представлена статистика по выполненному нагрузочному тесту.






Выводы


Нам удалось построить процесс проведения нагрузочного тестирования, вовлечь в него команду разработки и DevOps для совместного анализа и запуска тестов, а также собрать нагрузочные метрики, отобразить результаты в виде графиков в Grafana.