Почему тестирование производительности API важно
Каждое современное приложение зависит от API. Когда API замедляется, весь пользовательский опыт деградирует — страницы загружаются дольше, мобильные приложения зависают, а интеграции получают таймауты. Тестирование производительности API гарантирует, что ваши endpoint-ы справляются с ожидаемым и пиковым трафиком без деградации пользовательского опыта.
В отличие от тестирования производительности UI, тестирование производительности API изолирует бэкенд. Вы исключаете рендеринг браузера, сетевую вариативность и фронтенд-код из уравнения. Это даёт точные измерения того, как сервер обрабатывает запросы.
Ключевые метрики производительности
Прежде чем написать хотя бы один тест, нужно понять, что именно вы измеряете.
Throughput (Requests Per Second)
Throughput измеряет, сколько запросов ваша API может обработать в секунду. REST-endpoint, возвращающий профили пользователей, может поддерживать 5,000 RPS на хорошо настроенном сервере. Если ожидаемый пиковый трафик составляет 2,000 RPS, у вас есть комфортный запас.
Перцентили латентности
Средняя латентность обманчива. Если 95% запросов выполняются за 50мс, но 5% — за 3 секунды, среднее может быть 200мс — что скрывает ужасный опыт для этих 5%.
Используйте перцентили:
| Перцентиль | Значение |
|---|---|
| p50 (медиана) | Половина запросов быстрее этого значения |
| p90 | 90% запросов укладываются в это время |
| p95 | Порог, на который ориентируются большинство SLA |
| p99 | Отражает почти наихудший случай |
Процент ошибок
Доля запросов, возвращающих ошибки 5xx или таймауты под нагрузкой. Здоровая API поддерживает менее 0,1% ошибок при ожидаемой нагрузке. При увеличении нагрузки сверх ёмкости процент ошибок резко растёт — эта точка перегиба показывает реальную ёмкость.
Конкурентность
Количество одновременных соединений, которые обрабатывает API. Это отличается от throughput: API может обрабатывать 1,000 RPS с 50 конкурентными соединениями (быстрые ответы) или 1,000 RPS с 500 конкурентными соединениями (медленные ответы).
Типы тестов производительности API
Нагрузочное тестирование (Load Test)
Симулирует ожидаемый production-трафик. Проверяет, что производительность соответствует SLA при нормальных условиях.
// Пример load test на k6
export const options = {
stages: [
{ duration: '2m', target: 100 }, // нарастание
{ duration: '5m', target: 100 }, // стабильная нагрузка
{ duration: '2m', target: 0 }, // снижение
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.01'],
},
};
Стресс-тест (Stress Test)
Выходит за рамки ожидаемой нагрузки для нахождения точек отказа. Непрерывно увеличивает число виртуальных пользователей, пока API не начнёт деградировать или откажет.
Тест пиковой нагрузки (Spike Test)
Симулирует внезапные всплески трафика — как при flash-распродаже или вирусном посте в соцсетях. Проверяет, как API справляется с мгновенными скачками и восстанавливается после них.
Тест выносливости (Soak Test)
Прогоняет умеренную нагрузку часами (4-12+). Выявляет утечки памяти, исчерпание пула соединений, рост лог-файлов и другие проблемы, проявляющиеся только со временем.
Настройка k6 для тестирования API
k6 идеален для тестирования производительности API, потому что он лёгкий, программируется на JavaScript и предоставляет детальные метрики из коробки.
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 50,
duration: '5m',
thresholds: {
http_req_duration: ['p(95)<300', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get('https://api.example.com/users');
check(res, {
'статус 200': (r) => r.status === 200,
'время ответа < 500мс': (r) => r.timings.duration < 500,
'тело содержит данные': (r) => r.json().data !== undefined,
});
sleep(1);
}
Сценарии с несколькими endpoint-ами
Реальный трафик затрагивает множество endpoint-ов. Моделируйте это, распределяя запросы пропорционально паттернам production-трафика.
import http from 'k6/http';
import { group, sleep } from 'k6';
export default function () {
group('Просмотр товаров', () => {
http.get('https://api.example.com/products');
sleep(0.5);
});
group('Детали товара', () => {
const id = Math.floor(Math.random() * 100) + 1;
http.get(`https://api.example.com/products/${id}`);
sleep(0.3);
});
group('Поиск', () => {
http.get('https://api.example.com/search?q=widget');
sleep(0.5);
});
}
Интерпретация результатов
После прогона k6 вы получаете сводку вроде этой:
http_req_duration..........: avg=120ms min=15ms med=95ms max=4200ms p(90)=250ms p(95)=380ms
http_req_failed............: 0.23% ✓ 46 ✗ 19954
http_reqs..................: 20000 666.5/s
На что обращать внимание:
- p95 vs threshold — 380мс укладывается в SLA 500мс? Да, проходите.
- Разрыв p95 и среднего — 380мс vs 120мс среднее. Хвостовая латентность в 3 раза больше среднего, что указывает на медленные пути для некоторых запросов.
- Процент ошибок — 0,23% может казаться низким, но если SLA требует < 0,1%, вы не проходите.
- Максимальная латентность — 4,200мс означает, что хотя бы один запрос занял более 4 секунд. Расследуйте причину.
Упражнение: Полный набор тестов производительности API
Создайте комплексный тест производительности для REST API с этими требованиями:
Подготовка
Используйте публичную API JSONPlaceholder (https://jsonplaceholder.typicode.com) или поднимите локальную API.
Часть 1: Базовый тест
Создайте скрипт k6, устанавливающий базовую производительность:
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Trend, Rate, Counter } from 'k6/metrics';
// Кастомные метрики
const listLatency = new Trend('list_latency');
const detailLatency = new Trend('detail_latency');
const createLatency = new Trend('create_latency');
const errorRate = new Rate('errors');
const requestCount = new Counter('total_requests');
export const options = {
scenarios: {
baseline: {
executor: 'constant-vus',
vus: 10,
duration: '2m',
},
},
thresholds: {
list_latency: ['p(95)<500'],
detail_latency: ['p(95)<300'],
create_latency: ['p(95)<800'],
errors: ['rate<0.01'],
},
};
export default function () {
// GET /posts (список)
let res = http.get('https://jsonplaceholder.typicode.com/posts');
listLatency.add(res.timings.duration);
check(res, { 'список 200': (r) => r.status === 200 }) || errorRate.add(1);
requestCount.add(1);
sleep(0.5);
// GET /posts/:id (детали)
const id = Math.floor(Math.random() * 100) + 1;
res = http.get(`https://jsonplaceholder.typicode.com/posts/${id}`);
detailLatency.add(res.timings.duration);
check(res, { 'детали 200': (r) => r.status === 200 }) || errorRate.add(1);
requestCount.add(1);
sleep(0.3);
// POST /posts (создание)
res = http.post('https://jsonplaceholder.typicode.com/posts',
JSON.stringify({ title: 'Тест', body: 'Содержимое', userId: 1 }),
{ headers: { 'Content-Type': 'application/json' } }
);
createLatency.add(res.timings.duration);
check(res, { 'создание 201': (r) => r.status === 201 }) || errorRate.add(1);
requestCount.add(1);
sleep(0.5);
}
Часть 2: Нагрузочный тест с нарастанием
Расширьте базовый тест для симуляции production-подобного трафика:
export const options = {
scenarios: {
load_test: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '2m', target: 50 },
{ duration: '5m', target: 50 },
{ duration: '1m', target: 100 },
{ duration: '3m', target: 100 },
{ duration: '2m', target: 0 },
],
},
},
thresholds: {
http_req_duration: ['p(95)<1000'],
http_req_failed: ['rate<0.05'],
},
};
Часть 3: Spike Test
Симулируйте внезапный всплеск трафика:
export const options = {
scenarios: {
spike: {
executor: 'ramping-vus',
startVUs: 0,
stages: [
{ duration: '1m', target: 20 },
{ duration: '10s', target: 200 },
{ duration: '2m', target: 200 },
{ duration: '10s', target: 20 },
{ duration: '2m', target: 20 },
{ duration: '30s', target: 0 },
],
},
},
};
Задания по анализу
После выполнения каждого теста:
- Запишите таблицу метрик — Скопируйте p50, p90, p95, p99 и максимальную латентность для каждого endpoint.
- Сравните базу и нагрузку — Насколько увеличился p95, когда вы добавили больше виртуальных пользователей?
- Проанализируйте spike — Поддержала ли API приемлемую латентность во время spike? Как быстро она вернулась к базовым показателям после снижения spike?
- Определите узкое место — Какой endpoint деградировал быстрее всего под нагрузкой? Почему так может быть? (Подсказка: endpoint-ы списков обычно запрашивают больше данных, чем endpoint-ы деталей.)
Ожидаемые наблюдения
- Endpoint-ы списков (
GET /posts) скорее всего покажут более высокую латентность, чем endpoint-ы деталей (GET /posts/:id), потому что возвращают большие payload-ы. - POST-запросы обычно имеют более высокую латентность, так как включают операции записи.
- Во время spike test вы должны наблюдать рост латентности, затем стабилизацию или дальнейшую деградацию, если API не справляется с нагрузкой.
- После снижения spike латентность должна вернуться к значениям, близким к базовым — если этого не происходит, у API проблема с восстановлением.
Чек-лист тестирования производительности
Используйте этот чек-лист перед тем, как объявить API готовой к production:
- Зафиксирована базовая латентность для всех критических endpoint-ов
- Load test проходит пороги SLA при ожидаемом пиковом трафике
- Stress test определяет точку отказа (при каком RPS начинается деградация)
- Spike test подтверждает восстановление в приемлемое время
- Soak test (4+ часа) не показывает утечек памяти или постепенной деградации
- Процент ошибок остаётся ниже порога SLA при ожидаемой нагрузке
- Пулы соединений к базе данных не исчерпываются под нагрузкой
- Вызовы к сторонним API имеют таймауты и circuit breaker-ы