В современном цифровом мире обеспечение высокой производительности программных приложений является ключевым фактором, позволяющим компаниям сохранять конкурентоспособность и предоставлять пользователям безупречный опыт. Пользователи предъявляют высокие требования к скорости, отзывчивости и масштабируемости приложений. Именно здесь тестирование производительности играет решающую роль.
В этой статье мы рассмотрим, что такое тестирование производительности, рассмотрим несколько подходов, которые можно использовать для лучшего понимания того, как приложение ведёт себя при различных уровнях нагрузки. Мы также познакомимся с K6 — мощным инструментом для тестирования производительности, специально разработанным для современных процессов разработки, и продемонстрируем его использование на примере. Этот инструмент предлагает очень удобный опыт для пользователей, обладающих базовыми знаниями JavaScript или TypeScript, значительно упрощая процесс тестирования производительности.
Пример можно найти на GitHub.
Что такое тестирование производительности?
Тестирование производительности — это важнейший аспект жизненного цикла разработки программного обеспечения, направленный на оценку характеристик производительности приложения в различных условиях нагрузки. Оно включает оценку того, насколько хорошо приложение работает с точки зрения скорости, стабильности, масштабируемости и использования ресурсов. Путём имитации реальных пользовательских сценариев тестирование производительности помогает выявлять потенциальные узкие места («бутылочные горлышки»), обнаруживать проблемы с производительностью и оптимизировать приложение для обеспечения наилучшего пользовательского опыта.
Существует множество подходов к тестированию производительности, среди которых можно выделить следующие:
Smoke-тестирование («дымовое»)
Это предварительное и быстрое тестирование, выполняемое для проверки того, что основные функции приложения работают так, как ожидалось. Оно направлено на выявление критических проблем на ранних стадиях разработки или развёртывания.
Нагрузочное тестирование
Предствляет собой проверку производительности приложения под ожидаемой нагрузкой. Цель — оценить поведение системы при типичных сценариях использования и условиях нагрузки. Нагрузочные тесты имитируют одновременную активность нескольких пользователей, таких как одновременный доступ к приложению, чтобы определить, как система справляется с ожидаемой нагрузкой. Это помогает выявлять узкие места, оценивать время отклика и убеждаться, что приложение способно эффективно обрабатывать предполагаемый пользовательский трафик.
Стресс-тестирование
Этот вид тестирования проводится для оценки поведения приложения в экстремальных условиях, которые выходят за рамки нормальной нагрузки. Оно включает проверку производительности приложения при больших объёмах нагрузки, данных или ограничениях ресурсов. Цель стресс-тестирования — выяснить, как приложение справляется с такими сложными ситуациями, как резкий всплеск трафика, утечки памяти или сбои серверов. Оно помогает выявлять ограничения производительности, оценивать стабильность системы и определять поведение приложения в сложных сценариях.
Мы не будем углубляться в каждый отдельный тест в нашем сценарии, но документация инструмента, который мы будем рассматривать, содержит отличные примеры конфигурации и доступна по ссылке здесь.
Теперь, когда мы получили общее представление о том, как может выглядеть тестирование производительности, давайте посмотрим, как можно протестировать приложение.
Введение в K6
K6 — это инструмент с открытым исходным кодом для нагрузочного тестирования, разработанный Grafana Labs, который упрощает тестирование производительности.
K6 включает в себя инструмент командной строки (CLI), который используется для выполнения тестов и поддержания написания сценариев на JavaScript (ES2015/ES6), которые, разумеется, можно писать на TypeScript и транспилировать (пояснение: Транспилирование — это преобразование исходного кода, написанного на одном языке, на другой язык с сопоставимым уровнем абстракции) в исполняемый JavaScript.
Подождите, разве JavaScript не считается медленным? Почему же мы используем его для выполнения тестов производительности?
На самом деле, под капотом K6 использует язык Go и встраивает среду выполнения JavaScript. Это означает, что сценарии пишутся на JavaScript/TypeScript, но выполняются на Go, что обеспечивает высокую производительность при сохранении удобства написания сценариев на популярном языке.
В оставшейся части статьи мы рассмотрим написание сценариев для K6 на TypeScript, которые будут собираться с помощью Vite. Однако тесты на K6 легко запускать и с использованием JavaScript, пример можно найти здесь.
Пример теста
Для целей этой статьи, вместо создания собственного API, я буду использовать reqres. Это общедоступный REST API, предоставляющий различные конечные точки. Чтобы ознакомиться с доступными конечными точками, смотрите спецификацию Swagger для API здесь.
Предположим, мы хотим протестировать следующую функциональность:
GET /users/{id}
PATCH /users/{id}
DELETE /users/{id}
// src/tests/reqres.ts
import { SharedArray } from "k6/data";
import { vu } from "k6/execution";
import { Options } from "k6/options";
import { deleteUser, getUser, updateUser } from "../apis/reqres";
import { users } from "../data/users";
import { logger } from "../utils/logger";
const data = new SharedArray("users", function () {
return users;
});
export const options: Options = {
scenarios: {
login: {
executor: 'per-vu-iterations',
vus: users.length,
iterations: 20,
maxDuration: '1h30m',
},
},
};
export default function test () {
// Get a random user from data that isn't currently being tested
const user = data[vu.idInTest - 1];
logger.info(
`Running iteration ${vu.iterationInInstance} for user id ${user.id} with name ${user.first_name} ${user.last_name}`
);
getUser(user);
updateUser(user);
deleteUser(user.id);
}
В этом примере кода мы получаем данные пользователя, обновляем их, а затем удаляем этого пользователя. Рассмотрим это подробнее:
options задаёт конфигурацию, которая будет использоваться для теста:
vus определяет количество виртуальных пользователей, которые запускаются одновременно. Виртуальные пользователи в K6 представляют собой имитацию реальных пользователей, взаимодействующих с вашим приложением в ходе тестирования производительности. Установленное значение 1 означает, что на протяжении всего теста будет только один пользователь (или одно выполнение функции default, описанной ниже) в единицу времени.
iterations задаёт общее количество итераций, в течение которых выполняется функция
test()
. В данном случае значение равно 1, что означает выполнение логики внутри функции один раз. Обратите внимание, что альтернативой этому параметру является duration, который вместо фиксированного числа итераций позволяет выполнять тест в течение заданного времени (например, 3 минуты).executor
: "per-vu-iterations" означает, что для каждого виртуального пользователя отдельно будет выполняться указанное количество итераций. Например, в сценарии с двумя пользователями каждый из них выполнит по одной итерации. Альтернативой является shared-iterations, где итерации делятся между всеми виртуальными пользователями. Это значит, что если установлена 1 итерация и 2 виртуальных пользователя, будет выполнена всего одна итерация.maxDuration
определяет максимальное время выполнения теста, если итерации не завершены. Это своего рода таймаут, чтобы тест не выполнялся бесконечно.
export default function () {}
— это этап жизненного цикла, который используется для выполнения тестов. Подробнее о жизненных циклах можно узнать в документации K6.
getUser
, updateUser
и deleteUser
— это функции, которые мы определили в папке src/apis/reqres
. Они выполняют вызовы API и валидируют ответы.
Для справки, данные пользователей выглядят следующим образом:
// src/data/users.ts
export const users = [
{
id: 1,
email: "george.bluth@reqres.in",
first_name: "George",
last_name: "Bluth",
avatar: "https://reqres.in/img/faces/1-image.jpg",
},
...
]
Теперь, когда мы знаем, как выглядит пример выполнения, рассмотрим одну из функций API, которую мы создали для тестирования конечной точки. Можно посмотреть, как работает функция getUser
.
// src/apis/reqres.ts
import { check } from "k6";
import http, { StructuredRequestBody } from "k6/http";
import { Trend } from "k6/metrics";
import { logWaitingTime } from "../utils/logger";
/* Здесь размещён остальной код, но он намеренно опущен! */
/**
* Функция getUser выполняет GET-запрос к эндпоинту /users/:id и проверяет,
* что ответ имеет статус 200 и что id пользователя и email совпадают
* с объектом пользователя, переданным в функцию.
* @param user
* @returns Response<User>
*/
export const getUser = (user: User): Response<User> => {
const url = reqResUrl;
const params = {
headers: {},
};
const res = http.get(`${url}/users/${user.id}`, params);
const jsonRes = res.json() as { data: User };
logWaitingTime({
metric: metrics.getUserResponseTime,
response: res,
messageType: `Get User`,
});
check(res, {
"Получение пользователя по ID: статус 200": (r) => r.status === 200,
"Получение пользователя по ID: корректный id": (_) => jsonRes.data.id === user.id,
"Получение пользователя по ID: корректный email": (_) =>
jsonRes.data.email === user.email,
});
return {
data: jsonRes.data,
statusCode: res.status,
};
};
/* Здесь размещён остальной код, но он намеренно опущен! */
Функция getUser
использует модуль http
из k6/http
, чтобы выполнить GET-запрос к конечной точке https://reqres.in/api/users/${user.id}
. Затем данные десериализуются в объект типа {data: User}
через метод res.json()
. После этого вызывается пользовательская функция logWaitingTime
, которая регистрирует время отклика, а также обновляет метрику getUserResponseTime
.
После этого используется метод check для проверки ответа API-запроса, а затем возвращается результат.
Остальная часть файла содержит аналогичные функции для updateUser
и deleteUser
, с которыми можно ознакомиться на GitHub.
Выполнение теста и просмотр результатов
Для выполнения кода можно запустить скрипт из package.json, чтобы собрать код (транспилировать из TypeScript в JavaScript с помощью Vite), а затем запустить K6:
yarn test:demo
Результат будет выглядеть следующим образом:
➜ k6-example git:(main) ✗ yarn test:demo
yarn run v1.22.19
$ npm run build && k6 run dist/tests/reqres.cjs -a localhost:6566
> k6-example@1.0.0 build
> vite build
vite v4.3.8 building for production...
✓ 5 modules transformed.
Entry module "src/tests/reqres.ts" is using named and default exports together. Consumers of your bundle will have to use `chunk.default` to access the default export, which may not be what you want. Use `output.exports: "named"` to disable this warning.
dist/tests/config.cjs 0.19 kB │ gzip: 0.15 kB
dist/tests/reqres.cjs 4.94 kB │ gzip: 1.49 kB
✓ built in 84ms
/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | (‾) |
/ __________ \ |__| \__\ \_____/ .io
execution: local
script: dist/tests/reqres.cjs
output: -
scenarios: (100.00%) 1 scenario, 1 max VUs, 1h0m30s max duration (incl. graceful stop):
* use-all-the-data: 1 iterations shared among 1 VUs (maxDuration: 1h0m0s, gracefulStop: 30s)
INFO[0000] 17:38:23.982 Running iteration 0 for user id 1 with name George Bluth source=console
running (0h00m01.3s), 0/1 VUs, 1 complete and 0 interrupted iterations
use-all-the-data ✓ [======================================] 1 VUs 0h00m01.3s/1h0m0s 1/1 shared iters
✓ Get User By Id: is 200
✓ Get User By Id: has correct id
✓ Get User By Id: has correct email
✓ Update User By Id: is 200
✓ Update User By Id: has correct updatedAt
✓ Update User By Id: has correct job title
✓ Delete User By Id: is 204
checks.........................: 100.00% ✓ 7 ✗ 0
data_received..................: 4.5 kB 3.4 kB/s
data_sent......................: 692 B 525 B/s
delete_user_response_time......: avg=601.69ms min=601.69ms med=601.69ms max=601.69ms p(90)=601.69ms p(95)=601.69ms
get_user_response_time.........: avg=22.93ms min=22.93ms med=22.93ms max=22.93ms p(90)=22.93ms p(95)=22.93ms
http_req_blocked...............: avg=28.05ms min=0s med=1µs max=84.17ms p(90)=67.33ms p(95)=75.75ms
http_req_connecting............: avg=5.43ms min=0s med=0s max=16.31ms p(90)=13.05ms p(95)=14.68ms
http_req_duration..............: avg=410.2ms min=23.68ms med=601.86ms max=605.04ms p(90)=604.4ms p(95)=604.72ms
{ expected_response:true }...: avg=410.2ms min=23.68ms med=601.86ms max=605.04ms p(90)=604.4ms p(95)=604.72ms
http_req_failed................: 0.00% ✓ 0 ✗ 3
http_req_receiving.............: avg=77.33µs min=64µs med=71µs max=97µs p(90)=91.8µs p(95)=94.4µs
http_req_sending...............: avg=295.66µs min=81µs med=118µs max=688µs p(90)=574µs p(95)=630.99µs
http_req_tls_handshaking.......: avg=9.08ms min=0s med=0s max=27.25ms p(90)=21.8ms p(95)=24.52ms
http_req_waiting...............: avg=409.82ms min=22.93ms med=601.69ms max=604.86ms p(90)=604.22ms p(95)=604.54ms
http_reqs......................: 3 2.277847/s
iteration_duration.............: avg=1.31s min=1.31s med=1.31s max=1.31s p(90)=1.31s p(95)=1.31s
iterations.....................: 1 0.759282/s
update_user_response_time......: avg=604.86ms min=604.86ms med=604.86ms max=604.86ms p(90)=604.86ms p(95)=604.86ms
vus............................: 1 min=1 max=1
vus_max........................: 1 min=1 max=1
✨ Done in 3.48s.
Как видно, в консоли отображается один лог, связанный с итерацией тестовой функции для первого пользователя из нашего набора данных:INFO[0000] 17:38:23.982 Running iteration 0 for user id 1 with name George Bluth source=console
.
Мы видим, что все проверки прошли успешно, что подтверждается выводом.
✓ Get User By Id: is 200
✓ Get User By Id: has correct id
✓ Get User By Id: has correct email
✓ Update User By Id: is 200
✓ Update User By Id: has correct updatedAt
✓ Update User By Id: has correct job title
✓ Delete User By Id: is 204
checks.........................: 100.00% ✓ 7 ✗ 0
В тесте происходит много всего, но я не буду углубляться в детали, так как это сильно увеличило бы объём статьи. Описания всех метрик по умолчанию можно найти здесь.
Также можно разбить результаты по времени отклика API на следующие параметры:
Get User:
- Avg: 22.93ms
- Min: 22.93ms
- Max: 22.93ms
Update User:
- Avg: 604.86ms
- Min: 604.86ms
- Max: 604.86ms
Delete User:
- Avg: 601.69ms
- Min: 601.69ms
- Max: 601.69ms
Обратите внимание, что среднее, минимальное и максимальное время отклика для каждого API совпадают. Вы, вероятно, догадались, что это связано с тем, что мы выполнили всего одну итерацию для одного пользователя, то есть отправили по одному запросу на каждый API. Это вполне приемлемо для первичной проверки работоспособности (sanity-тестирования), но не даёт реального представления о производительности.
Если мы захотим увеличить количество одновременных пользователей и итераций для каждого из них, можно изменить объект options
в тесте, задав 5 виртуальных пользователей (vus) и 10 итераций. Это приведёт к выполнению нашего сценария 10 раз для 5 пользователей одновременно.
export const options: Options = {
scenarios: {
"use-all-the-data": {
executor: "per-vu-iterations",
vus: 5,
iterations: 10,
maxDuration: "1h",
},
},
};
running (0h00m13.3s), 0/5 VUs, 50 complete and 0 interrupted iterations
use-all-the-data ✓ [======================================] 5 VUs 0h00m13.3s/1h0m0s 50/50 iters, 10 per VU
✓ Get User By Id: is 200
✓ Get User By Id: has correct id
✓ Get User By Id: has correct email
✓ Update User By Id: is 200
✓ Update User By Id: has correct updatedAt
✓ Update User By Id: has correct job title
✓ Delete User By Id: is 204
checks.........................: 100.00% ✓ 350 ✗ 0
data_received..................: 84 kB 6.3 kB/s
data_sent......................: 11 kB 788 B/s
delete_user_response_time......: avg=606.32ms min=594.72ms med=601.36ms max=708.31ms p(90)=611.38ms p(95)=620.81ms
get_user_response_time.........: avg=38.29ms min=20.83ms med=23.44ms max=713.41ms p(90)=27.34ms p(95)=30.57ms
http_req_blocked...............: avg=3.03ms min=0s med=0s max=93.28ms p(90)=1µs p(95)=1µs
http_req_connecting............: avg=560.78µs min=0s med=0s max=17.22ms p(90)=0s p(95)=0s
http_req_duration..............: avg=420.87ms min=20.95ms med=600.01ms max=713.48ms p(90)=611.85ms p(95)=687.26ms
{ expected_response:true }...: avg=420.87ms min=20.95ms med=600.01ms max=713.48ms p(90)=611.85ms p(95)=687.26ms
http_req_failed................: 0.00% ✓ 0 ✗ 150
http_req_receiving.............: avg=42.53µs min=10µs med=31.5µs max=578µs p(90)=67.1µs p(95)=74.64µs
http_req_sending...............: avg=64.4µs min=22µs med=59µs max=374µs p(90)=96µs p(95)=117.94µs
http_req_tls_handshaking.......: avg=1.12ms min=0s med=0s max=35.67ms p(90)=0s p(95)=0s
http_req_waiting...............: avg=420.77ms min=20.83ms med=599.83ms max=713.41ms p(90)=611.76ms p(95)=687.13ms
http_reqs......................: 150 11.278302/s
iteration_duration.............: avg=1.27s min=1.22s med=1.23s max=2.03s p(90)=1.34s p(95)=1.4s
iterations.....................: 50 3.759434/s
update_user_response_time......: avg=617.69ms min=596.34ms med=604.34ms max=708.41ms p(90)=687.09ms p(95)=690.07ms
vus............................: 1 min=1 max=5
vus_max........................: 5 min=5 max=5
После выполнения такого теста видно, что все проверки (350 штук) завершились успешно за 13,3 секунды. Мы также можем увидеть для каждого API следующие детали:
Get User:
- Avg: 38.29ms
- Min: 20.83ms
- Max: 713.41ms
Update User:
- Avg: 617.68ms
- Min: 596.34ms
- Max: 708.41ms
Delete User:
- Avg: 606.32ms
- Min: 594.62ms
- Max: 708.31ms
Важно отметить, что под капотом все эти метрики представляют собой метрики Prometheus, которые могут быть интегрированы с различными инструментами мониторинга. Использование таких инструментов упрощает визуализацию и понимание результатов, включая время отклика, уровень ошибок и т. д., на протяжении теста.
Вывод
Тестирование производительности играет ключевую роль в создании высокопроизводительных приложений в сегодняшнем конкурентном цифровом мире. Оно помогает оценить скорость, стабильность, масштабируемость и использование ресурсов приложения.
K6 — это инструмент, который очень просто освоить для написания тестов производительности. Надеюсь, после прочтения этой статьи вам станет интересно попробовать его, чтобы упростить процесс тестирования ваших приложений.
Всех, кому интересна тема нагрузочного тестирования, приглашаем на открытый урок «Введение в Gatling: практика организации проекта».
На занятии познакомимся с устройством Gatling, организацией проектов на Gatling и применим его на практике. Вы узнаете, как эффективно использовать Gatling для тестирования производительности и создадите свои первые проекты. Записаться можно по ссылке.