load-tester Все курсы

✻ Урок 12.1 · Тема 12: Нагрузочное тестирование как процесс

Отчёт по нагрузочному тесту

⏱ 3 ч

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

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

Теперь ты в той же точке. До распродажи недели, в прошлом году сайт на ней упал. Ты провёл прогоны, нашёл узкое место и проверил починку на стенде: задержка на проблемном маршруте упала в разы. Менеджер спросит: «Выдержим распродажу? Что делать и сколько это стоит?» Разработчик: «На каком запросе ломается и как это повторить?» Ты сам через полгода: «Что мы тогда мерили?» Терминал и скриншот Grafana на это не ответят: результат живёт, пока открыта вкладка.

Документ, который превращает числа в решение, называется отчётом о нагрузочном тесте (load test report). Хороший отчёт читают, на него ссылаются в задачах, по нему повторяют тест через год. Плохой (тридцать страниц графиков без вывода или строка «всё работает») закрывают, не дочитав.

Шаг проекта: ты прогонишь «Магазин» ступенчатой нагрузкой (скорость растёт ступенями, каждая держится минуту), заведёшь шаблон ~/perf-lab/reports/TEMPLATE.md и заполнишь по нему первый настоящий отчёт. Его данные станут базовой линией для CI (урок 12.2) и основой прогноза ёмкости (урок 12.3).

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

  • Методика испытаний, цель, SLO и профиль нагрузки: файл ~/perf-lab/08-theory/methodology.md из урока 8.4. Отчёт описывает, как прошёл тест по этой методике.
  • Перцентили, ёмкость и «клюшка»: урок 8.1 и урок 8.2. Без них фразы «p95 = 480 мс» и «колено на 40 запросах в секунду» ничего не значат.
  • Скрипт k6 с порогами и кодом выхода: ~/perf-lab/10-k6/shop.js из темы 10. Его мы будем гонять ступенями.
  • Расследования из темы 11: отчёт строится вокруг найденных узких мест и доказательств.
  • Работа с git в ~/perf-lab: тема 3.

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

Возьми заключение врача после обследования. Перед врачом сотни показателей: анализы, снимки, давление по часам. Но пациенту выдают лист, где сверху диагноз и что делать, ниже обоснование, и только в конце приложены сами анализы. Врач не начинает с «в 8:15 измерили пульс»: он знает, что пациент дочитает до конца только то, что стоит первым. Отчёт устроен так же: сверху вывод, снизу доказательства, чем глубже, тем подробнее и тем меньше людей это читает. Аналогия ломается в одном месте: пациент один, а у отчёта читатели разные, и каждый берёт свой слой.

flowchart TD
    A["Руководитель<br/>30 секунд"] --> B["Итог в 3 строки"]
    C["Разработчик<br/>10 минут"] --> D["Узкие места,<br/>рекомендации"]
    E["Ты через полгода<br/>или коллега"] --> F["Среда, профиль,<br/>приложения"]
    B --> D --> F

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

Из девяти разделов состоит отчёт, и порядок у них такой:

flowchart TD
    S1["1. Итог в 3 строки"] --> S2["2. Цель и критерии"]
    S2 --> S3["3. Среда и её отличия от прода"]
    S3 --> S4["4. Профиль нагрузки"]
    S4 --> S5["5. Результаты: графики"]
    S5 --> S6["6. Узкие места"]
    S6 --> S7["7. Рекомендации"]
    S7 --> S8["8. Риски и непроверенное"]
    S8 --> S9["9. Приложения"]

Сначала вывод, потом почему ему можно верить, потом детали. Разделы 2–4 отвечают на вопрос «при каких условиях измерено», раздел 5 на «что получилось», разделы 6–8 на «что делать».

Теория

Кто читает отчёт и что ему нужно

Один и тот же отчёт откроют три человека, и каждому нужна своя страница. С какой начать писать?

Руководитель откроет его между двумя совещаниями. У него полминуты, ему нужно решение: выпускать или нет и что делать. Разработчик сядет на десять-пятнадцать минут и будет искать, где тесно и как повторить. Третий читатель это ты сам или твой преемник через полгода: ему надо понять, при каких условиях мерили.

Отсюда правило: итог пишется последним, но стоит первым, и в нём нет слов, которых руководитель не знает. Вместо «p95 равен 0,5 с» (95 запросов из 100 быстрее этого времени, урок 8.1) пиши «95 покупателей из 100 ждут не дольше полсекунды».

Руководитель пишет в чат: «Что с тестом?» Плохой ответ: ссылка на папку с тридцатью файлами. Хороший: три строки итога и «подробности по ссылке». Он прочёл за полминуты, решил «чиним до распродажи», и тебя не дёргали вопросами.

Осторожно: новички пишут отчёт в хронологическом порядке («сначала запустили, потом перезапустили»). Читателю интересен не путь, а результат: хронология уходит в приложение.

Проверь понимание: руководитель читает только первую страницу. Какие два вопроса должны получить на ней ответ?

Ответ

Выдерживает ли система нужную нагрузку (вердикт с числом и условием) и что нужно сделать к какому сроку. Всё остальное в отчёте служит доказательством этих двух ответов.

Главное: пиши для трёх читателей сразу: вывод сверху для занятых, доказательства снизу для тех, кто проверяет.

Теперь самый важный кусок, который прочтут все.

Итог в три строки

Если главное не умещается в три строки, вывода у тебя ещё нет, а есть куча цифр.

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

Сравни два итога для одного теста.

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

Хорошо: 1. Магазин выдерживает 200 запросов/с при p95 < 500 мс и ошибках < 1%.
        2. Пик сегодня 90 запросов/с (45% предела), к распродаже ждём ~172 (86%).
        3. Список заказов тормозит из-за Seq Scan по orders: индекс по user_id
           поднял предел до 290 запросов/с. Внедрить до 20 ноября.

В плохом нет ни одного числа, которое можно проверить. Хороший отвечает на оба вопроса руководителя: «выдержим?» (да, но тесно) и «что делать?» (индекс, срок). Seq Scan (база читает таблицу целиком, потому что нужного индекса нет) мы разбирали в теме 11.

Прикинь сам: предел 200 запросов в секунду, пик распродажи 172. Какую долю предела займёт распродажа?

Делим пик на предел: 172 на 200 даёт 0,86, то есть 86%. Из этого числа руководитель сам делает вывод «тесно».

Осторожно: в итоге часто пишут «тест успешно пройден». Это не вывод. Тест можно пройти и узнать, что система нужного не выдерживает. Вывод всегда про систему, а не про тест.

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

Читатель спросит: «А почему я должен тебе верить?» Ответ в трёх следующих разделах.

Цель, среда и профиль: почему этим цифрам можно верить

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

Цель берётся из методики урока 8.4: «до 200 запросов/с, p95 меньше 500 мс, ошибок меньше 1%». Если цели не было, так и пиши: «цели не задано, измерялась ёмкость».

Среда это таблица «тест против реальной работы»: процессор и память, база, где стоял генератор нагрузки, сеть, версия кода и конфигурация (.env). Самое ценное в ней отличия от реальной работы: от них зависит, насколько цифры переносятся на настоящий сервис. Честная запись: «Генератор работал на той же машине, что и сервис, и отнимал у него процессор: реальная ёмкость выше». Спрятанное отличие потом всплывает как «вы нас обманули».

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

Раздел «Среда» для нашего стенда выглядит так:

| Параметр               | Тест                                  | Прод (реальный сервис)     |
|------------------------|---------------------------------------|----------------------------|
| Сервис «Магазин»       | 1 контейнер, 1 процессор, 512 МБ      | зависит от установки       |
| Воркеров               | 1 (WEB_CONCURRENCY=1)                 | несколько экземпляров      |
| База                   | PostgreSQL 18, 200 000 заказов        | больше данных и индексов   |
| Генератор              | k6 на той же машине, что и стенд      | отдельные машины           |
| Сеть                   | localhost, нет TLS, нет балансировщика| есть, добавит 1–5 мс       |
| Версия кода            | commit a1b2c3d стенда                 |                            |

Пустая клетка в колонке «Прод» допустима: она напоминает читателю, что перенос на прод это отдельное предположение.

Осторожно: самое опасное в отчёте утверждение это «среда как в проде». Если оно ложно, обесцениваются все выводы: докажи цифрами или перечисли отличия.

Проверь понимание: генератор нагрузки и сервис на одной машине. В какую сторону это искажает ёмкость и как записать это в отчёт?

Ответ

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

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

Условия описаны. Как теперь показать результат так, чтобы картинка не обманула?

Результаты: графики, которые отвечают на вопрос

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

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

Честный график держится на простых правилах. На осях есть единицы («p95, мс»). Ось начинается с нуля, иначе разница в 5% выглядит пропастью. Линия цели нарисована прямо на графике. Показаны перцентили, а не среднее. Ошибки стоят рядом с задержкой: быстрые отказы улучшают задержку, и без них график врёт.

Вот ступенчатый прогон учебного «Магазина». Сценарий покупки: каталог, карточка, корзина, заказ, список заказов (пять запросов). Числа сквозные для этого урока и для урока 12.3. Стенд в таком состоянии: кэш карточек включён (CACHE_ENABLED=1, урок 11.5), индекса по orders.user_id и починки N+1 ещё нет: это узкое место отчёта. Стенд «из коробки» (урок 11.1) слабее: предел около 31 сценария в секунду и p95 около 1,9 с на 30. Форма кривой та же, числа у тебя будут другими.

До 30 сценариев в секунду кривая пологая, потом колено и подъём «клюшкой». Пунктир цели 500 мс кривая пересекает между 40 и 45 сценариями.

Прикинь сам: на 40 сценариях p95 равен 460 мс, на 45 уже 980 мс, цель 500 мс. Где примерно пересечение?

Почти у 40: до цели там всего 40 мс, а до следующей ступени кривая подскакивает на 520. Выходит около 41. Это ёмкость по SLO, то есть наибольшая нагрузка, при которой цель ещё выполняется: около 40 сценариев в секунду, примерно 200 HTTP-запросов в секунду (пять запросов на сценарий).

Теперь тот же прогон по ошибкам и пропускной способности.

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

Под картинкой в отчёте стоит подпись с выводом: «Рис. 1. p95 по ступеням, цель 500 мс. На 40 сценариях/с p95 = 460 мс, на 45 уже 980 мс. Ёмкость по SLO: 40 сценариев/с (около 200 запросов/с)». Тот, кто пролистывает отчёт, получает ответ, не вглядываясь.

Осторожно: скриншот Grafana без подписи и периода это иллюстрация, а не доказательство.

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

График показал, что на 45 сценариях всё плохо. Разработчик спросит: «Почему?»

Узкие места: симптом, доказательство, эффект

Раздел «Результаты» говорит, что случилось, раздел «Узкие места» говорит, почему. Без него разработчик спросит «а где именно?», и ты потратишь день на ответ, который должен был лежать в отчёте.

Каждое узкое место описывается тремя шагами из темы 11. Первый шаг, симптом: что видно снаружи. GET /api/orders даёт p95 около 1000 мс на 40 сценариях в секунду, остальные маршруты быстрее 140 мс.

Второй шаг, доказательство: что показало причину. EXPLAIN (команда базы, которая показывает, как она ищет данные) выдаёт Seq Scan по orders (200 000 строк) из-за отсутствия индекса по user_id. Кроме того, на один список заказов уходит 21 запрос к базе (N+1: отдельный запрос на каждую строку).

Третий шаг, эффект: два измерения при той же нагрузке, до починки и после. После индекса p95 списка заказов упал с 1000 до 45 мс, а ёмкость по SLO выросла с 200 до 290 запросов в секунду.

Третий шаг превращает рекомендацию из мнения в факт: «индекс уже проверен на стенде, вот измерение» принимают охотнее, чем «советую добавить индекс».

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

Сравни два пункта отчёта. «Узкое место: база данных» (какой запрос, откуда известно?). И «список заказов, Seq Scan по orders, 21 SQL-запрос на вызов (метрика http_requests_total и лог со request_id, приложение 3)». Второе можно проверить.

Осторожно: первое, что показал график ресурсов («процессор 80%»), выдают за узкое место, хотя процессор мог быть следствием. Узкое место подтверждено, когда починка убрала симптом. Если починить не можешь, пиши «гипотеза» и что нужно для проверки.

Главное: узкое место в отчёте описано тремя шагами (симптом, доказательство, эффект), без доказательства это только гипотеза.

Мы собрали факты. Осталось научиться формулировать из них выводы так, чтобы их нельзя было оспорить словами «а мне кажется».

Как писать выводы с цифрами

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

Пример: «На 40 сценариях в секунду (наблюдение) p95 равен 460 мс (число), цель 500 мс (сравнение), запас 8%, рост p95 на 40 мс и более нарушит SLO (значение для решения)». Без числа вывод нельзя проверить, без сравнения непонятно, хорошо это или плохо, без значения незачем читать.

Прикинь сам: p95 460 мс, цель 500 мс. Откуда «запас 8%»?

До цели осталось 500 минус 460, то есть 40 мс. Делим 40 на 500, получаем 0,08: это 8% от цели.

К формуле добавляются привычки. Пиши величину: «p95 вырос в 7 раз (с 62 до 460 мс)», а не «заметно вырос». Отделяй факт («p95 460 мс») от интерпретации («значит, база не справляется»): вторая требует доказательства. Давай диапазон там, где есть шум: «460 мс в трёх прогонах (440–490)», ведь один прогон это одно измерение (урок 12.2 про шум). И не прячь плохие новости: «ёмкость 200, к распродаже ждём 172, это выше рабочей границы, то есть 70% предела или 140 (урок 12.3)» честнее, чем «ёмкости хватает».

Вот как слабые выводы превращаются в сильные.

Слабо:   Система работает стабильно.
Сильно:  В часовом прогоне при 30 сценариях/с p95 держался в пределах 130–150 мс,
         ошибок 0: деградации за час нет.

Слабо:   Задержка на заказе высокая.
Сильно:  POST /api/orders даёт p95 = 140 мс при 40 сценариях/с: 80% времени это вызов
         оплаты (метрика shop_payment_duration_seconds, p95 = 110 мс).

Слабо:   Рекомендуется увеличить мощность.
Сильно:  Ёмкость 200 запросов/с уже тесна для распродажи: ждём 172 (86%), а рабочая
         граница 70% это 140 (урок 12.3). Нужно +23% или индекс по orders.user_id
         (проверено: +45%).

В каждом «сильно» есть число, условие и следствие.

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

Среднее и хвост на одном прогоне выглядят так.

При шести медленных запросах из ста среднее почти не видит проблему, а p95 и p99 видят сразу. Подвигай число медленных запросов: когда среднее «заметит»?

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

Ответ

Например: «После индекса по orders.user_id p95 GET /api/orders при 40 сценариях/с снизился с 1000 до 45 мс (три прогона, разброс до 8%)». Названы маршрут, нагрузка, было и стало, число прогонов, разброс.

Главное: сильный вывод это наблюдение, число, сравнение и значение для решения, а факт всегда отделён от догадки.

Вывод готов. Но читатель ждёт следующего вопроса: «И что нам теперь делать?»

Рекомендации и риски

Отчёт без рекомендаций это сводка, с ними это инструмент решения. А раздел рисков отвечает на вопрос, который тебе скоро зададут сами: «а что вы не проверили?»

Рекомендация это действие с ответственным, сроком и ожидаемым эффектом, которое можно записать в задачу. «Оптимизировать базу данных» это направление, а не задача. А «добавить индекс по orders.user_id и заменить 21 запрос одним (команда магазина, до 20 ноября, ожидаемо +45% ёмкости)» превращается в карточку в трекере. Приоритет задаёт связка «эффект на единицу усилий»: дешёвое и сильное идёт первым.

| № | Действие                                   | Эффект (проверено на стенде)  | Трудозатраты | Срок     |
|---|--------------------------------------------|-------------------------------|--------------|----------|
| 1 | Индекс по orders.user_id, убрать N+1       | ёмкость 200 -> 290 запросов/с | 1 день       | 20 нояб. |
| 2 | Больше воркеров (WEB_CONCURRENCY=2)        | не измерено, нужен тест       | 0,5 дня      | после 1  |
| 3 | Пересчитать ёмкость после п. 1 и 2         | -                             | 0,5 дня      | 25 нояб. |

Прикинь сам: три рекомендации в таблице выше. Что делать первым и почему?

Первую. Она единственная с измеренным эффектом (+45% ёмкости, и распродажа займёт уже 172 из 290, то есть 59%, ниже границы 70%) и стоит один день. Вторая не проверена, третья зависит от первых двух.

Риски это честный перечень того, что отчёт не доказывает. Предположения среды («генератор на той же машине»). Что не вошло в профиль («возвраты заказов не проверяли»). Ограничения прогона («час, а не сутки: утечку памяти этот тест не покажет»). Внешние зависимости («оплата подменена заглушкой в 50 мс, настоящая может отвечать медленнее»). Раздел рисков вызывает доверие, а не подрывает его: читатель видит, что ты знаешь границы своих знаний.

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

Главное: рекомендация это задача с владельцем, сроком и эффектом, а риски честно показывают границы отчёта.

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

Приложения и типичные ошибки отчётов

Через полгода тебя не будет рядом, или ты забудешь детали. Приложения превращают отчёт из «поверьте на слово» в «проверьте сами».

В них кладут команды запуска ровно так, как выполнялись, версии скрипта, k6 и стенда, сырые данные (JSON-сводки k6 из ~/perf-lab/results/), конфигурацию (.env) и журнал испытаний. Проверка простая: человек, у которого есть только репозиторий и отчёт, должен суметь повторить тест и получить числа в пределах шума.

Осторожно: тридцать строк сути со ссылками лучше ста страниц логов, которые никто не откроет.

Самые частые провалы отчётов: нет вывода, нет цели, нет условий, среднее вместо перцентилей, выводы без доказательств, один прогон принят за истину. Разбор такого отчёта ждёт тебя в разделе «Сломай и почини».

И самая обидная ошибка: отчёт не обновили после починки. В нём «ёмкость 200», а после индекса она 290, и читатель решает по устаревшим числам. Ставь дату и версию кода и пересматривай отчёт после изменений.

Главное: приложения делают тест повторяемым, а привычка пересматривать отчёт после каждой починки не даёт ему устареть.

Вернёмся к распродаже. Менеджер спросил «выдержим?», и ответ у тебя на одной странице: вердикт с числом, запас 86%, одно узкое место с проверенным лекарством и срок. Теперь соберём такой отчёт руками.

Практика

Все файлы урока складывай в ~/perf-lab/12-process/ и ~/perf-lab/reports/. Нагрузку даём только на свой локальный стенд: нагружать чужие сайты нельзя.

1. Подними стенд и убедись, что он в исходном состоянии

cd ~/learning/load-tester/project/shop
cp .env.example .env
docker compose --profile monitoring down -v
docker compose --profile monitoring up -d --build --wait
curl -s localhost:8000/readyz
{"status":"ready"}

Копия .env.example возвращает только настройки сервисов. Данные она не трогает: заказы прошлых прогонов, созданные индексы и корзины остаются в томах, поэтому «исходным» стенд считается после down -v и нового up, как в блоке «Чистый старт» урока 11.1. Проверь исходное состояние тремя командами:

docker compose exec -T postgres psql -U shop -d shop -tA -c "SELECT count(*) FROM orders"
docker compose exec -T postgres psql -U shop -d shop -tA -c "SELECT indexname FROM pg_indexes WHERE tablename='orders'"
curl -s -X POST localhost:8001/admin/config -H 'Content-Type: application/json' -d '{"delay_ms": 50, "fail_rate": 0}'

Ждёшь: 200000 (исторические заказы без новых), в списке индексов только orders_pkey (индекса orders_user_id_idx нет), а оплата настроена на 50 мс без ошибок. Другое число заказов или лишний индекс значит, что стенд не чистый: повтори down -v. -tA печатает значения без рамки таблицы.

В таком состоянии числа у тебя будут как в уроке 11.1: предел около 31 сценария в секунду, p95 около 1,9 с на 30. Образец отчёта ниже снят с включённым кэшем: если хочешь числа ближе к нему, допиши в .env строку CACHE_ENABLED=1 и перезапусти стенд (docker compose up -d --wait). Индекс и N+1 пока не трогай: их починка будет разделом 6 твоего отчёта. curl -s печатает ответ без индикатора загрузки.

2. Собери ступенчатый прогон

Сначала поправь запас VU в ~/perf-lab/10-k6/shop.js. Сколько VU нужно, считаем по закону Литтла из урока 8.2: VU = темп × длительность итерации. Итерация это пауза sleep(1) плюс пять запросов. Пока сервис быстрый (запрос 20-30 мс), она длится около 1,2-1,5 с: на 20 сценариях в секунду нужно 20 × 1,5 = 30 VU. Около предела запросы замедляются (в таблице ниже на 40 сценариях среднее 190 мс, значит итерация около 2 с, нужно 80 VU), а на 50 среднее 910 мс: итерация около 5,5 с и 50 × 5,5 ≈ 275 VU. Потолок по умолчанию в shop.js из урока 10.2 это 50 VU (MAX_VUS || 50): он упрётся уже на 35 сценариях, k6 начнёт пропускать итерации (dropped_iterations), и ты будешь измерять не магазин, а собственный лимит. Править скрипт не нужно: он уже читает потолок из переменной MAX_VUS, и stress.sh ниже передаст запас в 300 VU (ступени до 50 сценариев в секунду с запасом 10%). Обычные прогоны остаются на 50.

Нужен скрипт, который гоняет k6 несколько раз с растущей нагрузкой и складывает сводки в results/. Создай ~/perf-lab/12-process/stress.sh:

#!/usr/bin/env bash
# Ступенчатый прогон: по минуте на каждую ступень. Запуск: ./stress.sh [ступени через пробел]
set -u
cd "$(dirname "$0")/.."
mkdir -p results
STEPS="${*:-10 20 30 35 40 45 50}"
rm -f results/stress-*.json results/stress-*.exit   # сводки прошлого прогона не должны попасть в таблицу
failed=0
for rate in $STEPS; do
  echo "== $rate сценариев/с, 60 с =="
  k6 run -q -e RATE="$rate" -e DURATION=60s -e MAX_VUS=300 \
    --summary-export "results/stress-$rate.json" 10-k6/shop.js
  code=$?
  echo "$code" > "results/stress-$rate.exit"
  if [ "$code" -ne 0 ] && [ "$code" -ne 99 ]; then
    echo "!! ступень $rate: k6 упал с кодом $code, проверь вывод выше" >&2
    failed=1
  fi
  sleep 20   # пауза: очередь рассасывается, следующая ступень начинается с чистого листа
done
exit "$failed"

Разбор: set -u останавливает скрипт при обращении к несуществующей переменной. cd "$(dirname "$0")/.." переходит в корень ~/perf-lab независимо от того, откуда запустили скрипт ($0 это имя скрипта, dirname берёт его каталог). ${*:-10 20 ...} берёт ступени из аргументов, а если их нет, использует значения по умолчанию. -e RATE=... передаёт переменные окружения в k6: скрипт shop.js читает RATE (сценариев в секунду), DURATION и потолок VU (MAX_VUS). --summary-export сохраняет итоговую сводку в JSON. code=$? запоминает код выхода k6. На перегруженных ступенях пороги shop.js не выполняются и k6 завершается кодом 99 (урок 10.2): это ожидаемая реакция на перегрузку, и скрипт идёт дальше молча. Любой другой ненулевой код значит, что упал сам k6 (ошибка в скрипте, нет файла), и if печатает предупреждение (>&2 отправляет его в поток ошибок, чтобы оно не потерялось) и запоминает сбой в failed. Код каждой ступени ложится рядом со сводкой в results/stress-<темп>.exit, по нему table.py ниже отличит честный замер от аварии. rm -f в начале стирает сводки прошлого прогона: иначе ступень, которая сейчас упала, подхватила бы в таблицу старый файл с теми же цифрами. Хочешь сохранить прошлый прогон, переложи его файлы в отдельную папку до запуска. exit "$failed" в конце возвращает 1, если хоть одна ступень упала: так сбой виден и в коде выхода всего скрипта. sleep 20 даёт сервису отдышаться.

chmod +x ~/perf-lab/12-process/stress.sh
~/perf-lab/12-process/stress.sh

Прогон займёт около 10 минут: пока он идёт, открой Grafana (http://localhost:3000) и смотри, как растёт нагрузка на дашборде «Магазин: обзор (эталон)». Можешь дочитать следующий раздел.

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

  • k6: command not found: k6 не установлен (урок 10.1).
  • ERRO[0000] ... connection refused: стенд не запущен, вернись к шагу 1.
  • Файла 10-k6/shop.js нет: скрипт из темы 10 лежит в ~/perf-lab/10-k6/; если назвал иначе, поправь путь.

3. Собери таблицу из сводок

Создай ~/perf-lab/12-process/table.py:

"""Таблица по ступенчатому прогону: python table.py [шаблон файлов]"""
import glob
import json
import os
import re
import sys

pattern = sys.argv[1] if len(sys.argv) > 1 else "../results/stress-*.json"


def values(summary, name):
    metric = summary["metrics"][name]
    return metric.get("values", metric)  # у разных версий k6 поля лежат по-разному


rows, broken = [], []
for path in glob.glob(pattern):
    rate = int(re.search(r"-(\d+)\.json$", path).group(1))
    exit_path = path[:-len(".json")] + ".exit"
    code = open(exit_path).read().strip() if os.path.exists(exit_path) else None
    if code not in ("0", "99"):  # 0 и 99 (пороги нарушены) значат, что тест дошёл до конца
        broken.append((rate, code))
        continue
    with open(path) as stream:
        summary = json.load(stream)
    duration = values(summary, "http_req_duration")
    requests = values(summary, "http_reqs")
    failed = values(summary, "http_req_failed")
    error = failed.get("rate", failed.get("value", 0))
    dropped = values(summary, "dropped_iterations")["count"] if "dropped_iterations" in summary["metrics"] else 0
    rows.append((rate, requests["rate"], duration["avg"], duration["p(95)"], error * 100, dropped))
# Упавшая ступень могла не оставить сводки вовсе: её видно только по файлу .exit.
for exit_path in glob.glob(pattern[:-len(".json")] + ".exit"):
    if not os.path.exists(exit_path[:-len(".exit")] + ".json"):
        broken.append((int(re.search(r"-(\d+)\.exit$", exit_path).group(1)), open(exit_path).read().strip()))

print(f"{'сцен/с':>7} {'запр/с':>8} {'среднее':>9} {'p95':>8} {'ошибок %':>9} {'пропуски':>9}")
for rate, rps, avg, p95, error, dropped in sorted(rows):
    mark = "  <- замер недействителен" if dropped > 0 else ""
    print(f"{rate:>7} {rps:>8.0f} {avg:>9.0f} {p95:>8.0f} {error:>9.1f} {dropped:>9.0f}{mark}")
for rate, code in sorted(broken):
    why = f"k6 завершился с кодом {code}" if code else "нет файла .exit, сводка не из этого прогона"
    print(f"{rate:>7}  замер недействителен: {why}")
sys.exit(1 if broken else 0)

glob.glob находит файлы по шаблону, re.search достаёт число из имени (stress-40.json → 40), рядом лежащий .exit говорит, чем закончилась ступень: если кодом не 0 и не 99 (или файла нет), ступень уходит в список broken и печатается в конце строкой «замер недействителен», а скрипт завершается кодом 1. metric.get("values", metric) берёт поля как из старого, так и из нового формата сводки. Если скрипт падает с KeyError, посмотри, какие ключи есть у тебя: jq '.metrics | keys' ~/perf-lab/results/stress-10.json.

cd ~/perf-lab/12-process && source ~/perf-lab/.venv/bin/activate && python table.py
 сцен/с   запр/с   среднее      p95  ошибок %  пропуски
     10       50        21       62       0.0         0
     20      100        27       85       0.0         0
     30      150        44      140       0.0         0
     35      175        85      260       0.0         0
     40      198       190      460       0.1         0
     45      215       420      980       0.8         0
     50      224       910     2100       3.5         0

Как читать вывод: колонка «запр/с» это полученная пропускная способность в HTTP-запросах (в сценарии их пять, поэтому она примерно в пять раз больше «сцен/с»). Смотри на p95: он плоский до 30 и ломается между 35 и 45. Когда p95 пересекает 500 мс (твоя цель из методики), записывай ёмкость по SLO: у примера 40 сценариев в секунду, около 200 запросов в секунду. Колонка «пропуски» это dropped_iterations: итерации, которые k6 не смог запустить, потому что кончились VU. Правило: пропуски больше нуля значат, что заданный поток не получен и замер ступени недействителен (подними MAX_VUS и повтори). В примере везде нули: запаса в 300 VU хватило. Если ступень упала (k6 завершился не кодом 0 и не 99), вместо чисел будет строка «замер недействителен»: разберись по выводу stress.sh и повтори эту ступень. На «ошибок %» смотри первой на перегруженных ступенях. Твои числа будут другими: важна форма, а не точное значение.

4. Заведи шаблон отчёта

Создай ~/perf-lab/reports/TEMPLATE.md:

# <Система>: нагрузочное тестирование «<сценарий>», <месяц год>

Автор: <имя>. Версия кода: <хеш коммита>. Дата прогонов: <дата>. Статус: <черновик / принят>.

## 1. Итог (три строки)

1. **Вердикт:** <система выдерживает / не выдерживает> <нагрузка> при <p95 < ... мс, ошибки < ...%>.
2. **Запас:** <пик сейчас, ожидаемый пик к <дата>, % от предела>.
3. **Действие:** <главное узкое место> -> <что сделать, кто, до какого срока, ожидаемый эффект>.

## 2. Цель и критерии успеха

Что проверяли и зачем: <...>. Критерии (из методики): <нагрузка, p95, ошибки>.

## 3. Среда

| Параметр | Тест | Прод / реальная работа |
|---|---|---|
| Сервис (процессор, память, воркеры) | | |
| База (версия, объём данных) | | |
| Генератор нагрузки (где, версия) | | |
| Сеть | | |
| Версия кода, конфигурация | | |
| Состояние стенда перед прогоном (чистый старт `down -v` или нет, прогрев в секундах, сколько данных) | | |

**Отличия от прода и как они влияют на выводы:** <...>

## 4. Профиль нагрузки

Сценарий: <шаги>. Доли операций: <...>. Форма: <ступени, плато>. Модель: <открытая/закрытая>. Длительность: <...>. Скрипт: <путь и коммит>.

## 5. Результаты

<график + таблица + подпись-вывод под каждым графиком>

## 6. Узкие места

### 6.1 <название>
Симптом: <...>. Доказательство: <...>. Эффект исправления (до -> после): <...>.

## 7. Рекомендации

| № | Действие | Эффект | Трудозатраты | Срок | Как проверить |
|---|---|---|---|---|---|

## 8. Риски и что не проверено

- <...>

## 9. Приложения

Команды запуска, версии, сырые данные (`results/`), журнал испытаний, ссылки на дашборды.

Шаблон сам напоминает, что писать в каждом разделе, и заставляет заполнять «Отличия от прода», который проще всего пропустить.

5. Заполни отчёт по своему прогону

Скопируй шаблон: cp ~/perf-lab/reports/TEMPLATE.md ~/perf-lab/reports/2026-10-shop-purchase.md. Заполни в таком порядке (вывод идёт последним, хотя стоит первым):

  1. Раздел 3 «Среда»: параметры своего ноутбука (nproc, free -h, docker compose version, твой machine.md из урока 1.1), хеш коммита стенда (git -C ~/learning rev-parse --short HEAD), версию k6 (k6 version). Запиши минимум три отличия от прода.
  2. Раздел 4: сценарий из shop.js. Загляни в скрипт и перечисли пять запросов итерации.
  3. Раздел 5: таблица из шага 3 и график. В Markdown на GitHub график строится прямо из текста блоком xychart-beta. Подставь свои числа:
```mermaid
xychart-beta
    title "p95 задержки по ступеням нагрузки"
    x-axis "Сценариев в секунду" [10, 20, 30, 35, 40, 45, 50]
    y-axis "p95, мс" 0 --> 2200
    line [62, 85, 140, 260, 460, 980, 2100]
```
  1. Раздел 6: возьми одно узкое место из темы 11, которое ты нашёл (например, индекс по orders.user_id), и опиши по схеме «симптом, доказательство, эффект». Если ещё не чинил, повтори исправление и измерь «до» и «после» одним и тем же stress.sh.
  2. Разделы 7 и 8: два-три действия с эффектом и сроком, не меньше трёх рисков.
  3. Раздел 2 и, наконец, раздел 1 «Итог» в три строки.

Для примера: так выглядит готовый отчёт по «Магазину» с реалистичными числами (твои числа другие, оформление то же).


Магазин: нагрузочное тестирование «покупка», октябрь 2026

Автор: нагрузочный инженер. Версия кода стенда: a1b2c3d. Прогоны: 12 и 13 октября. Статус: принят.

1. Итог
  1. Вердикт: Магазин выдерживает 40 сценариев покупки в секунду (около 200 запросов/с) при p95 < 500 мс и ошибках < 1%. Это предел по SLO: на 45 сценариях p95 = 980 мс.
  2. Запас: пик сегодня 90 запросов/с (45% предела); ожидаемый пик акции в ноябре около 172 (86%): ниже предела, но выше рабочей границы 70% (доля предела, после которой уже тесно: урок 12.3).
  3. Действие: запрос списка заказов проходит полным перебором таблицы orders и делает 21 запрос к базе на вызов. Индекс по orders.user_id и один запрос вместо 21 подняли предел до 290 запросов/с (проверено на стенде). Внедрить до 20 ноября.
2. Цель и критерии

Проверить, выдерживает ли Магазин ожидаемый пик акции. Критерии из методики: до 200 запросов/с (40 сценариев/с), p95 < 500 мс, ошибок < 1%.

3. Среда
Параметр Тест Прод
Сервис 1 контейнер, 1 процессор, 512 МБ, 1 воркер несколько экземпляров
База PostgreSQL 18.6, 200 000 заказов, 10 000 товаров больше данных
Генератор k6 2.3 на той же машине (8 ядер, 16 ГБ) отдельные машины
Сеть localhost, без TLS и балансировщика есть, +1-5 мс
Конфигурация .env.example и CACHE_ENABLED=1 своя

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

4. Профиль нагрузки

Сценарий из 10-k6/shop.js: каталог, карточка товара, корзина, заказ, список заказов (пять запросов, пауза 1 с). Открытая модель (постоянная скорость прихода). Ступени 10, 20, 30, 35, 40, 45, 50 сценариев/с по 60 секунд, между ступенями 20 секунд паузы.

5. Результаты
Сценариев/с Запросов/с p95, мс Ошибок %
30 150 140 0
40 198 460 0,1
45 215 980 0,8
50 224 2100 3,5

Вывод: после 40 сценариев/с пропускная способность растёт всё медленнее (198, 215, 224), а задержка в 4,5 раза за две ступени: сервис упёрся. Запас до цели на 40 сценариях всего 8%.

6. Узкие места

6.1 Список заказов. Симптом: GET /api/orders даёт p95 около 1000 мс при 40 сценариях/с, остальные маршруты быстрее 140 мс. Доказательство: EXPLAIN показывает Seq Scan по orders (200 000 строк), на один вызов приходится 21 SQL-запрос (N+1). Эффект: индекс и один запрос: p95 1000 -> 45 мс, предел 200 -> 290 запросов/с.

7. Рекомендации
№ Действие Эффект Трудозатраты Срок
1 Индекс по orders.user_id, убрать N+1 предел 200 -> 290 запр/с 1 день 20 нояб.
2 Проверить WEB_CONCURRENCY=2 не измерено 0,5 дня после п. 1
3 Повторить stress-тест и пересчитать ёмкость - 0,5 дня 25 нояб.
8. Риски
  • Генератор на той же машине: ёмкость занижена, но точная цифра для прода неизвестна.
  • Профиль не включает возврат заказов и поиск: нагрузка этих операций не проверена.
  • Прогон по минуте на ступень: утечки и деградацию во времени он не покажет (нужен soak-тест).
  • Оплата подменена заглушкой, реальный сервис может отвечать медленнее.
9. Приложения

~/perf-lab/12-process/stress.sh, results/stress-*.json, reports/TEMPLATE.md, k6 2.3, стенд a1b2c3d, Docker Compose, журнал испытаний.


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

Положи отчёт и шаблон под git:

cd ~/perf-lab && git add 12-process reports results && git commit -m "12.1: ступенчатый прогон и отчёт по Магазину" && git push

Типичные ошибки: git add не видит results/, если он в .gitignore: это нормально для больших файлов, тогда положи в отчёт только сводную таблицу, а на сырые данные дай ссылку. Если Mermaid-график не рисуется на GitHub, проверь, что блок открывается тремя обратными кавычками и словом mermaid.

Закончил черновик отчёта? Сначала сам прочти его по списку дефектов из «Сломай и почини», потом дай нейросети и попроси найти слабые места. Каждое замечание проверь по своим прогонам: если числа нет в сводке, замечание или правка неверны.

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

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

Отчёт: нагрузочное тестирование Магазина.

Мы запустили тест на Магазине. Сначала запустили с 10 пользователями, потом
с 50. Среднее время ответа 120 мс, это отлично. В Grafana видно, что процессор
загружен, видимо, проблема в базе. Ошибок почти не было. Тест прошёл успешно.
Рекомендуем оптимизировать базу данных, тогда всё будет работать быстро.
Среда как в проде.

Выпиши дефекты в файл ~/perf-lab/reports/broken-review.md, потом перепиши этот текст в формате шаблона, используя числа своего прогона (или условные, но реалистичные), и сравни с разбором.

Разбор

Дефекты:

  1. Нет итога и вывода. «Тест прошёл успешно» это про тест, а не про систему: непонятно, выдерживает ли Магазин нужную нагрузку.
  2. Нет цели. Не сказано, что хорошо, что плохо; «отлично» не с чем сравнить.
  3. Среднее вместо перцентилей. 120 мс может скрывать хвост в секунды (урок 8.1).
  4. Нет условий. Какой сценарий, сколько длился тест, «10 и 50 пользователей» это сколько запросов в секунду, какая версия.
  5. Гипотеза выдана за факт. «Видимо, база»: нет запроса, плана, метрики. Доказательства нет.
  6. «Ошибок почти не было» без числа и без связи с задержкой: сколько процентов, на какой ступени?
  7. «Среда как в проде» без доказательства; генератор, размер базы, версии не названы.
  8. Нет графиков, рисков и приложений; тест нельзя повторить.
  9. Рекомендация без конкретики, срока и ожидаемого эффекта.

Переписанный итог мог бы быть таким: «Магазин выдерживает 40 сценариев/с (около 200 запросов/с) при p95 < 500 мс и ошибках < 1%. Пик сегодня 90 запросов/с. Узкое место: список заказов (Seq Scan, N+1), индекс поднял предел до 290 запросов/с, внедрить до 20 ноября». Проверка себя: в каждой строке есть число и условие, из итога понятно, что делать, а отличия среды перечислены. Если хотя бы один пункт из списка дефектов остался, вернись к шаблону.

ИИ в помощь

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

Задача: проверить черновик отчёта как придирчивый читатель.

Ты руководитель, который решает, выпускать ли сервис. Прочитай мой отчёт о нагрузочном тесте
и найди слабые места: нет цели или критерия, среднее вместо перцентилей, гипотеза выдана за факт,
не указаны условия, нет итога и рекомендаций с эффектом и сроком. Верни список замечаний по пунктам,
с цитатой из отчёта. Числа и факты не добавляй и не исправляй, только отмечай, чего не хватает.
Отчёт: <вставь текст отчёта без внутренних адресов и имён>

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

Задача: переписать рекомендацию в конкретный вид.

Вот моя рекомендация из отчёта: <вставь текст, например «оптимизировать базу»>.
Факты, на которые она опирается (только они): <вставь: маршрут, метрика, до и после, предел в запросах в секунду>.
Перепиши рекомендацию в формате: что сделать, ожидаемый эффект с числом, кто и к какому сроку, как проверить.
Если фактов не хватает, задай мне вопросы, а не придумывай числа.

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

Реальные имена клиентов, внутренние адреса и результаты чужих систем в чат не отправляй: замени на <сервис> и <стенд>.

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

Термин Простыми словами
Отчёт о нагрузочном тесте (load test report) Документ, который превращает числа прогона в решение: что выдерживаем, где тесно, что делать
Итог (executive summary) Три строки в начале отчёта: вердикт, запас, действие. Единственное, что читают все
Критерии успеха Числа, с которыми сравнивают результат: нагрузка, p95, доля ошибок. Берутся из методики
Среда тестирования Всё, на чём шёл тест: машина, версии, конфигурация, где генератор
Отличия от прода Список того, чем тестовая среда не похожа на реальную; определяет, насколько можно переносить цифры
Профиль нагрузки Что и как делает «пользователь», с какой скоростью приходит и сколько длится тест
Узкое место (bottleneck) Участок системы, который первым упирается в предел и ограничивает всё остальное
Доказательство Измерение, показывающее, что причина здесь: план запроса, метрика, профиль
Эффект исправления Измерение «до и после» при одинаковой нагрузке и среде
Рекомендация Конкретное действие с ответственным, сроком и ожидаемым эффектом
Риск Что отчёт не доказывает: допущения, непроверенное, ограничения прогона
Приложения Команды, версии, сырые данные: всё, чтобы повторить тест
Журнал испытаний Хронология прогонов: когда, что менялось, какой результат

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

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

1. [junior] [часто] Что должно быть в отчёте о нагрузочном тесте?

Ответ

Итог для руководителя (вердикт, запас, действие), цель и критерии, среда с отличиями от прода, профиль нагрузки, результаты с графиками и подписями-выводами, узкие места с доказательствами, рекомендации со сроками, риски и непроверенное, приложения для повторения теста.

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

Красный флаг: «графики и таблица с цифрами».

2. [junior] [часто] Что такое итог отчёта и что в нём должно быть?

Ответ

Три строки в начале: вердикт (выдерживает ли цель, с числами и условием), запас (сколько до предела и до ожидаемого пика), действие (главное узкое место и что делать, кто, до какого срока). Без жаргона, с числами, самодостаточно для читателя, который остановится на этом месте.

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

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

3. [junior] [часто] Почему нельзя писать «система работает хорошо»?

Ответ

Это нельзя проверить и сравнить. Вывод должен содержать наблюдение, число, сравнение с целью и значение для решения: «при 40 сценариях в секунду p95 равен 460 мс при цели 500 мс, запас 8%». Тогда читатель может оспорить, повторить и принять решение.

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

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

4. [junior] Зачем в отчёте раздел «Среда» и «отличия от прода»?

Ответ

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

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

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

5. [junior] Какие графики обязательны в нагрузочном отчёте?

Ответ

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

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

Красный флаг: «средняя задержка по времени».

6. [middle] Как показать, что исправление узкого места помогло?

Ответ

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

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

Красный флаг: «запустил один раз, стало быстрее».

7. [middle] Тест показал p95 в пределах нормы, но выводы «система выдерживает» делать нельзя. Почему так может быть?

Ответ

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

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

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

8. [middle] Как построить рекомендации, чтобы их выполнили?

Ответ

Каждая рекомендация это конкретное действие с владельцем, сроком, ожидаемым эффектом в числах, трудозатратами и способом проверки. Их немного (3–5) и они упорядочены по приоритету: сначала дешёвое и сильное. Лучше с уже проверенным эффектом на стенде. Записывается в трекер как задача.

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

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

9. [middle] Руководитель просит «отчёт на одну страницу». Что ты оставишь?

Ответ

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

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

Красный флаг: «выброшу графики и оставлю таблицу».

10. [junior] [на скорость] Назови порядок разделов отчёта.

Ответ

Итог, цель и критерии, среда, профиль, результаты, узкие места, рекомендации, риски, приложения. Сверху вывод, снизу доказательства.

Что хотят услышать: итог первым.

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

11. [junior] [на скорость] Из каких четырёх частей состоит сильный вывод?

Ответ

Наблюдение, число, сравнение с целью или базой, значение для решения. Например: «на 40 сценариях/с p95 460 мс при цели 500: запас 8%, рост p95 на 40 мс и более нарушит SLO».

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

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

12. [junior] [на скорость] Почему в выводах не используют среднюю задержку?

Ответ

Среднее прячет редкие медленные запросы: один из ста может ждать секунды, а среднее будет «отлично». В выводах стоят p95 и p99 вместе с долей ошибок.

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

Красный флаг: «среднее проще понять».

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

Ubuntu 24.04, k6 2.3, стенд «Магазин» из project/shop (Python 3.14, FastAPI 0.142, PostgreSQL 18.6), Prometheus 3.15, Grafana 13.2, Mermaid xychart-beta на GitHub. Числа в примерах учебные: твои будут другими, важна форма. Октябрь 2026.

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

  • Объяснить, кто читает отчёт и какой слой ему нужен.
  • Написать итог в три строки: вердикт, запас, действие, с числами.
  • Описать среду, профиль и отличия от прода так, чтобы результат можно было сравнить и повторить.
  • Выбрать график под вопрос и подписать его выводом, с линией цели и без обрезанной оси.
  • Оформить узкое место по схеме «симптом, доказательство, эффект до/после».
  • Превратить наблюдение в вывод с числом, сравнением и значением для решения.
  • Написать рекомендации с владельцем, сроком и эффектом и честный раздел рисков.
  • Заполнить ~/perf-lab/reports/TEMPLATE.md по своему ступенчатому прогону.
  • Попросить нейросеть найти слабые места в отчёте, не позволяя ей добавлять числа, и сверить каждое замечание с данными.

Дальше: урок 12.2. Нагрузка в CI: регресс производительности: число из отчёта становится базовой линией, а проверка запускается сама на каждый коммит.

Проверь себя

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

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

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