load-tester Все курсы

✻ Урок 13.2 · Тема 13: Финал: работа и собеседования

Тестовое задание за один день

⏱ 4 ч

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

Я твой опытный коллега, и расскажу, как у меня сорвалось первое тестовое. Многие работодатели вместо долгих расспросов дают тестовое задание (take-home assignment): письмо с условиями, срок «за день-два» и просьба прислать результат. Я полдня писал красивый сценарий, на анализ остался час, отчёт строчил на бегу. Прислал папку без вывода и ответа не дождался.

Кандидаты чаще проваливают тестовое не из-за незнания инструмента, а из-за времени и подачи. За один день нанимающий видит всё сразу: читаешь ли ты чужое задание, задаёшь ли вопросы, проводишь ли честный эксперимент, доказываешь ли цифрами. И главное: пишешь ли отчёт, который занятый человек прочтёт за пять минут.

Шаг проекта: ты получишь текст задания по «Магазину», разберёшь его и составишь план на восемь часов. Потом подготовишь каталог ~/perf-lab/13-final/take-home/ и сделаешь сквозной минимум: сценарий и первый прогон. Целиком задание выполнишь в отдельный день с таймером. Урок рассчитан на 4 часа.

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

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

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

День укладывается в конвейер, у каждого этапа есть результат:

flowchart TD
    A["Разбор задания:<br/>вопросы и допущения"] --> B["Профиль нагрузки:<br/>цифры из условий"]
    B --> C["Сценарий и smoke:<br/>всё ходит и считается"]
    C --> D["Прогоны:<br/>ступени нагрузки и наблюдение"]
    D --> E["Анализ:<br/>узкое место и доказательство"]
    E --> F["Исправление и повтор:<br/>до и после"]
    F --> G["Отчёт:<br/>вывод первым"]

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

Теория

Кто читает твоё решение

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

Отсюда видно, на что тратить время: вывод сверху, расчёт нагрузки, подписанные графики, команды запуска в README.

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

Но прежде чем что-то делать, надо понять, что именно просят.

Разбор условий: вопросы и допущения

Любое тестовое неполное: пропущено то, что автор считает очевидным. Начинающие делают одну из двух ошибок: молча додумывают или заваливают автора двадцатью вопросами. Правильно выписать неясности и разделить их. Две-три критичные уходят автору письмом в начале, остальные закрываются допущениями (assumptions) и записываются в отчёт.

Пример. В задании «сервис должен выдерживать пиковую нагрузку». Что неясно: сколько это в числах, какие операции, что значит «выдерживает». Критичное обычно есть в тексте (проверь!), а если нет, спроси. Некритичное («сколько пользователей в БД», «что с кэшем») берёшь как есть и записываешь:

Допущение 1. Нагрузка задана сессиями в пиковый час; пик минуты в 1,5 раза выше среднего часа, запас на рост 2x.
Допущение 2. Настройки стенда не меняются до итогового прогона «после исправления».
Допущение 3. Тест идёт с одной машины, генератор не стал узким местом (проверяю CPU генератора и достигнутый RPS).

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

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

Допущения сказали, чего просят. Теперь переведём это в цифры.

Профиль нагрузки: из слов в цифры

«3000 сессий в час» ничего не говорит генератору. Считаем (тот же расчёт, что в уроке 8.4): 3000 сессий за час это 0,83 сессии в секунду. Пик внутри часа выше в 1,5 раза: 1,25. Запас на рост вдвое: 2,5 сессии в секунду. В одной сессии вход, три просмотра каталога, две карточки и корзина, плюс заказ в половине сессий и список заказов в 30%: 1 + 3 + 2 + 1 + 0,5 + 0,3, то есть 7,8 запроса, около восьми. Значит, 2,5 × 7,8 даёт примерно 20 запросов в секунду. Расчёт лучше положить в скрипт: его можно пересчитать, если автор поправит условия, и он служит доказательством. Готовый скрипт будет в практике, шаг 3.

Прикинь сам: автор задания поменял «3000 сессий» на «6000». Сколько запросов в секунду теперь?

Вдвое больше, то есть около 40: число растёт ровно пропорционально сессиям.

Профиль определяет, что ты найдёшь. Много входов упрутся в процессор из-за bcrypt (урок 11.2), много просмотров заказов найдут отсутствие индекса (база перебирает всю таблицу вместо быстрого поиска) и N+1 (на каждую строку списка отдельный запрос к базе; урок 11.3). Поэтому профиль нельзя подгонять под известное узкое место: берёшь из условий, что найдётся, то найдётся.

Главное: профиль считают от условий задания и кладут в скрипт, а не берут «100 пользователей» с потолка.

Цифры есть. Как их проверить честно?

Эксперимент: гипотеза, метод, критерий

Нагрузочный тест это эксперимент, и оформлять его стоит как эксперимент. Вопрос: выдерживает ли магазин 20 запросов в секунду с p95 каталога меньше 300 мс? Метод: ступени 5, 10, 20, 30, 40 запросов в секунду по три минуты, смесь из профиля, одна конфигурация. Критерий заранее: ступень пройдена, если p95 каталога меньше 300 мс, p95 заказа меньше 1 с, ошибок меньше 1% и p95 не растёт внутри ступени. Контроль: минутный smoke при двух пользователях до замеров: всё отвечает 200, числа сходятся с Prometheus.

Prometheus собирает метрики раз в 5 секунд, окно rate берёт минуту, а первую минуту ступени система прогревается. Значимы вторая и третья минуты: на ступенях короче видишь только переходной процесс.

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

На одной из ступеней сломалось. Как найти почему за ограниченное время?

Как искать узкое место за ограниченное время

Времени мало, поэтому идём от дешёвых проверок к дорогим, как в уроке 11.1:

Не прыгай к шагу 5: гипотеза без метрик это угадывание.

Для каждого вывода в отчёте нужны минимум два независимых подтверждения. Для «упёрлись в пул БД»: shop_db_pool_waiting больше нуля там, где растёт p95, и 503 в логах («couldn’t get a connection after 5.00 sec»). Один график это впечатление, два совпадающих источника это доказательство.

Осторожно: причина это не то, что просто совпало. Если p95 вырос и одновременно вырос CPU, это ещё не причинность. Проверь экспериментом «до и после».

Главное: узкое место ищут от дешёвых проверок к дорогим, а вывод держится на двух независимых подтверждениях и опыте «до и после».

Мы знаем, что делать. Осталось распорядиться временем.

Как распорядиться восемью часами

«У меня весь день, начну со сценария» и пять часов уходит на сценарий. Спасает тайм-бокс (time-box): жёсткий лимит на этап, и когда он вышел, идёшь дальше, даже если результат не идеален. Подвигай ползунки и посмотри, как перераспределение бьёт по отчёту:

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

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

Главное: у каждого этапа жёсткий лимит, к полудню есть сквозной минимум, а заметки ведутся по ходу.

Теперь работу надо показать так, чтобы прочли.

Отчёт: вывод первым

Структура опирается на урок 12.1, но на тестовом она жёстче: один экран отвечает на вопрос задания.

Вывод. Магазин держит 20 RPS в смеси из профиля при p95 каталога 180 мс. На 30 RPS p95 каталога
растёт до 1,1 с, на 40 появляются ошибки. Узкое место: shop упирается в квоту одного ядра (cpus 1.0) из-за bcrypt при входе.
Рекомендация: дать shop второе ядро (cpus 2.0). После правки 30 RPS проходит, p95 каталога 160 мс.

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

До 20 всё в порядке, на 30 появляется «полка» и взлёт p95, на 40 уже ошибки: классическая «хоккейная клюшка» из урока 8.1. Автор не остановился на графике. У shop загрузка CPU около 100% при 20% у PostgreSQL, docker stats показывает упор в один процессор, а /api/login даёт заметную долю времени (bcrypt). Гипотеза: «shop упирается в квоту одного ядра». Эксперимент: cpus: "2.0" для shop и повтор ступеней 20 и 30. Добавить воркеров не помогло бы: они делят ту же квоту, это ты видел в уроке 11.2. До и после: на 30 RPS p95 каталога 1,1 с против 0,16 с. В «ограничениях» он честно пишет: стенд на ноутбуке, генератор на той же машине, база маленькая, результат переносится на прод только как направление.

В другом запуске узким местом окажется /api/orders (нет индекса, N+1), и это такой же хороший ответ, если он доказан. Проверяющему важно, как ты к нему пришёл.

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

Осталось сверить себя со шкалой проверяющего.

Критерии оценки

Это шкала, по которой ставят оценку, максимум 20 баллов. Используй её перед сдачей. По каждому пункту 0 баллов это «нет», 4 балла это «сделано полностью»:

Критерий 2 балла 4 балла
Профиль расчёт есть, но неполный от условий, смесь операций, допущения
Эксперимент ступени, но условия смешаны ступени, smoke, равные условия, критерий заранее
Узкое место названо, одно подтверждение два независимых подтверждения
Исправление предложено словами проверено «до и после»
Отчёт и честность вывод есть, ограничения слабые вывод сверху, подписи, «что не успел», воспроизводимость

В таблице пять строк по 4 балла, максимум 20. Порог «берём дальше» обычно 12-14: это «в целом сильно, но есть дыры». Не хватает их чаще всего в строках «Узкое место» и «Отчёт». Самые дорогие ошибки: нет вывода в начале, только средние без p95, несколько изменений за прогон, графики без единиц. Сдавай с запасом в полчаса и проверь работу «чистым» клоном. И смотри генератор: процесс Locust у 100% ядра или его предупреждение CPU usage above 90% значит, что ты, скорее всего, меряешь генератор, а не сервис. 70% это мой запас для планирования, а не доказательство: выше него я проверяю, совпадает ли достигнутый RPS с заданным и растёт ли он со вторым процессом Locust.

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

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

Практика

1. Подготовь рабочий каталог

mkdir -p ~/perf-lab/13-final/take-home/{scripts,results,screenshots}
cd ~/perf-lab/13-final/take-home

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

Создай файл с текстом задания, чтобы он всегда был под рукой:

cat > TASK.md <<'EOF'
# Тестовое задание: «Магазин» перед распродажей

Вам передан интернет-магазин (стенд из репозитория, поднимается `docker compose up -d`).
В пятницу запланирована распродажа. Нужно проверить, выдержит ли магазин ожидаемую нагрузку,
найти главное узкое место и предложить, как его убрать.

Условия:
1. Ожидаемая нагрузка: 3000 пользовательских сессий в пиковый час. Пик внутри часа выше среднего
   в 1,5 раза. Заложите запас на рост в 2 раза.
2. Сессия: один вход, 3 просмотра каталога, 2 карточки товара, добавление товара в корзину,
   в половине сессий оформление заказа, в 30% сессий просмотр списка своих заказов.
3. Критерии успеха: p95 каталога меньше 300 мс, p95 оформления заказа меньше 1 с,
   доля ошибок меньше 1%.
4. Нагружать только локальный стенд.

Что сдать (срок: 8 часов):
1. Скрипты нагрузки и инструкция запуска.
2. Расчёт профиля нагрузки.
3. Результаты прогонов: ступенчатый тест и дашборд.
4. Отчёт (до двух страниц): вывод, узкое место с доказательствами, рекомендации.
5. Проверка одного исправления: прогон до и после.
EOF

Как читать задание: пункт 1 даёт цифры, пункт 2 даёт смесь операций, пункт 3 критерии успеха, пункт 4 границы. Пункт «сдать» это твой чек-лист: каждая строка станет файлом.

2. Вопросы и допущения

cat > questions.md <<'EOF'
# Вопросы и допущения

## Вопросы автору (критичные)
1. Критерии p95 относятся к каждому маршруту отдельно или к смеси целиком?
2. Допустимо ли менять конфигурацию стенда для исправления (переменные `.env`), или только SQL и код?

## Допущения (если ответа нет)
1. Критерии применяю к каждому маршруту отдельно: «каталог» это `/api/products`, «оформление заказа» это `/api/orders` (POST).
2. Менять можно только то, что описано в README стенда (переменные `.env`, индекс в БД).
3. Генератор и стенд на одной машине; контролирую CPU генератора и достигнутый RPS.
4. Пик внутри часа 1,5x, запас на рост 2x, как в задании.
EOF

Измени вопросы и допущения под себя. Главное: каждый пункт, который ты принял без ответа, записан.

3. Расчёт профиля скриптом

cat > scripts/calc.py <<'EOF'
sessions_per_hour = 3000
peak = 1.5      # пик внутри часа к среднему
growth = 2      # запас на рост
per_session = {            # операций за сессию
    "login": 1, "catalog": 3, "product": 2,
    "cart": 1, "order": 0.5, "my_orders": 0.3,
}
sessions_per_sec = sessions_per_hour / 3600 * peak * growth
print(f"сессий в секунду: {sessions_per_sec:.2f}")
total = 0
for name, count in per_session.items():
    rps = sessions_per_sec * count
    total += rps
    print(f"{name:10s} {rps:5.2f} RPS")
print(f"{'итого':10s} {total:5.2f} RPS")
EOF
python3 scripts/calc.py | tee results/profile.txt

Скрипт делит сессии в час на 3600, умножает на пик и запас, потом на число операций за сессию. tee показывает вывод и сохраняет его в файл (повтор из урока 1.2).

сессий в секунду: 2.50
login       2.50 RPS
catalog     7.50 RPS
product     5.00 RPS
cart        2.50 RPS
order       1.25 RPS
my_orders   0.75 RPS
итого      19.50 RPS

Как читать вывод: целевая нагрузка около 20 RPS. Посмотри на login: 2,5 входа в секунду. Каждый вход это bcrypt с параметром 12, то есть заметная часть секунды процессорного времени единственного ядра shop. Это первая гипотеза на проверку, но гипотеза, а не вывод: проверять её будешь метриками.

4. План на день

cat > plan.md <<'EOF'
# План (старт 09:00)
| Время | Этап | Результат |
| --- | --- | --- |
| 09:00-09:30 | Разбор, вопросы, допущения | questions.md |
| 09:30-10:00 | Профиль | scripts/calc.py, results/profile.txt |
| 10:00-11:15 | Сценарий, smoke | scripts/locustfile.py, минутный прогон без ошибок |
| 11:15-12:45 | Ступенчатый прогон | results/step_*.csv, скриншоты |
| 12:45-14:00 | Анализ, узкое место | notes.md с доказательствами |
| 14:00-15:00 | Исправление, повтор | results/after_*.csv |
| 15:00-16:30 | Отчёт | report.md |
| 16:30-17:00 | Запас, проверка, push | всё закоммичено |
EOF

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

5. Сценарий нагрузки

Если в теме 9 ты написал свой сценарий покупателя, возьми его за основу. Ниже самодостаточный вариант, где одна задача Locust это целая сессия из условий:

cat > scripts/locustfile.py <<'EOF'
import random
import time

from locust import HttpUser, between, task


def think():
    time.sleep(random.uniform(0.2, 0.6))


class Session(HttpUser):
    wait_time = between(1, 2)

    @task
    def visit(self):
        number = random.randint(1, 1000)
        with self.client.post("/api/login", name="/api/login", catch_response=True,
                              json={"email": f"user{number:04d}@shop.lab", "password": "password"}) as r:
            if r.status_code != 200:
                r.failure(f"вход: {r.status_code}")
                return
            token = r.json()["token"]
        headers = {"Authorization": "Bearer " + token}
        for _ in range(3):
            self.client.get("/api/products", params={"page": random.randint(1, 10)}, name="/api/products")
            think()
        product = random.randint(1, 10000)
        for _ in range(2):
            self.client.get(f"/api/products/{random.randint(1, 10000)}", name="/api/products/[id]")
            think()
        self.client.post("/api/cart/items", json={"product_id": product, "qty": 1},
                         headers=headers, name="/api/cart/items")
        if random.random() < 0.5:
            think()
            self.client.post("/api/orders", headers=headers, name="/api/orders")
        if random.random() < 0.3:
            think()
            self.client.get("/api/orders", headers=headers, name="/api/orders [GET]")
EOF

Разбор. Каждая итерация задачи visit это новая сессия: вход, три просмотра каталога, две карточки, корзина, заказ в половине случаев, список заказов в 30% случаев. Такая структура воспроизводит расчёт: входы идут постоянно, а не один раз на пользователя, поэтому bcrypt нагружается так, как задумано профилем. name= собирает похожие URL в одну строку статистики, а имя "/api/orders [GET]" отделяет просмотр заказов от оформления (иначе два разных метода слились бы в одну строку).

Минутный smoke-прогон до больших замеров:

source ~/perf-lab/.venv/bin/activate
cd ~/perf-lab/13-final/take-home
locust -f scripts/locustfile.py --host http://localhost:8000 --headless -u 2 -r 1 -t 1m --only-summary --csv results/smoke

Ожидаемое: в итоговой таблице Fails равно 0 у всех строк, в Aggregated порядка 10-15 запросов в секунду на двух пользователях не ожидай: у каждого пользователя пауза между сессиями, получится около 3-4 RPS.

Типичные ошибки: Все строки входа красные, а остальных маршрутов в таблице нет: вход вернул не 200 (в скрипте стоит return после r.failure), смотри текст ошибки в results/step_<N>_failures.csv и убедись, что стенд поднят. Все /api/orders дают 400: корзина пуста, потому что ты не положил товар (в сценарии заказ идёт после корзины, проверь порядок). ModuleNotFoundError: locust: не активировано окружение, source ~/perf-lab/.venv/bin/activate.

6. Ступенчатый прогон и сводка

cat > scripts/run_steps.sh <<'EOF'
#!/usr/bin/env bash
# Ступени по числу пользователей: 3, 6, 12, 18, 24 (ожидаемо около 5, 10, 19, 24, 25 RPS: потолок около 25)
# Без set -e: Locust завершается кодом 1, если на ступени были ошибки, и это не повод бросать тест.
set -u
cd "$(dirname "$0")/.."
mkdir -p results
rm -f results/step_*   # файлы прошлого прогона не должны попасть в сводку
failed=0
for users in 3 6 12 18 24; do
  echo "=== ступень: $users пользователей, $(date +%T)"
  locust -f scripts/locustfile.py --host http://localhost:8000 --headless \
         -u "$users" -r 3 -t 3m --only-summary --csv "results/step_$users" > "results/step_$users.log" 2>&1
  code=$?
  echo "$code" > "results/step_$users.exit"
  echo "    код выхода Locust: $code"
  if [ "$code" -gt 1 ]; then failed=1; fi   # 0 и 1 значат, что ступень дошла до конца
done
exit "$failed"
EOF
chmod +x scripts/run_steps.sh
cat > scripts/summary.py <<'EOF'
import csv
import glob
import os
import re
import sys

# Критерии из задания: p95, мс, по каждому целевому запросу
LIMITS = {"/api/products": 300, "/api/orders": 1000}
MAX_ERRORS = 1.0  # процентов, по всей смеси


def step_users(path):
    return int(re.search(r"step_(\d+)", path).group(1))


invalid = 0
print(f"{'польз.':>6} {'RPS':>6} {'ошибки, %':>9} {'p95 каталога':>13} {'p95 заказа':>11}  вердикт")
# Ступени берём по файлам .exit: их пишет run_steps.sh этого прогона.
for exit_file in sorted(glob.glob("results/step_*.exit"), key=step_users):
    users = step_users(exit_file)
    code = open(exit_file).read().strip()
    path = f"results/step_{users}_stats.csv"
    if code not in ("0", "1") or not os.path.exists(path):
        print(f"{users:6d}  замер недействителен: Locust упал (код {code}), смотри results/step_{users}.log")
        invalid += 1
        continue
    rows = {row["Name"]: row for row in csv.DictReader(open(path))}
    total = rows.get("Aggregated")
    missing = [name for name in LIMITS if name not in rows or int(rows[name]["Request Count"]) == 0]
    if total is None or int(total["Request Count"]) == 0 or missing:
        print(f"{users:6d}  замер недействителен: нет запросов {', '.join(missing) or 'вообще'} (код {code})")
        invalid += 1
        continue
    errors = 100 * int(total["Failure Count"]) / int(total["Request Count"])
    p95 = {name: float(rows[name]["95%"]) for name in LIMITS}
    bad = [name for name, limit in LIMITS.items() if p95[name] >= limit]
    if errors >= MAX_ERRORS:
        bad.append("ошибки")
    verdict = "ок" if not bad else "НАРУШЕНО: " + ", ".join(bad)
    print(f"{users:6d} {float(total['Requests/s']):6.1f} {errors:9.1f} {p95['/api/products']:13.0f} "
          f"{p95['/api/orders']:11.0f}  {verdict} (код {code})")
sys.exit(1 if invalid else 0)
EOF

Скрипт run_steps.sh запускает Locust пять раз подряд по 3 минуты и сохраняет results/step_<N>_stats.csv. summary.py берёт из этих файлов строку «Aggregated» (RPS и долю ошибок) и p95 двух целевых запросов, каталога и заказа, и сверяет их с критериями из задания. Ступень с ошибками не обрывает прогон: run_steps.sh не использует set -e (иначе первый код выхода 1 остановил бы остальные ступени) и записывает код каждой ступени в results/step_<N>.exit, а вывод Locust в step_<N>.log. В начале он стирает файлы прошлого прогона (rm -f results/step_*), а summary.py берёт только ступени с файлом .exit, так что старый CSV в сводку не попадёт. Ступень считается настоящим замером, только если Locust завершился кодом 0 или 1 и в CSV есть запросы к обоим целевым адресам. Иначе (Locust упал, не нашёл файл сценария, заказов не было вовсе) вместо цифр будет «замер недействителен», а summary.py завершится кодом 1: подставить ноль и написать «ок» было бы враньём. Запусти ступени в фоне и наблюдай дашборд (это и есть главная часть):

./scripts/run_steps.sh

Пока идёт, в Grafana (период «последние 30 минут») следи за p95 по маршрутам, 5xx, CPU контейнера shop, пулом БД и длительностью оплаты. На границах ступеней делай скриншоты панелей в screenshots/. После конца:

python3 scripts/summary.py | tee results/summary.txt
польз.    RPS ошибки, %  p95 каталога  p95 заказа  вердикт
     3    5.1       0.0           110         400  ок (код 0)
     6    9.7       0.0           120         450  ок (код 0)
    12   19.0       0.0           240         700  ок (код 0)
    18   24.2       0.1           290        1300  НАРУШЕНО: /api/orders (код 1)
    24   25.1       4.8          3900        7000  НАРУШЕНО: /api/products, /api/orders, ошибки (код 1)

Как читать вывод: RPS растёт вместе с пользователями до 12, потом упирается в потолок около 25 (насыщение), а p95 уходит вверх: классическое «колено». В колонках p95 смотри на каждый запрос отдельно: на 18 пользователях каталог ещё укладывается в 300 мс, а заказ уже нет (1300 мс при пороге 1000), и в строке «Aggregated» эту поломку было бы не видно, потому что быстрые запросы разбавляют медленные. «Код» в конце строки это код выхода Locust: 0 значит ошибок не было, 1 значит были (на ступенях с кодом 1 тест не прерывается, это нормально). Пример иллюстративный. Твоя цель: найти ступень, где нарушен критерий, и сделать вывод о пределе. Не забудь, что «пользователи» в Locust это закрытая модель: рост пользователей не равен росту нагрузки в RPS, поэтому в отчёте ты указываешь именно достигнутый RPS.

7. Заметки по ходу и отчёт

Во время работы веди notes.md:

echo "- $(date +%T) запустил ступени 3-24, p95 каталога растёт с 18 пользователей" >> notes.md

В конце собери отчёт из заметок по шаблону:

cat > report.md <<'EOF'
# Отчёт: «Магазин» перед распродажей

## Вывод
<3 предложения: что выдерживает, где ломается и почему, что рекомендуем и что получилось после>

## Что проверяли
- Цель: 20 RPS, критерии: p95 каталога < 300 мс, p95 заказа < 1 с, ошибки < 1%.
- Профиль: расчёт в `scripts/calc.py`, смесь операций из задания.
- Метод: ступени по 3 минуты, одна конфигурация, генератор и стенд на одной машине.

## Результаты
<таблица из results/summary.txt и график RPS и p95 по ступеням>

## Узкое место
<гипотеза; два независимых подтверждения со скриншотами и запросами PromQL>

## Рекомендации
| # | Что сделать | Ожидаемый эффект | Приоритет |
| --- | --- | --- | --- |

## Проверка исправления: до и после
<таблица: ступень, RPS, p95, ошибки до и после изменения; что именно изменили>

## Ограничения и что не успел
- <допущения, чем отличается стенд от прода, что не проверил>

## Как повторить
<команды: подъём стенда, запуск ступеней, сводка, версии>
EOF

Заполни его по результатам. Для узкого места используй цепочку шагов из теории, для проверки исправления внеси ровно одно изменение (например, квоту cpus для shop или индекс по рекомендациям уроков темы 11), повтори ступени 12 и 18 пользователей в results/after_* и впиши сравнение.

8. Самопроверка и сдача

Прогони отчёт и папку по таблице критериев. Для каждой строки поставь себе 0, 2 или 4 балла и запиши итог в конец report.md (строкой «Самооценка: N из 20, слабое место: …»). Потом проверь воспроизводимость: склонируй репозиторий в пустой каталог и убедись, что README из take-home позволяет запустить хотя бы calc.py и summary.py.

cd ~/perf-lab
git add 13-final/take-home
git commit -m "13.2: тестовое задание: профиль, сценарий, прогоны, отчёт"
git push

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

Проверь, как ты ведёшь себя, когда план ломается. Выполни эту тренировку на пустой голове, после первого прогона:

  1. Во время ступени 18 остановите контейнер магазина: docker compose stop shop (из ~/learning/load-tester/project/shop), подожди 20 секунд, запусти: docker compose start shop. Ты только что сымитировал «потерю часа» из-за технической неполадки.
  2. Что с этим сделать: не паниковать, записать в notes.md («17:05 стенд перезапущен вручную, ступень 18 невалидна»), перезапустить ступень целиком и сохранить новый результат. Нельзя «склеивать» прогон, в котором была остановка, с чистым.
  3. Измени план: что ты вырежешь, если времени остался час вместо двух? (Подсказка: исправление остаётся, красивые графики сокращаются, отчёт нет.)

Вторая поломка: твой генератор оказался узким местом. Запусти ступень 24 и в соседнем терминале смотри top: если процесс locust занимает 100% ядра, то 25 RPS это предел генератора, а не магазина. Как это проверить честно: запустить два процесса Locust (--processes 2 или режим --master/--worker из урока 9.5) и убедиться, что RPS вырос. Запиши в отчёт, как ты убедился, что генератор не ограничивает результат.

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

ИИ в помощь

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

Сначала узнай правила. Задай организаторам вопрос: «Можно ли пользоваться ИИ-помощником, и нужно ли это указать в отчёте?» Если запрещено, не используй. Если разрешено, честно напиши в отчёте, где помогал ИИ. Сданное под своим именем, чего ты не понимаешь, на собеседовании раскроется за минуту.

Задача: найти вопросы к заданию до начала работы.

Вот текст тестового задания по нагрузочному тестированию: <вставь текст без названий компании и закрытых данных>.
Составь список вопросов к автору: что неясно в цели, критериях, среде, данных и сроках. Для каждого вопроса
скажи, какое допущение я приму, если ответа не будет.

Проверь ответ: оставь только вопросы, которые меняют твою работу, лишние вычеркни. Типичная ошибка: общие вопросы вроде «какая цель проекта?», когда цель уже написана в тексте.

Задача: понять строки сценария, которые ты не писал.

Вот фрагмент сценария Locust 2.46 (или k6 2.3), с которым я работаю: <вставь код>.
Объясни по строкам, что делает каждая часть, что может сломаться, и что бы ты проверил перед запуском.
Я должен уметь объяснить этот код вслух, так что говори простыми словами.

Проверь ответ: запусти код на стенде, измени параметр и убедись, что поведение изменилось так, как сказано. Типичная ошибка: устаревший API (HttpLocust, --slaves в Locust, старые команды k6) и придуманные параметры.

Закрытые данные заказчика и ключи в чат не отправляй: замени их на заглушки.

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

Термин Простыми словами
Тестовое задание (take-home assignment) Задача на дом от работодателя: выполняешь сам за заданное время и присылаешь результат.
Допущение (assumption) Явно записанное «я считаю так», когда в условии не хватает данных.
Тайм-бокс (time-box) Жёсткий лимит времени на этап: вышел срок, переходишь к следующему.
Критерий успеха Заранее заданные числа (p95, ошибки), по которым решают, прошёл ли тест.
Ступенчатая нагрузка Тест, где нагрузка растёт ступенями, и на каждой ступени держится постоянной.
Воспроизводимость Свойство результата: другой человек по твоим командам получает то же самое.
До и после Сравнение двух прогонов с одним изменением, доказывающее, что оно помогло.
Ограничения отчёта Честный список того, что в тесте отличалось от реальности и что не проверено.

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

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

1. [junior] [часто] Как ты подходишь к тестовому заданию, если в условиях не хватает данных?

Ответ

Выписываю неясности и делю на критичные и некритичные. Критичные (цель, критерий успеха) уточняю у автора одним коротким письмом в начале. Остальное закрываю допущениями и записываю их в отчёт. Так я не блокируюсь и не молчу, а проверяющий видит ход мысли.

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

Красный флаг: «я сам всё додумал» или двадцать вопросов на старте.

2. [junior] [часто] Как ты выбираешь нагрузку для теста, если заказчик дал только «пиковую посещаемость»?

Ответ

Перевожу в RPS: сессии в пиковый час делю на 3600, умножаю на пик внутри часа и запас на рост, потом на число операций каждого вида за сессию. Расчёт кладу в скрипт, чтобы его можно было пересчитать. Допущения (пик 1,5x, запас 2x) называю явно.

Что хотят услышать: расчёт от сессий к RPS, смесь операций, запас.

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

3. [junior] [часто] Что должно быть на первом экране отчёта?

Ответ

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

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

Красный флаг: вывод в конце после десяти графиков.

4. [junior] Почему ступени нагрузки держат несколько минут, а не секунд?

Ответ

Система прогревается, метрики собираются с шагом (Prometheus каждые 5 секунд), а окно rate берёт минуту. На короткой ступени видишь только переходной процесс. Значимы последние минуты ступени, когда значения установились.

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

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

5. [junior] Как понять, что тест упёрся в генератор, а не в сервис?

Ответ

Смотрю загрузку CPU генератора: если процесс Locust держит почти 100% ядра, RPS не растёт, а у сервиса CPU и пул свободны, то узкое место генератор. Проверяю запуском нескольких процессов или воркеров: если RPS вырос, это был генератор.

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

Красный флаг: «если RPS не растёт, значит сервер слабый».

6. [middle] Ты нашёл, что p95 вырос при росте CPU. Можно ли писать, что причина в CPU?

Ответ

Не сразу: совпадение не причинность. Нужно второе независимое подтверждение (например, профиль py-spy показывает горячую функцию, а при втором ядре для сервиса p95 падает) и эксперимент «до и после» с одним изменением. Только тогда вывод доказан.

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

Красный флаг: «CPU вырос, значит CPU и виноват».

7. [middle] Как ты оформляешь проверку исправления?

Ответ

Меняю одно за прогон, остальные условия те же (стенд, сценарий, ступени, длительность). Таблица «до и после» по тем же ступеням: достигнутый RPS, p95, ошибки. Фиксирую, что именно изменено (конфиг, коммит). Если эффект малый, повторяю прогон, чтобы оценить разброс.

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

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

8. [middle] Что ты сделаешь, если за час до конца срока узкое место не найдено?

Ответ

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

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

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

9. [middle] Что ты включишь в раздел «Ограничения»?

Ответ

Чем стенд отличается от прода (один хост, ограничения Docker, маленькая база), что генератор и сервис на одной машине, какие допущения принял, чего не проверял (долгий soak, пиковые всплески), насколько результат переносится на реальную систему. Это защита от ложной уверенности.

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

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

10. [на скорость] Назови пять частей структуры отчёта.

Ответ

Вывод; что проверяли; результаты; узкое место с доказательствами; рекомендации. Дополнительно: до/после, ограничения, как повторить.

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

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

11. [на скорость] Почему «стало быстрее» без цифр не принимается?

Ответ

Это нельзя проверить и нельзя сравнить. Нужны условия, до и после, p95 и ошибки.

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

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

12. [на скорость] Сколько изменений за один прогон «до и после»?

Ответ

Одно: иначе не знаешь, что помогло.

Что хотят услышать: «одно».

Красный флаг: «сколько нужно».

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

Ubuntu 24.04, Locust 2.46, Python 3.12, стенд «Магазин» из project/shop (Python 3.14, FastAPI 0.142, PostgreSQL 18.6), Prometheus 3.15, Grafana 13.2. Числа в примерах вывода иллюстративные. Октябрь 2026.

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

  • Разобрать условия тестового задания, выписать вопросы и допущения.
  • Перевести условия задачи в целевой RPS и смесь операций скриптом.
  • Расписать восемь часов по этапам с тайм-боксами и запасом.
  • Написать сценарий, который воспроизводит профиль, и проверить его smoke-прогоном.
  • Провести ступенчатый прогон, собрать сводку и найти ступень, где нарушен критерий.
  • Доказать узкое место двумя независимыми фактами и проверить исправление «до и после».
  • Написать отчёт с выводом сверху, ограничениями и инструкцией повтора, и оценить его по критериям.
  • Выяснить, разрешён ли ИИ на тестовом, указать его участие и объяснить каждую строку сданной работы.

Дальше: урок 13.3. Портфолио и резюме, где ты оформишь всё сделанное в портфолио и резюме.

Проверь себя

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

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

тема 13 урок 13.2 4 ч курс 0/0 ← → уроки