load-tester Все курсы

✻ Урок 8.4 · Тема 8: Теория производительности

Профиль нагрузки, SLO и методика испытаний

⏱ 3 ч

Зачем это нужно

Ты умеешь мерить задержку и пропускную способность (8.1), понимаешь очереди и колено (8.2) и знаешь типы тестов (8.3). Остались два вопроса, на которых спотыкаются даже опытные: с какими числами гонять тест и как понять, что сервис его прошёл.

Я однажды выбрал нагрузку «1000 пользователей, потому что красиво». Тест провалился, команда месяц ускоряла то, что на деле никому не мешало. В другой раз взял число поменьше, тест прошёл, а в первый реальный пик сервис лёг. Числа «из головы» плохи в обе стороны. Правильные берутся из данных о реальном использовании: сколько людей приходит в пиковый час, что они делают и в каких пропорциях. Это называется профилем нагрузки. А «прошёл или нет» решают цели, записанные заранее: SLO. Вместе они ложатся в один документ, методику испытаний, по которому любой может повторить тест и получить сравнимый результат.

Шаг проекта: ты соберёшь «реальный» трафик на «Магазине», получишь из Prometheus доли действий и скриптом profile_calc.py пересчитаешь их в RPS по каждому маршруту. Затем запишешь SLO и оформишь всё в ~/perf-lab/08-theory/methodology.md. Этот файл станет основой для тем 9-11 (скрипты Locust и k6 повторят профиль) и для отчёта (урок 12.1).

Что нужно знать

  • Задержка, p95, ошибки, baseline: урок 8.1.
  • Закон Литтла, рабочая нагрузка 60–70% предела: урок 8.2. Пределы логина и каталога из твоего baseline.md нужны для сравнения.
  • Типы тестов и критерии успеха: урок 8.3.
  • PromQL rate, increase, sum by, histogram_quantile: урок 7.2.

Картина целиком

Врач не назначает «побольше анализов», а идёт от жалобы: что болит, как давно, сколько лет пациенту. У нагрузочного тестирования та же цепочка: кто пользуется (данные), что делает (профиль), что считаем нормой (SLO), как проверяем (тест), что решаем (вывод).

flowchart TD
    D["Данные: логи и<br/>метрики за период"] --> P["Профиль: сколько,<br/>что и в каких долях"]
    P --> R["Расчёт: RPS по<br/>каждому действию"]
    S["Цели: SLO<br/>и критерии успеха"] --> T["Методика и тест"]
    R --> T
    T --> V["Вердикт: прошёл<br/>или что чинить"]

В тест ведут два входа. Один говорит, какая нагрузка (из данных), другой, какой результат считаем хорошим (из целей). Ошибка в любом обесценивает тест, поэтому мы разбираем оба подробно, а не торопимся запускать. Профиль состоит из состава (какие действия и в каких долях) и объёма (сколько их в пиковый час). RPS получается арифметикой плюс запас на пик и рост. А цель это SLO: измеримое обещание, а не «чтобы было быстро».

Теория

Что такое профиль нагрузки

«Нагрузка» сама по себе не число. 20 RPS карточек товара и 20 RPS логинов это разные испытания. Первое сервер держит с запасом, второе в пять раз выше его предела (логин считается 0,26 с на процессоре, см. 8.2). Поэтому нагрузку описывают профилем (workload model): что делают пользователи и сколько их.

Как расписание кинотеатра. «Сегодня 500 зрителей» ничего не говорит. Важно, сколько на какой сеанс и когда пик: перед вечерним сеансом, а не днём.

В профиль входят четыре вещи. Пользовательские сценарии: цепочки действий вроде «зашёл, открыл список, открыл карточку, добавил в корзину, оформил». Состав: доля каждого действия, и на сайте читают намного больше, чем покупают. Объём: сколько визитов (сессий) приходится на пиковый час. И форма во времени: разгон и полка из урока 8.3.

Есть ещё данные. Если все 100 виртуальных пользователей входят под одним логином и открывают одну карточку, ты измеряешь кэш, а не сервис. Поэтому профиль включает разнообразие: разные пользователи, разные товары.

Главное: профиль нагрузки это что делают пользователи и сколько их: сценарии, состав, объём и форма во времени, а не одно число RPS.

Откуда взять эти числа, чтобы не выдумывать?

Откуда брать данные

Ты не угадаешь, что логинов в 10 раз меньше, чем просмотров каталога, или что большинство уходит, не дойдя до корзины. Поэтому первый шаг всегда один: «покажите, что происходило».

Лучший источник это метрики сервиса. У «Магазина» есть http_requests_total{method,route,status} с разбивкой по маршрутам, из неё считаем доли и объёмы. Дальше идут логи (запросы с путями, пользователями и временем, по ним видны сессии) и аналитика бизнеса (сколько было визитов и какой процент посетителей дошёл до заказа). Если трафика ещё нет, спроси у бизнеса, сколько людей ждут, и честно пометь «оценка, пересмотреть после запуска».

Окно данных должно содержать пик: неделя, лучше месяц, и внутри него самый нагруженный час. Среднее за сутки бесполезно: ночью трафика почти нет, и среднее занижено в разы.

Вот объём запросов по маршрутам за час:

sum by (route) (increase(http_requests_total[1h]))

http_requests_total счётчик, он только растёт. increase(...[1h]) показывает, на сколько он вырос за последний час. sum by (route) складывает по всем статусам и методам и оставляет маршруты. Чтобы найти именно пиковый час, в Grafana строят график sum(increase(http_requests_total[1h])) за неделю и ищут максимум.

Прикинь сам: почему нельзя взять среднее число запросов за сутки как цель теста?

Нагрузка неравномерна: вечером пик в несколько раз выше среднего. Сервис, проверенный на среднем, в пик окажется перегружен. Цель теста это пиковый час. Осторожно: запросы и сессии разные вещи (запросов в десять раз больше), а пиковая минута не то же самое, что пиковый час. Профиль считают по часу, а всплески внутри часа закрывают запасом, о нём ниже.

Главное: числа профиля берут из реальных метрик за период с пиком и считают по пиковому часу, а не по среднему за сутки.

Объём есть. Теперь состав: из чего он складывается.

Состав: доли действий

Действия разные по цене. Карточка товара это одно чтение, около 5 мс. Логин около 250 мс процессорного времени. Заказ это транзакция плюс оплата. Неверные пропорции либо недогрузят самое слабое место, либо перегрузят его в пять раз.

Сессия (визит) это одно посещение: человек пришёл, вошёл в аккаунт, походил по страницам и ушёл. Состав считают в действиях на сессию: сколько раз за визит средний посетитель делает то или иное. Объём запросов по маршрутам делим на число сессий. Для «Магазина» сессий столько же, сколько логинов: логин один раз на визит. На стенде после сбора данных получается так:

Действие Маршрут На сессию Доля запросов
Вход POST /api/login 1,00 9,3%
Список каталога GET /api/products 5,00 46,5%
Карточка товара GET /api/products/{id} 3,00 27,9%
Добавить в корзину POST /api/cart/items 1,20 11,2%
Оформить заказ POST /api/orders 0,25 2,3%
История заказов GET /api/orders 0,30 2,8%
Всего   10,75 100%

На каждый визит приходится 10,75 запроса. Из каждых 100 запросов 74 это чтение каталога, и всего 2-3 оформление заказа. Конверсия в заказ 0,25 на визит: покупает каждый четвёртый, для магазина это много, для стенда нормально.

Посмотри картинкой: виджет считает RPS по каждому действию и сравнивает с пределом логина. Подвигай ползунки «сессий в пиковый час» и «рост».

Столбцы это RPS каждого действия, красная метка предел. Логин занимает самую маленькую долю объёма (2 RPS), но он ближе всех к своему пределу, потому что дороже всех. Отсюда главное правило профиля: смотри не на то, что больше всего, а на то, что ближе всего к пределу. Подними рост до 3, и столбец логина зайдёт за 70% предела.

Осторожно: не давай всем действиям равные доли, такого профиля не бывает (увидишь в «Сломай и почини»).

Главное: состав это доли действий на сессию, и самое опасное действие то, что ближе всего к своему пределу, а не самое частое.

Состав известен. Осталось пересчитать его в число для генератора.

Расчёт RPS шаг за шагом

Расчёт простой, но у него пять шагов, и пропуск любого портит результат. Возьмём 2400 визитов в пиковый час и логин.

Запросов в час: 2400 визитов × 1 логин = 2400 (для списка 2400 × 5 = 12 000). Средний RPS: 2400 ÷ 3600 = 0,67. Дальше запас на пик внутри часа: нагрузка неровная, и в самую горячую минуту приходит в 1,3-2 раза больше среднего. Берём 1,5: 0,67 × 1,5 = 1,0. Это выбор, а не закон: смотри в данных, во сколько раз пиковая минута выше среднего часа, и записывай число в методику. Потом запас на рост: мы проверяем не сегодняшний день, а ближайшие полгода-год, и берём коэффициент, например, 2 (спроси у бизнеса, во сколько раз он ждёт рост): 1,0 × 2 = 2,0 логина в секунду. Общее правило: целевой RPS равен среднему × 1,5 × 2, то есть среднему × 3. По всем действиям:

Действие В час Средний RPS Целевой RPS (×3)
Вход 2400 0,67 2,0
Список каталога 12 000 3,33 10,0
Карточка 7200 2,00 6,0
Корзина 2880 0,80 2,4
Заказ 600 0,17 0,5
История 720 0,20 0,6
Всего 25 800 7,17 21,5

Теперь самый важный шаг: сверка с пределом. Предел логина 3,8 RPS (в 8.1 мы грубо считали 1 ÷ 0,25 = 4, в 8.2 точнее: 1 ÷ 0,26 = 3,8), цель 2,0: это 53% предела, то есть в рабочей зоне. А если бизнес ждёт рост не ×2, а ×4?

Прикинь сам: какой тогда будет цель по логину и что это значит?

Цель вырастет до 4,0, а это выше предела 3,8 (2,0 × 2 = 4,0). Находка появилась до начала тестов: вход надо ускорять (тема 11), а не гонять тест, который заведомо провалится.

Генератор из темы 9 просит число пользователей, а не RPS. Переводим по закону Литтла (8.2). Сессия длится 10,75 действий × 2,05 с (пауза в среднем 2 с плюс ответ около 50 мс): 10,75 × 2,05 ≈ 22 секунды. Сессий приходит 2400 ÷ 3600 = 0,67 в секунду. Значит, внутри одновременно 0,67 × 22 = 15 пользователей, а с запасами ×3 около 44. Проверка: 15 пользователей, каждый шлёт запрос раз в 2,05 с, это 7,3 RPS, то есть те же 7,17 среднего. Одна оговорка про логин. В реальности каждая сессия входит заново, но тестовый пользователь Locust входит один раз и живёт весь тест, поэтому такие 44 пользователя дали бы около нуля входов в секунду, а bcrypt остался бы ненагруженным. Нужные 2,0 входа в секунду набираются отдельным сценарием: в Locust это класс Auth из урока 9.3 (4 пользователя по одному входу в 2 секунды), в k6 отдельным сценарием, урок 10.2. Остальные пользователи ходят по каталогу и заказу с уже готовым токеном. Паузы 1-3 секунды это упрощение стенда: у настоящих людей они 5-30 секунд, и пользователей на ту же нагрузку нужно больше. Поэтому в методике всегда записывай паузу (think time) и откуда её взял.

Осторожно: 2400 сессий в час не 2400 пользователей одновременно. Онлайн в среднем 15, остальные приходят и уходят в течение часа.

Главное: целевой RPS это средний RPS, умноженный на запас на пик и рост, и его всегда сверяют с известным пределом до начала тестов.

С нагрузкой разобрались. Теперь вторая половина: как записать, что считаем успехом.

SLI, SLO и SLA простыми словами

«Сервис работает хорошо»: для одного это «быстро», для другого «без ошибок», для третьего «как вчера». Пока это не записано числами, любой результат можно трактовать как угодно. Доставка пиццы «за 30 минут или бесплатно»: «за 30 минут» измеримая цель, «бесплатно» последствие, а «в 99% заказов» серьёзность обещания.

Три сокращения, которые часто смешивают. SLI (service level indicator, «показатель уровня сервиса») это что мы меряем: доля успешных запросов, p95 каталога. SLO (service level objective, цель) это какое значение SLI считаем хорошим: «95% запросов к каталогу быстрее 300 мс за 30 дней». Формула: «SLI, порог, период». SLA (service level agreement, соглашение) это юридическое обещание клиенту с последствиями, обычно мягче SLO: внутри цель ставят строже, чтобы остался запас до нарушения договора. В курсе работаем с SLO.

Хорошие SLI это то, что видит пользователь: ответили ли и как быстро, а не загрузка процессора. Для веб-сервиса типичны два: доступность (доля успешных запросов) и задержка (доля запросов быстрее порога или перцентиль). Для «Магазина»:

Что SLI SLO
Каталог (список, карточка) p95 задержки не больше 300 мс
Вход p95 задержки не больше 800 мс (bcrypt 250 мс чистой работы плюс очередь)
Заказ p95 задержки не больше 1500 мс (оплата 50 мс плюс транзакция)
Всё вместе доля запросов без 5xx и таймаутов не ниже 99%

SLI считаются из метрик простыми запросами PromQL:

# p95 задержки каталога за 5 минут
histogram_quantile(0.95,
  sum by (le) (rate(http_request_duration_seconds_bucket{route="/api/products"}[5m])))

# доля успешных запросов (без 5xx) за 5 минут
sum(rate(http_requests_total{status!~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))

Первый запрос ты видел в 8.1: rate(...[5m]) скорость роста счётчика за 5 минут, histogram_quantile оценивает p95 по «корзинам» гистограммы: le - верхняя граница корзины (запросов быстрее 0,05 с, быстрее 0,1 с и так далее). Во втором status!~"5.." значит «статус не подходит под шаблон 5xx» (точка в регулярном выражении любой символ), и успешные делятся на все.

Из SLO вытекает бюджет ошибок (error budget). Если цель «99% успешных», ошибиться разрешено в 1% запросов. Это не недочёт, а запас, который команда тратит на релизы и эксперименты. Кончился бюджет: стоп новым фичам, чиним надёжность. 100% недостижимо и не нужно, цель задаёт допустимый уровень брака.

Прикинь сам: SLO «доля успешных не ниже 99% за сутки», сутки 150 000 запросов. Сколько ошибок можно себе позволить? Ночью упала оплата, 400 заказов завершились 5xx. Сколько бюджета ушло?

Бюджет 1% от 150 000 это 1500 ошибок. Ушло 400 из 1500, то есть 27%. Если такое повторится четыре раза за сутки, уйдёт 1600, больше бюджета, и SLO нарушено.

Осторожно: SLO по среднему («среднее не больше 200 мс») плохая цель, среднее прячет хвост (урок 8.1). Честнее p95 или p99.

Главное: SLI это что мерим, SLO это какое значение считаем хорошим, а бюджет ошибок это допустимый брак, которым команда распоряжается.

Есть способ следить за бюджетом, не дожидаясь, пока он кончится.

Burn rate: алерт на скорость сгорания бюджета

Напомню: бюджет ошибок для SLO 99% это 1% запросов за месяц. Алерт (сообщение дежурному на телефон, часто ночью) «доля ошибок больше 1%» плохо подходит для такого SLO. Твоя нагрузка на проде отзовётся именно такой тревогой. Короткий всплеск на две минуты разбудит дежурного, хотя бюджет почти не тронут. А ровная тонкая утечка в 0,9% не сработает никогда, хотя за месяц съест почти весь бюджет. Нужен алерт на вопрос «как быстро мы тратим бюджет и доживём ли до конца месяца». Как стрелка бензобака: важно не сколько осталось, а как быстро она падает.

Burn rate (скорость сгорания) это фактическая доля ошибок, делённая на допустимую по SLO. Для SLO 99% допустима доля 1%. Если ошибок 1%, burn rate равен 1, и бюджет кончится ровно к концу 30 дней. Если 14,4%, burn rate 14,4, и бюджет кончится за 30 ÷ 14,4 ≈ 2,1 суток. Число 14,4 выбрано не случайно. За час при такой скорости сгорает 14,4 ÷ 720 = 2% месячного бюджета (в 30 днях 720 часов), и это повод разбудить человека. Скорость 6 сжигает 5% за 6 часов и опустошает бюджет за 5 суток: для обычного рабочего тикета.

Чем выше скорость, тем круче падает остаток. Линия 14,4 доходит до нуля через 2,1 суток: на отметке «2 дня» остаётся около 4%, на отметке «3» уже ноль. Линия нормы доживает до конца месяца.

Один порог по одному окну не годится. Короткое окно (5 минут) реагирует быстро, но шумит, длинное (1 час) надёжное, но долго «остывает» после починки. Поэтому берут два окна сразу, и алерт срабатывает, только когда верны оба условия: длинное подтверждает, что беда настоящая, короткое, что она идёт прямо сейчас. Пары окон давно проверены практикой команд надёжности, их берут готовыми:

Алерт Скорость Окна Порог по ошибкам (SLO 99%) Бюджет, сгоревший за длинное окно Важность
ShopErrorBudgetBurnFast 14,4 1 час и 5 минут 14,4 × 1% = 14,4% 2% critical
ShopErrorBudgetBurnSlow 6 6 часов и 30 минут 6 × 1% = 6% 5% warning
flowchart TD
    A["Доля ошибок за 1 час<br>больше 14,4%?"] --> C{"Оба «да»<br>2 минуты подряд?"}
    B["Доля ошибок за 5 минут<br>больше 14,4%?"] --> C
    C -->|да| D["Алерт critical:<br>бюджет горит быстро"]
    C -->|нет| E["Молчим"]

Алерт срабатывает по пересечению двух условий, а for: 2m в правиле отсекает мигание. Запись правила для Prometheus вынесена в блок ниже, для идеи она не нужна.

Для любопытных: алерт в Prometheus

Считать окна 5m, 30m, 1h и 6h в каждом алерте громоздко. Их вынесли в recording rules (урок 7.2), то есть готовые ряды: sli:http_error_ratio:rate5m, ...rate30m, ...rate1h, ...rate6h. Алерт получается коротким:

- alert: ShopErrorBudgetBurnFast
  expr: sli:http_error_ratio:rate1h > (14.4 * 0.01) and sli:http_error_ratio:rate5m > (14.4 * 0.01)
  for: 2m
  labels: {severity: critical}

14.4 * 0.01 это скорость на бюджет, and оставляет момент, когда оба ряда выше порога. Те же правила для задержки каталога лежат в monitoring/prometheus/rules/slo.yml. Там SLI это доля запросов медленнее 300 мс (sli:catalog_slow_ratio:rate5m и так далее), бюджет 5%, поэтому пороги 14,4 × 5% = 72% и 6 × 5% = 30%. Порог тот же 300 мс, что в таблице цели: долю быстрых можно посчитать только по готовой границе корзины гистограммы (le="0.3"), поэтому в гистограмме магазина есть корзина 0,3. Если нужной границы нет, пересчитать нельзя: сначала добавляют корзину, потом пишут правило.

Прикинь сам: SLO 99%, оплата сломалась на 5 минут, половина запросов получила 5xx. Сработает ли быстрый алерт?

За 5 минут доля ошибок 50%, короткое окно сильно выше порога. Но за час ошибки составили (5 ÷ 60) × 50% ≈ 4,2%, это ниже 14,4: длинное окно не согласно, и алерта нет. Это правильно: за 5 минут сгорело всего около 0,6% бюджета. А если ошибки держатся на 30% полчаса, за час наберётся 15% (выше 14,4%), и ещё через 2 минуты придёт critical. После починки короткое окно упадёт до нуля за пять минут, и алерт погаснет, пока часовое окно ещё помнит беду.

Осторожно: burn rate 14,4 это не «14,4% ошибок» вообще, а 14,4 бюджета. Для SLO 99,9% тот же порог означал бы 1,44% ошибок: порог считают от бюджета. И алерт увидит беду только при трафике: без запросов ряда ошибок нет, и алерт молчит (для этого есть ShopNoTraffic, урок 7.6).

Главное: burn rate это во сколько раз быстрее нормы горит бюджет, и алерт на два окна ловит настоящую беду без ложных тревог.

Цели на месяц есть. Теперь превратим их в критерий для одного прогона.

От SLO к критериям успеха теста

SLO записан на 30 дней, а тест длится полчаса. Нужно перевести «обещание на месяц» в «порог для этого прогона», иначе тест проходит «по ощущениям». Берём те же три числа из урока 8.1, RPS, p95 и ошибки, и добавляем ресурсы. Нагрузка достигнута: RPS на полке не ниже 95% цели (иначе тест не проверил обещанное). Задержка: p95 каждого действия не выше порога SLO. Ошибки: доля не выше порога (обычно 1%) и нет таймаутов. Ресурсы: CPU не выше 70-80%, пул БД не упирается (shop_db_pool_waiting близок к нулю), память не растёт.

Для load-теста «Магазина» на 21,5 RPS это значит: полка 15 минут, RPS не ниже 20, p95 в пределах SLO, ошибок меньше 1%, CPU shop не выше 70%. Проверяют по Grafana и выводу генератора, в отчёт идёт «прошёл» или «не прошёл, вот что именно».

Осторожно: «чтобы без ошибок» не критерий, на длинных тестах нулевых ошибок не бывает. Порог всегда ненулевой.

Главное: критерий теста это пороги RPS, p95, ошибок и ресурсов на полке, выведенные из SLO и записанные до запуска.

Критерии есть. Остаётся собрать всё в один документ.

Методика испытаний: один документ на весь тест

Через месяц ты захочешь повторить тест и сравнить результат. Если помнишь «примерно 300 RPS и кажется 10 минут», сравнение невозможно: непонятно, изменился сервис или условия. Методика фиксирует условия. Это документ из семи разделов (так устроен шаблон для ~/perf-lab/08-theory/methodology.md):

flowchart TD
    A["1. Цели"] --> B["2. Объект"]
    B --> C["3. Среда"]
    C --> D["4. Профиль"]
    D --> E["5. Сценарии"]
    E --> F["6. Критерии"]
    F --> G["7. Риски"]

Порядок «от вопроса к рискам». Цели первые, потому что без них остальное нечем оправдать. В них одно-два предложения: что хотим узнать и какое решение примем. Объект: сервис, версия, конфигурация (.env, лимиты, размер пула), набор данных. Среда: машина, ресурсы, генератор на том же хосте или отдельно, что может влиять. Профиль: таблицы из этого урока. Сценарии: какие тесты, в каком порядке, какой длительности (урок 8.3). Критерии: пороги SLO и условия остановки. Риски идут последними, они вытекают из всего: что может исказить результат или навредить (генератор слабее сервиса, нет прогрева) и что с этим делаем. Методика короткая: страница-две.

В разделе 6 к критериям добавляют условие остановки: например, «ошибок больше 20% в течение 30 секунд». Тогда другой человек проверит тест и получит тот же вердикт.

Осторожно: методику пишут до теста, отчёт после. Методика, написанная после, незаметно подгоняется под результат.

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

Последнее: как профиль чаще всего портят на практике.

Типичные ошибки профиля

Ошибки профиля коварны: тест красиво проходит или проваливается, а вывод неверен. Самая частая: нагрузка по среднему часу вместо пика, и тест пропускает реальные перегрузки. Вторая: равные доли действий, и логин нагружается в разы больше реального. Третья: нет пауз между действиями. Человек думает между кликами (у нас 1-3 секунды, у настоящих людей дольше), и без пауз 15 пользователей создадут нагрузку, которую обычно создают 300.

Дальше идут данные и вход. Один пользователь и одна карточка берутся из кэша, результат оптимистичен: разнообразь пользователей и товары. Логин на каждый запрос ломает самое слабое место, хотя люди входят раз за визит. Нет прогрева и нет запаса на рост: первые минуты холодные, а цель взята «как сейчас». И профиль «из головы» без данных: хотя бы пометь «оценка» и пересмотри.

Главное: профиль должен быть похож на реальность: пик, а не среднее, правильные доли, паузы, разные данные и запас на рост.

Вернёмся к вопросу «выдержим ли распродажу?». Теперь у тебя есть цифры, которые можно проверить, и документ, по которому любой повторит проверку.

Практика

Стенд поднят (docker compose --profile monitoring up -d --wait в ~/learning/load-tester/project/shop), утечка выключена, оплата 50 мс, как в 8.2. Рабочий каталог ~/perf-lab/08-theory, окружение source ~/perf-lab/.venv/bin/activate.

1. Сгенерируй «обычный» трафик

В реальной работе данные уже есть. На учебном стенде их нет, поэтому создадим: простой скрипт, где виртуальные покупатели ведут себя примерно как настоящие. Один покупатель это сессия: вход, десять действий с паузами 1–3 секунды, потом уходит и приходит новый. Доли действий похожи на таблицу выше.

Создай ~/perf-lab/08-theory/traffic.py:

"""Имитация обычных покупателей, чтобы появились данные для профиля.
Запуск: python traffic.py 15 180     (покупателей одновременно, секунд)
"""
import random
import sys
import time
from concurrent.futures import ThreadPoolExecutor

import requests

from measure import BASE

ACTIONS = ["list", "card", "cart", "order", "history"]
WEIGHTS = [5.0, 3.0, 1.2, 0.25, 0.3]  # на сессию: список 5, карточка 3, корзина 1,2 и так далее


def session_once(deadline):
    """Одна сессия: вход, около 10 действий с паузами, выход."""
    s = requests.Session()
    n = random.randint(1, 1000)
    r = s.post(f"{BASE}/api/login", json={"email": f"user{n:04d}@shop.lab", "password": "password"}, timeout=30)
    if r.status_code != 200:
        return
    s.headers["Authorization"] = f"Bearer {r.json()['token']}"
    in_cart = False
    for _ in range(10):
        if time.time() > deadline:
            return
        action = random.choices(ACTIONS, WEIGHTS)[0]
        if action == "order" and not in_cart:
            action = "cart"  # заказ с пустой корзиной бессмыслен
        if action == "list":
            s.get(f"{BASE}/api/products", params={"page": random.randint(1, 10)}, timeout=30)
        elif action == "card":
            s.get(f"{BASE}/api/products/{random.randint(1, 10000)}", timeout=30)
        elif action == "cart":
            s.post(f"{BASE}/api/cart/items", json={"product_id": random.randint(1, 10000), "qty": 1}, timeout=30)
            in_cart = True
        elif action == "order":
            s.post(f"{BASE}/api/orders", timeout=60)
            in_cart = False
        else:
            s.get(f"{BASE}/api/orders", timeout=30)
        time.sleep(random.uniform(1, 3))  # пауза на раздумье


def customer(deadline):
    while time.time() < deadline:
        session_once(deadline)


def main():
    users, seconds = int(sys.argv[1]), float(sys.argv[2])
    deadline = time.time() + seconds
    with ThreadPoolExecutor(max_workers=users) as pool:
        list(pool.map(lambda _: customer(deadline), range(users)))
    print("Готово")


if __name__ == "__main__":
    main()

Разбор нового: random.choices(ACTIONS, WEIGHTS)[0] выбирает действие с вероятностью, пропорциональной весу (список в пять раз вероятнее входа, заказ в двадцать); time.sleep(random.uniform(1, 3)) случайная пауза от 1 до 3 секунд, как wait_time Locust; list(pool.map(...)) запускает покупателей параллельно и ждёт, когда все закончатся.

Запусти три минуты на 15 покупателях:

python traffic.py 15 180
Готово

Типичные ошибки:

  • Скрипт сразу завершился с Готово: вход не удался (ответ не 200). Проверь curl -s localhost:8000/readyz и что в базе есть пользователи user0001@shop.lab и далее (стенд их заводит сам при старте).
  • Посыпались исключения KeyError: 'token': логин вернул ошибку; печатай r.text.

2. Достань состав из Prometheus

Открой http://localhost:9090 и введи запрос. Окно [4m] покрывает минуты трафика:

sum by (route) (increase(http_requests_total{route!~"/metrics|/healthz|/readyz"}[4m]))
{route="/api/login"}           133
{route="/api/products"}        683
{route="/api/products/{id}"}   410
{route="/api/cart/items"}      164
{route="/api/orders"}            72

Как читать вывод: метрика собрана по маршрутам, и маршруты GET и POST /api/orders слиплись в одну строку (метка method скрыта суммой). Чтобы разделить, добавь method в группировку: sum by (route, method) (...). Число логинов (133) это число сессий: 15 покупателей, сессия около 21 секунды, за 180 секунд набегает около 130 сессий (по Литтлу из 8.2). Делим остальное на 133: список 683 ÷ 133 = 5,1, карточка 410 ÷ 133 = 3,1, корзина 164 ÷ 133 = 1,2, заказы 72 ÷ 133 = 0,54 (это 0,25 заказа плюс 0,3 просмотра истории, оба маршрута /api/orders). Это те самые «на сессию»: твои числа немного отличаются (случайность), но близки к весам скрипта. Увидеть их по реальным данным и есть смысл этого шага.

Типичные ошибки:

  • Empty query result: окно трафика уже вне [4m]. Увеличь окно ([15m]) или запусти traffic.py ещё раз.
  • Цифры «прыгают» и не делятся ровно: increase в Prometheus экстраполирует границы окна, это нормально, смотри на порядок и доли.

3. Посчитай профиль: profile_calc.py

Расчёт из теории оформим скриптом, чтобы не считать руками и пересчитывать при смене чисел. Создай ~/perf-lab/08-theory/profile_calc.py:

"""Из состава сессии и объёма считает целевой RPS по каждому действию.
Запуск: python profile_calc.py [сессий_в_час] [запас_на_минутный_пик] [запас_на_рост]
"""
import sys

# действие: (сколько раз на сессию, предел RPS из твоих замеров или None)
MIX = {
    "Вход":             (1.00, 3.8),
    "Список каталога":  (5.00, None),
    "Карточка товара":  (3.00, 500),
    "В корзину":        (1.20, None),
    "Оформить заказ":   (0.25, None),
    "История заказов":  (0.30, None),
}


def main():
    sessions = float(sys.argv[1]) if len(sys.argv) > 1 else 2400
    peak = float(sys.argv[2]) if len(sys.argv) > 2 else 1.5
    growth = float(sys.argv[3]) if len(sys.argv) > 3 else 2.0
    print(f"{'действие':<18}{'в час':>8}{'средний':>9}{'цель':>7}{'предел':>8}{'% предела':>11}")
    total_hour = total_target = total_usage = 0.0
    for name, (per_session, limit) in MIX.items():
        per_hour = sessions * per_session
        average = per_hour / 3600
        target = average * peak * growth
        total_hour += per_hour
        total_target += target
        usage = f"{target / limit * 100:.0f}%" if limit else "-"
        if limit:
            total_usage += target / limit
        print(f"{name:<18}{per_hour:>8.0f}{average:>9.2f}{target:>7.1f}{limit or '-':>8}{usage:>11}")
    print(f"{'ИТОГО':<18}{total_hour:>8.0f}{total_hour / 3600:>9.2f}{total_target:>7.1f}")
    # все маршруты делят один процессор: доли предела складываются
    print(f"Суммарная загрузка по маршрутам с измеренным пределом: {total_usage * 100:.0f}%")
    # закон Литтла: сколько пользователей одновременно (пауза 2 с, ответ 0,05 с)
    actions = sum(p for p, _ in MIX.values())
    session_seconds = actions * 2.05
    online = sessions / 3600 * session_seconds
    print(f"Одновременно онлайн: {online:.0f} (в среднем), {online * peak * growth:.0f} (цель)")


if __name__ == "__main__":
    main()

Разбор нового: словарь MIX это состав профиля, меняй числа на свои из шага 2; предел в третьей колонке берёшь из baseline.md (если не измерял, None); {limit or '-':>8} печатает предел или прочерк; f"{...:<18}" выравнивание по левому краю шириной 18 символов, >8 по правому.

python profile_calc.py
действие             в час  средний   цель  предел  % предела
Вход                  2400     0.67    2.0     3.8        53%
Список каталога      12000     3.33   10.0       -          -
Карточка товара       7200     2.00    6.0     500         1%
В корзину             2880     0.80    2.4       -          -
Оформить заказ         600     0.17    0.5       -          -
История заказов        720     0.20    0.6       -          -
ИТОГО                25800     7.17   21.5
Суммарная загрузка по маршрутам с измеренным пределом: 54%
Одновременно онлайн: 15 (в среднем), 44 (цель)

Как читать вывод: колонка «цель» это RPS, которые ты введёшь в генератор: 21,5 всего. Главное смотри в последней колонке: логин занимает 53% своего предела, карточка 1% (у остальных пределы пока не измерены). Все маршруты работают на одном процессоре, поэтому их доли складываются: строка «Суммарная загрузка» показывает 54% (53% + 1%, округлено), и это уже в рабочей зоне 60–70% без остальных четырёх маршрутов. Предел каждого маршрута измерен при пустых остальных: под полной нагрузкой реальные пределы ниже, особенно у быстрых запросов, которые стоят в очереди за тяжёлым логином (250 мс на один процессор). Поэтому колонку «% предела» читай как нижнюю оценку. Строка «онлайн» отвечает на вопрос «сколько пользователей ставить в Locust»: около 44. Попробуй python profile_calc.py 2400 1.5 4 (рост ×4): логин получит 4,0 RPS при пределе 3,8, то есть 105%: это тревога ещё до запуска теста.

Формулируешь SLO и не уверен в числах? Сначала посчитай по своей базовой линии и профилю, потом спроси нейросеть, где формулировка расплывчата. Не принимай её цифры порогов: порог берётся из твоих измерений и требований, а не «в среднем по отрасли».

4. Запиши SLO и методику

Создай ~/perf-lab/08-theory/methodology.md. Подставь свои числа:

# Методика испытаний: «Магазин», пиковый час

## 1. Цели
Проверить, что «Магазин» выдерживает пиковый час с запасом роста ×2 и укладывается в SLO.
Решение: если не укладывается, чиним узкое место (темы 11) до выхода на публику.

## 2. Объект испытаний
Стенд `project/shop` (коммит: ___), `.env` по умолчанию: WEB_CONCURRENCY=1, DB_POOL_MAX=5,
BCRYPT_ROUNDS=12, оплата 50 мс. Данные: 1000 пользователей, 10 000 товаров.

## 3. Среда
Ноутбук ___, Docker, контейнер shop: 1 CPU, 512 МБ. Генератор на том же хосте (искажает на
высоких RPS, проверять htop). Фоновых нагрузок нет.

## 4. Профиль нагрузки
Пиковый час: 2400 сессий. Состав на сессию: вход 1, список 5, карточка 3, корзина 1,2,
заказ 0,25, история 0,3 (10,75 запроса). Паузы 1-3 с. Запас: ×1,5 на минутный пик, ×2 на рост.
Цель: 21,5 RPS (вход 2,0), около 44 одновременных пользователей.

## 5. Сценарии и тесты
smoke (2 RPS, 30 с) -> load (полка 21,5 RPS, 15 мин) -> breakpoint (лестница до p95 > порога)
-> spike (x3 на 20 с) -> soak (позже, тема 11).

## 6. Критерии успеха
Load: RPS не ниже 20; p95 каталога <= 300 мс; p95 входа <= 800 мс; p95 заказа <= 1500 мс;
ошибок < 1%; CPU shop <= 70%; shop_db_pool_waiting в среднем < 1.
Остановка: ошибок > 20% в течение 30 с.

## 7. Риски и ограничения
- Генератор на том же хосте: при росте RPS может стать пределом.
- Первые 2 минуты не учитываем (прогрев).
- Предел списка каталога, корзины и заказа ещё не измерен: замерить sweep.py до load.
- Нагрузка только на локальный стенд.

Закоммить: cd ~/perf-lab && git add 08-theory && git commit -m "8.4: профиль, SLO и методика" && git push.

5. Посмотри на сгорание бюджета в стенде

Правила SLO уже подключены к стенду (monitoring/prometheus/rules/slo.yml), запусти профиль monitoring, если он выключен, и пусти лёгкий трафик из урока 7.2 (traffic.sh). Команда делает мгновенный запрос к Prometheus (так же, как promq.sh из урока 7.2), напомню её разбор в одну строку: curl -s -G шлёт запрос с параметром query, jq достаёт значение.

curl -s -G localhost:9090/api/v1/query --data-urlencode 'query=sli:http_error_ratio:rate5m / 0.01' | jq -r '.data.result[0].value[1]'

Разбор: sli:http_error_ratio:rate5m готовый ряд доли ошибок за 5 минут, / 0.01 делит его на бюджет SLO 99%, то есть выдаёт burn rate.

0

Как читать вывод: 0 значит ошибок нет, burn rate нулевой, бюджет не тратится. Единица означала бы «тратим ровно по плану», 14,4 «горим так быстро, что разбудим дежурного». Если ответ NaN или пустой (null), трафика за 5 минут нет: запусти traffic.sh. Подними 5xx на оплате (fail_rate у payment, как в уроке 7.6), и значение вырастет.

Типичные ошибки: NaN или null в выводе: нет трафика (деление 0 на 0 даёт NaN) или правила ещё не посчитались (подожди пару минут после старта); jq: error ... Cannot index: Prometheus не запущен, проверь docker compose ps.

Типичные ошибки:

  • Расчёт «на пальцах» не совпадает со скриптом: проверь, что в скрипте и в расчёте одинаковые коэффициенты запаса (1,5 и 2); частая ошибка забыть одно из них.
  • Предел из baseline.md не подставлен: колонка «% предела» пустая, и главный вывод (логин близко к пределу) пропадает. Подставь пределы каждого маршрута, который измерил.

Сломай и почини

Поломка. Коллега считал профиль «на глаз»: «Магазин»: четыре основных действия, пусть будут поровну: вход, каталог, корзина, заказ. Общая цель 21,5 RPS, значит, по 5,4 RPS каждого. Он прогнал load на этих числах, и тест на входе провалился:

Вход: цель 5.4 RPS, сделано 3.8, p95 6800 мс, таймаутов много

Он делает вывод: «Магазин не выдерживает пик, надо переписывать логин и покупать серверы». Задача. Что не так с выводом? Разберись, не заглядывая в ответ.

Разбор

Ошибка в профиле, а не в «Магазине». Логин происходит один раз на визит, а действий на визит больше десяти: по данным Prometheus (шаг 2) логин составляет около 9% запросов, а не 25%. Реальная цель логина 2,0 RPS (53% предела 3,8), а коллега подал 5,4: на 40% больше предела, поэтому очередь и таймауты (та же картина, что в «Сломай и почини» урока 8.3).

Что делать: считать состав из данных (sum by (route) (increase(http_requests_total[1h]))), а не из головы. Починка: профиль из profile_calc.py. Повторить load на 21,5 RPS с реальной смесью: p95 входа около 300 мс, тест проходит.

Обратная ошибка опаснее: если бы он занизил долю логина, тест прошёл бы, а в реальный пик «Магазин» упал. Поэтому профиль всегда проверяют данными и пишут в методике, откуда взяты числа. Если для логина реальный пик окажется выше (скажем, рассылка загнала всех логиниться в одну минуту), то это отдельный spike-тест и задача для темы 11: ускорять вход.

ИИ в помощь

Нейросеть помогает отшлифовать формулировку цели и методики и найти слабые места документа, но числа порогов должны идти из твоих измерений. Общие правила: ИИ-помощник.

Задача: проверить формулировку SLO.

Вот мой черновик SLO для сервиса магазина: <вставь текст SLO: метрика, порог, окно, доля>.
Проверь: измеримо ли это, есть ли окно и доля запросов, не смешаны ли цель (SLO)
и измерение (SLI). Предложи переформулировку, но числа не меняй: они из моих измерений.
Что неясно, переспроси.

Проверь ответ: проверь, что каждую формулировку можно выразить запросом в Prometheus. Типичные ошибки: «99,9% запросов быстрее 200 мс» без окна времени, «доступность» без определения успешного ответа и подмена чисел «более типичными».

Задача: составить профиль нагрузки из состава трафика.

Вот состав запросов за час по Prometheus: <вставь маршруты и доли в процентах>
и пиковый RPS <число>. Нужен профиль для теста: доля каждого сценария и число пользователей
при паузе <число> с между действиями. Покажи формулу и расчёт по шагам.

Проверь ответ: пересчитай на калькуляторе и сравни с результатом profile_calc.py из практики. Типичная ошибка: доли, которые в сумме не дают 100%, и забытая пауза в расчёте числа пользователей.

Словарик урока

Термин Простыми словами
Профиль нагрузки (workload model) Что делают пользователи, в каких долях, сколько их и как нагрузка меняется во времени
Пользовательский сценарий (user journey) Последовательность действий одного посетителя: вход, каталог, корзина, заказ
Состав (mix) Доля каждого действия в общем трафике
Сессия (session) Один визит пользователя от входа до ухода
Пиковый час Самый нагруженный час периода: по нему считают цель теста
Запас на пик и рост Коэффициенты, на которые умножают средний RPS (минутный всплеск и рост бизнеса)
Пауза на раздумье (think time) Время между действиями пользователя, пока он читает и решает
SLI Что мы меряем: например, p95 задержки или доля успешных запросов
SLO Цель на показатель: SLI, порог и период
SLA Юридическое соглашение с клиентом о уровне сервиса и последствиях нарушения
Бюджет ошибок (error budget) Сколько брака допускает SLO (100% минус цель): запас на релизы и сбои
Burn rate Во сколько раз быстрее нормы тратится бюджет ошибок: доля ошибок, делённая на бюджет SLO
Критерий успеха (pass/fail) Пороги, по которым тест считается пройденным
Методика испытаний Документ: цели, объект, среда, профиль, сценарии, критерии, риски
Условие остановки Правило, при котором тест прерывается, чтобы не добить сервис

Вопросы с собеседований

Раздел для повторения: ответь вслух, потом открой ответ. Вопросы [на скорость] тренируй на время: ответ за 30 секунд.

1. [junior] [часто] Что такое SLI, SLO и SLA?

Ответ

SLI это измеряемый показатель (например, p95 задержки или доля успешных запросов). SLO это внутренняя цель на этот показатель (95% запросов быстрее 300 мс за 30 дней). SLA это юридическое соглашение с клиентом с последствиями при нарушении; его делают мягче SLO, чтобы был запас.

Что хотят услышать: три определения, пример с цифрами, разница «внутри» и «наружу».

Красный флаг: путаница SLI и SLO или «SLA это то же самое, что аптайм».

2. [junior] [часто] Что такое профиль нагрузки и из чего он состоит?

Ответ

Описание того, что делают пользователи и сколько их: сценарии, доли действий, объём в пиковый час и форма во времени (разгон, полка). Составляется из реальных данных (метрики, логи). Из профиля получают RPS по каждому действию.

Что хотят услышать: состав, объём, форму; «из данных, а не из головы».

Красный флаг: «1000 пользователей и всё».

3. [junior] [часто] Почему тест нельзя строить на среднем за сутки?

Ответ

Нагрузка неравномерна: ночью почти нет, в пик в несколько раз выше среднего. Тест на среднем покажет «всё хорошо», а в пик сервис перегрузится. Тестируют пиковый час с запасом на минутные всплески и рост.

Что хотят услышать: пик, запасы, объяснение причины.

Красный флаг: «среднее самое честное число».

4. [junior] Как из числа сессий получить RPS?

Ответ

Сессии в пиковый час умножить на число действий в сессии, поделить на 3600. Затем умножить на запас на минутный пик (около 1,5) и на запас на рост (например, 2). Для каждого действия считается отдельно: у входа и каталога разные цели.

Что хотят услышать: формулу с коэффициентами, ключевое слово «по каждому действию».

Красный флаг: «просто число пользователей на 1 запрос в секунду».

5. [middle] Как перевести RPS в число виртуальных пользователей для генератора?

Ответ

Законом Литтла: одновременно онлайн = сессий в секунду × длительность сессии. Длительность сессии = действий в сессии × (пауза + ответ). Например, 0,67 сессии в секунду × 22 с = 15 пользователей; с запасами около 44. Проверка обратным расчётом: 15 пользователей, запрос раз в 2 с даёт те же 7 RPS.

Что хотят услышать: закон Литтла, роль паузы, проверка обратным расчётом.

Красный флаг: «пользователей столько же, сколько RPS».

6. [middle] Какие SLI ты выберешь для веб-сервиса и почему?

Ответ

То, что видит пользователь: доступность (доля запросов без 5xx и таймаутов) и задержку (p95 или p99 по важным маршрутам, а не среднее). Для критичных действий (оплата) отдельные SLI. Использование CPU и памяти это не SLI, а диагностика: пользователю важен результат, а не загрузка процессора.

Что хотят услышать: пользовательский взгляд, перцентили, по маршрутам.

Красный флаг: «CPU меньше 80%».

7. [middle] Как превратить месячное SLO в критерий для теста на 30 минут?

Ответ

Берут те же пороги SLI (p95, доля ошибок) и применяют их к полке теста при профильной нагрузке. Добавляют условия: достигнута цель RPS, ресурсы не на пределе, нет таймаутов. Тест не доказывает месячное SLO, но показывает, что при расчётном пике сервис в пороги укладывается.

Что хотят услышать: переносим пороги, не период; нагрузка достигнута; ресурсы.

Красный флаг: «30 минут без ошибок значит SLO выполнен».

8. [middle] Что должно быть в методике испытаний?

Ответ

Цели и решение по результату, объект (версия, конфигурация), среда, профиль нагрузки, сценарии и типы тестов, критерии успеха и остановки, риски и ограничения. Пишется до теста, чтобы результат можно было повторить и сравнить, и чтобы критерии не подгонялись под полученные цифры.

Что хотят услышать: хотя бы пять разделов, «до теста», повторяемость.

Красный флаг: «методика это скрипт на Locust».

9. [middle] Тест пройден, но через неделю в реальный пик сервис упал. Что могло быть не так с профилем?

Ответ

Типичные причины: цель посчитана по среднему, а не по пику; не было запаса на минутные всплески и рост; доли действий из головы (недогружено слабое место); нет пауз и разнообразия данных (всё из кэша); логин не на каждый визит; генератор слабее цели и недодал нагрузку. Проверяю каждое по данным пика и методике.

Что хотят услышать: несколько конкретных причин, проверка по данным.

Красный флаг: «просто не повезло».

10. [junior] [на скорость] Что такое пиковый час и зачем он?

Ответ

Самый нагруженный час периода. По нему задают цель теста: проверяем самое трудное, а не среднее.

11. [junior] [на скорость] Посчитай RPS: 1800 сессий в час, 10 запросов на сессию.

Ответ

1800 × 10 = 18 000 запросов в час, ÷ 3600 = 5 RPS в среднем. С запасами 1,5 и 2: 15 RPS.

12. [middle] Что такое burn rate и почему алерт по SLO делают на двух окнах?

Ответ

Burn rate это отношение фактической доли ошибок к допустимой по SLO: при 1 бюджет кончится ровно к концу периода, при 14,4 примерно за 2 суток (30 ÷ 14,4). Алерт «быстрое сгорание» берут как burn rate выше 14,4 одновременно на окне в час и в пять минут. Длинное окно подтверждает, что беда настоящая, а не всплеск, короткое быстро гасит алерт после починки. Для медленного сгорания (скорость 6) берут окна 6 часов и 30 минут и важность ниже.

Что хотят услышать: определение через бюджет, смысл пары окон, что порог считают от бюджета, а не от круглого числа, разные важности быстрого и медленного алертов.

Красный флаг: «алерт на ошибки больше 1% за минуту» для месячного SLO.

Проверено на версиях

Ubuntu 24.04, Python 3.12, requests 2.34.2, стенд «Магазин» из project/shop (Python 3.14, PostgreSQL 18.6), Prometheus 3.15, Grafana 13.2. Октябрь 2026.

Итог урока: ты умеешь

  • Описать профиль нагрузки: сценарии, состав, объём, форму.
  • Достать состав действий из метрик (sum by (route) (increase(...))) и посчитать действий на сессию.
  • Пересчитать сессии в целевой RPS по каждому действию с запасами на пик и рост.
  • Перевести RPS в число одновременных пользователей по закону Литтла.
  • Сверить цель с пределом ресурса и заметить проблему до теста.
  • Объяснить разницу между SLI, SLO и SLA и записать SLO для маршрута.
  • Посчитать burn rate и объяснить, почему быстрый алерт стоит на двух окнах (1 час и 5 минут).
  • Записать критерии успеха теста и оформить методику испытаний из семи разделов.

Дальше: тема 9. Locust: воспроизведёшь этот профиль в настоящем инструменте нагрузки.

Проверь себя

Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.

Тест работает с включённым JavaScript.

тема 8 урок 8.4 3 ч курс 0/0 ← → уроки