{ "index": 222, "slug": "editorial-2021-11-practice-load-testing", "title": "Нагрузочное тестирование: как связать профиль, измерение и решение", "excerpt": "Нагрузочный тест даёт полезный ответ только тогда, когда команда заранее описала сценарий, среду, данные и критерий успеха. Разбираем практическую схему, пример на k6 и отрицательный путь, который не позволяет выдать учебный счётчик за capacity.", "contentHtml": "
Проблема: после запуска команда видит растущую задержку и ошибки, но не может сказать, какой сценарий их вызвал; цена ошибки — неверный фикс, повторный прогон на другой среде и пропущенный отказ в production.
\nНагрузочное тестирование проверяет не абстрактное «быстрее или медленнее», а поведение системы при заданном потоке работы. Поэтому сначала описывают workload, затем выбирают инструмент и метрики. Workload отвечает на четыре вопроса: какой запрос выполняется, с какими данными, с какой моделью поступления и до какой границы продолжается проверка. Если хотя бы один ответ потерян, итоговый RPS мало что объясняет.
\nТезис простой: нагрузочный тест становится инженерным доказательством только тогда, когда его вход, наблюдение и решение связаны одним контрактом. Число запросов без контекста не показывает ни причину задержки, ни безопасный следующий шаг.
\nЗапросы создают конкуренцию за ограниченные ресурсы. Это могут быть соединения к базе, worker-потоки, CPU, память, дисковый ввод-вывод или лимит внешнего API. Пока запас ресурса велик, время ответа растёт слабо. Когда очередь становится длиннее, система начинает накапливать работу. Затем появляются таймауты, ретраи и ошибки. Ретраи увеличивают поток и могут ускорить отказ.
\nОдин итоговый счётчик скрывает переходы между этими состояниями. Профиль сохраняет их: короткий прогрев проверяет сам сценарий, стабильный участок даёт повторяемую базу, ступенька проверяет заранее выбранную границу, а восстановление показывает, уходит ли очередь после снижения входа. Названия сегментов не важны сами по себе. Важно объяснить роль каждого сегмента и его условия.
\nНужно также различать закрытую и открытую модели. В закрытой модели следующий шаг обычно начинается после завершения предыдущего. Замедление системы уменьшает фактический поток. В открытой модели новые итерации стартуют по расписанию независимо от ответа системы. Замедление увеличивает число одновременно работающих итераций. Эти модели отвечают на разные вопросы. Их нельзя сравнивать только по числу виртуальных пользователей.
\n| Поле | Пример | Зачем оно нужно |
|---|---|---|
| Сценарий | GET /catalog/items, доля чтений 80% | Показывает, какую работу моделирует тест |
| Данные | Версия набора, размер ответа, правило очистки | Отделяет эффект данных от эффекта кода |
| Модель входа | 30 итераций в секунду в течение 5 минут | Объясняет, что означает скорость |
| Наблюдение | p95, p99, доля ошибок, насыщение CPU | Связывает симптом с ресурсом |
| Порог | p95 < 300 мс, ошибки < 1% | Превращает график в проверяемое решение |
Допустим, нужно проверить чтение каталога перед изменением индекса. Учебный сценарий ниже показывает форму проверки. Он не обращается к реальному сервису и не подтверждает его производительность. Адрес, длительность, скорость и пороги здесь демонстрационные. В рабочем тесте их заменяют условиями конкретного сервиса.
\nimport http from 'k6/http';\nimport { check, sleep } from 'k6';\n\nexport const options = {\n scenarios: {\n catalog_read: {\n executor: 'ramping-arrival-rate',\n startRate: 5,\n timeUnit: '1s',\n preAllocatedVUs: 10,\n maxVUs: 50,\n stages: [\n { target: 5, duration: '30s' },\n { target: 30, duration: '2m' },\n { target: 5, duration: '30s' }\n ]\n }\n },\n thresholds: {\n http_req_failed: ['rate<0.01'],\n http_req_duration: ['p(95)<300']\n }\n};\n\nexport default function () {\n const response = http.get(`${__ENV.BASE_URL}/catalog/items`);\n check(response, {\n 'status is 200': (r) => r.status === 200,\n 'body is not empty': (r) => r.body.length > 0\n });\n sleep(1);\n}\nВ этом примере ramping-arrival-rate задаёт изменение скорости старта итераций, а не обещает, что сервер завершит их с той же скоростью. Параметр maxVUs ограничивает ресурс самого генератора. Если виртуальных пользователей не хватает, тест может не поддержать заданный arrival rate. Это отдельный сигнал о конфигурации теста, а не доказательство отказа приложения.
Порог тоже не равен факту. Запись p(95)<300 задаёт правило pass/fail для конкретной метрики. Она не говорит, почему порог нарушен. Причину ищут по временной связи с очередями, CPU, базой, внешними вызовами и лимитами. Если порог не был согласован до запуска, команда легко выбирает удобное объяснение уже после графика.
| Симптом | Возможная причина | Проверка | Действие |
|---|---|---|---|
| p95 растёт, RPS почти не меняется | Очередь внутри сервиса или зависимость отвечает медленнее | Сопоставить latency с queue depth, CPU, pool и временем внешних вызовов | Найти первую насыщенную очередь; не увеличивать таймаут вслепую |
| Ошибки появляются только на ступени | Пересечён лимит соединений, workers или внешнего API | Проверить лимиты и распределение ошибок по endpoint и коду | Изменить лимит или сценарий только после подтверждения владельца ресурса |
| График выглядит лучше после увеличения VU генератора | Генератор был bottleneck и не подавал заявленный поток | Сравнить запланированный и фактически начатый arrival rate | Увеличить ресурс генератора и повторить тот же сценарий |
| Тест стабилен, но production падает | Не совпали данные, зависимости, размер ответа или модель входа | Сравнить версии среды и workload по карточке запуска | Закрыть различия; не объявлять стенд доказательством production capacity |
| После снижения входа ошибки продолжаются | Очередь не успевает опустеть или ретраи продолжают поток | Наблюдать recovery, backlog и число повторных попыток | Остановить тест по лимиту и разобрать остаточную работу отдельно |
Иллюстрация полезна как проверка полноты. Если на ней нельзя подписать источник входа, границу сегмента и условие остановки, этих данных, скорее всего, нет и в описании запуска. Сегменты должны быть достаточно длинными для выбранного наблюдения. Короткая ступенька может показать только разогрев клиента, кэша или соединительного пула.
\nНагрузочный тест не измеряет «мощность приложения» вообще. Он измеряет ответ системы на конкретный workload в конкретной среде. Учебная модель с четырьмя сегментами не моделирует сеть, TLS, DNS, планировщик, базу, кэш, балансировщик, реальные пользовательские данные или распределённый генератор. Искусственно отклонённая итерация в такой модели не является HTTP-ошибкой и не превращается в error rate.
\nДаже реальный запуск может дать ложное спокойствие. Кэш прогрелся, база использовала другой план, тестовый набор меньше рабочего, а внешняя система не участвовала. Обратная ситуация тоже возможна: генератор упёрся в CPU, и сервис получил меньше входа, чем было заявлено. Поэтому отрицательный результат нужно сохранять с контекстом, а положительный — ограничивать теми же условиями.
\nНе лечите симптом увеличением таймаута, числа ретраев или VU, пока не проверили источник задержки. Таймаут может уменьшить число видимых ошибок и увеличить незавершённую работу. Ретрай может улучшить долю успешных ответов для клиента и одновременно удвоить нагрузку на зависимость. Любое действие проверяйте повторным прогоном с тем же профилем.
\nТест готов к инженерному решению, если другой человек может по его записи восстановить сценарий, среду, данные, модель входа, пороги и остановку; фактический вход отделён от возможностей генератора; результаты привязаны к версиям и времени; а каждый вывод отвечает на вопрос, который был сформулирован до запуска. Для изменения индекса этого достаточно, чтобы сравнить два запуска на одинаковом контракте. Для заявления о production capacity потребуются отдельные условия, масштаб и согласование владельцев системы.
\nПрактический финал должен быть коротким: «при такой модели входа и таких данных p95 пересёк порог на такой ступени; одновременно вырос такой ресурс; повтор с теми же условиями подтвердил или опроверг результат». Если в эту фразу нельзя вставить источник наблюдения и границу применимости, тест ещё не закончен.
\n