Введение в нагрузочное тестирование на k6

Материалы
Видео доступно по ссылке
Что будем проверять
Для примера берем простой Go-сервис с одним тяжелым endpoint-ом.
Внутри есть искусственная тяжелая операция:
func heavyComputation(userID string) string {
sum := 0
for i := 0; i < 1e8; i++ {
sum += i + rand.Intn(100)
}
return fmt.Sprintf("User %s - Сумма: %d", userID, sum)
}
И endpoint без кеша:
func noCacheHandler(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user")
if userID == "" {
userID = "guest"
}
result := heavyComputation(userID)
fmt.Fprintf(w, "Источник: без кеша | Результат: %s", result)
}
Это не пример идеального production-кода. Это удобная болванка для воркшопа: есть тяжелая операция, ее легко дергать под нагрузкой и потом видно, как меняются метрики.
Первый тест на k6
Минимальный сценарий выглядит так:
import http from 'k6/http';
import { sleep } from 'k6';
export const options = {
vus: 20,
duration: '10s',
};
export default function () {
http.get('http://localhost:8080/no-cache');
sleep(1);
}
Запуск:
k6 run load.js
Тут важно не путать vus и RPS.
vus - это виртуальные пользователи, то есть сколько независимых исполнителей крутит сценарий. RPS - это уже результат: сколько запросов в секунду реально получилось. Если каждый запрос занимает долго, большой vus не превращается автоматически в большой RPS.
В первом замере из презентации получилось:
http_req_duration..............: avg=1.76s
http_reqs......................: 81 6.824519/s
http_req_failed................: 0.00%
То есть 20 виртуальных пользователей дали около 6.8 запросов в секунду. Это ожидаемо: endpoint тратит время на тяжелое вычисление, а сценарий еще делает sleep(1).
Какие метрики смотреть первыми
На старте я бы не пытался сразу построить идеальную модель нагрузки.
Сначала достаточно понять несколько вещей:
http_reqs- сколько запросов реально прошлоhttp_req_duration- сколько в среднем и по хвостам отвечает сервисhttp_req_failed- сколько запросов упалоiteration_duration- сколько длится полный проход сценария
Если уже на этом шаге видно, что endpoint отвечает секунды, не нужно прятать это за красивыми графиками. Сначала разбираемся, почему конкретная операция тяжелая.
Добавим кеш
В примере оптимизация максимально простая: кладем результат тяжелого вычисления в память и читаем его под sync.RWMutex.
var (
cache = make(map[string]string)
cacheMutex = sync.RWMutex{}
)
func withCacheHandler(w http.ResponseWriter, r *http.Request) {
cacheMutex.RLock()
item, found := cache["heavyResult"]
cacheMutex.RUnlock()
if found {
fmt.Fprintf(w, "Источник: кеш | Результат: %s", item)
return
}
result := heavyComputation("guest")
cacheMutex.Lock()
cache["heavyResult"] = result
cacheMutex.Unlock()
fmt.Fprintf(w, "Источник: расчёт | Результат: %s", result)
}
Для кешированного endpoint-а сценарий уже другой:
import http from 'k6/http';
export const options = {
scenarios: {
cached_users: {
executor: 'constant-vus',
exec: 'cached',
vus: 300,
duration: '10s',
},
},
};
export function cached() {
http.get('http://localhost:8080/cache');
}
После этого результат меняется радикально:
http_req_duration..............: avg=3.4ms
http_reqs......................: 842707 84257.205366/s
http_req_failed................: 0.01%
В презентации это сформулировано как рост примерно в 12000 раз: было около 6.8 RPS, стало около 84257 RPS.
Что важно не перепутать
Этот пример не доказывает, что кеш всегда решает проблемы производительности.
Он показывает другое:
- сначала делаем измерение
- потом меняем код или архитектуру
- потом повторяем тот же сценарий
- сравниваем не ощущения, а цифры
В реальном сервисе кеш может быть неправильным решением: данные могут устаревать, память может закончиться, ключ может быть слишком грубым, а горячий path может упереться вообще не в CPU. Но без теста это все легко превращается в спор мнениями.
С чего начать у себя
Минимальный полезный алгоритм:
- Выбрать один пользовательский сценарий, а не “нагрузить все”.
- Написать маленький k6-скрипт.
- Зафиксировать параметры:
vus,duration, endpoint, данные запроса. - Сохранить результат первого запуска.
- Сделать одно изменение.
- Повторить тест с теми же условиями.
И только потом усложнять: добавлять thresholds, разные сценарии, ramping нагрузку, профилирование, отдельное окружение и нормальные отчеты.
Первый тест должен быть простым. Его задача - дать точку отсчета.
kirya522.tech — Блог Кирилла Грищука о разработке