Содержание

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

Материалы

    / [pdf]

Видео доступно по ссылке

Замечание
Нагрузочное тестирование полезно не только для больших релизов. Иногда достаточно маленького локального эксперимента, чтобы перестать спорить ощущениями и увидеть конкретные числа.

Что будем проверять

Для примера берем простой 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. Но без теста это все легко превращается в спор мнениями.

С чего начать у себя

Минимальный полезный алгоритм:

  1. Выбрать один пользовательский сценарий, а не “нагрузить все”.
  2. Написать маленький k6-скрипт.
  3. Зафиксировать параметры: vus, duration, endpoint, данные запроса.
  4. Сохранить результат первого запуска.
  5. Сделать одно изменение.
  6. Повторить тест с теми же условиями.

И только потом усложнять: добавлять thresholds, разные сценарии, ramping нагрузку, профилирование, отдельное окружение и нормальные отчеты.

Первый тест должен быть простым. Его задача - дать точку отсчета.