{ "index": 220, "slug": "editorial-2021-11-field-load-testing", "title": "Нагрузочный сигнал: как отличить проблему среды от узкого места", "excerpt": "Рост ошибок или задержки после нагрузочного теста ещё не указывает на bottleneck. Разбираем evidence, workload и границы измерения, а затем выбираем одно проверяемое действие.", "contentHtml": "

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

\n

Цена ошибки — не только лишние минуты на расследование. Неверный лимит может скрыть отказ зависимости. Повторный прогон в другой среде создаёт ложное сравнение. Исправление кода по агрегату без сценария может ухудшить обычный путь пользователя. В итоге команда получает красивое число без доказательства того, что именно оно измерило.

\n

Тезис простой: нагрузочный сигнал становится основанием для изменения только тогда, когда его можно связать с конкретным endpoint, workload, средой, данными и критерием завершения. До этого signal — повод для проверки. Диагностика должна разделить три ветки: изменилась среда, неполон сценарий или проявилась граница системы. Учебный пример ниже показывает порядок рассуждения. Он не является результатом production-прогона.

\n

Механизм: один aggregate скрывает несколько причин

\n

Итоговый error rate складывает ответы разных маршрутов, фаз и наборов данных. То же происходит с latency. В одном запуске p95 мог вырасти из-за медленного endpoint. В другом — из-за короткого burst, который пришёл в другой момент. В третьем — из-за retry в клиенте. Если в отчёте есть только total, эти случаи выглядят одинаково.

\n

Сначала зафиксируйте intent запроса: метод, путь, параметры и ожидаемый ответ. Затем запишите identity среды: build, конфигурацию, версию зависимостей, лимиты и внешний сервис, к которому обращается приложение. Для данных укажите версию набора и правило изменения. Наконец, разделите профиль на фазы. Warm-up проверяет договор запуска. Steady удерживает повторяемый участок. Step проверяет заранее выбранное изменение. Recovery показывает, что система или модель вернулась к меньшему плану.

\n

Эта структура нужна не для красивого отчёта. Она связывает наблюдение с действием. Если identity среды отличается, нельзя делать вывод о коде. Если пропущен step, нельзя объяснить переход. Если нет recovery, нельзя утверждать, что после пика система вернулась в исходное состояние. Если сигнал не содержит измеряемой метрики, его нельзя называть latency или error rate.

\n

Конкретный пример: порог и контекст должны жить рядом

\n

Ниже — учебный фрагмент для Grafana k6. Он показывает контракт теста: две именованные метрики, два порога и один endpoint. Адрес замените на разрешённый тестовый стенд. Числа выбраны только для объяснения синтаксиса. Они не описывают допустимые значения вашего сервиса.

\n
import http from 'k6/http';\n\nexport const options = {\n  thresholds: {\n    http_req_failed: ['rate<0.01'],\n    http_req_duration: ['p(95)<400'],\n  },\n};\n\nexport default function () {\n  const response = http.get(\n    'https://test.example.invalid/catalog/neutral-item'\n  );\n\n  if (response.status !== 200) {\n    console.log(`unexpected status: ${response.status}`);\n  }\n}
\n

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

\n

В этом примере нет собственного результата. Условный домен не предназначен для настоящего вызова, а значения 1% и 400 мс — учебные. Реальный threshold должен следовать из контракта сервиса или SLO. Отдельно проверьте, что метрика собрана именно для нужного маршрута, а не для суммы всех запросов. В k6 порог привязан к имени метрики и агрегированию; это полезная граница между критерием pass/fail и объяснением причины.

\n

Как читать сигнал

\n

Начните с environment drift. Сверьте manifest запуска с manifest ожидаемого стенда. Важны не только слова stage и preprod. Проверьте commit, переменные окружения, лимиты контейнера, размер пула, версии базы и поведение внешних зависимостей. Если один из этих элементов изменился, сохраните различие и остановите вывод о приложении. Это отрицательный путь: иногда результат нельзя классифицировать, и честное «недостаточно evidence» лучше неподтверждённого bottleneck.

\n

Затем ищите scenario gap. Сравните endpoint, порядок фаз, данные, интенсивность и время каждой фазы. Общее число запросов не заменяет arrival pattern. Пять тысяч запросов за короткий burst и пять тысяч равномерных запросов проверяют разные очереди. Данные из пустого кэша и данные из прогретого кэша тоже создают разные условия. Если запуск не записывает эти параметры, сначала восстановите карточку workload.

\n

Третья ветка — граница системы. Для неё нужны независимые признаки: распределение задержки по маршруту, доля ответов по статусу, загрузка CPU и памяти, очередь, pool wait, обращения к базе и состояние зависимости. Один график не доказывает причину. Даже совпадение по времени — только гипотеза. Проверка должна изменить одну переменную или добавить один сигнал, который способен подтвердить или опровергнуть ветку.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Ошибка выросла после запускаДругая среда или зависимостьСверить build, config, лимиты и версии по manifestНе менять код; повторить сравнение в одной identity среды
p95 хуже, но total похожИзменился профиль или маршрутРазложить результат по endpoint, фазе, статусу и даннымВосстановить workload и повторить одну фазу
Отказы начинаются только на stepОчередь, pool или лимит зависимостиСопоставить время отказов с wait, saturation и статусамиПроверить одну границу отдельным прогоном
После нагрузки сигнал не исчезаетНакопившееся состояние или неполный recoveryПроверить очистку, backlog, кэш и фазу recoveryОстановить расширение нагрузки и восстановить состояние
Есть только один aggregateНедостаточный evidenceПроверить наличие intent, среды, данных, фаз и stop criterionОставить verdict неизвестным и собрать недостающие поля
\n
\"Диагностическое
Схема показывает порядок чтения сигнала. Учебная ветка не объявляет production-причину: её нужно подтвердить отдельным измерением.
\n

Порядок расследования

\n
  1. Сохраните raw output, время запуска, commit, конфигурацию и версию инструмента. Не перезаписывайте первый результат.
  2. Назовите сигнал точно: error rate, статус ответа, p95, pool wait или другой измеряемый показатель. Не подменяйте его общим словом «тормозит».
  3. Сверьте identity среды и внешних зависимостей. При расхождении остановите сравнение и зафиксируйте drift.
  4. Разложите workload по маршрутам, фазам, данным и интенсивности. Проверьте, что запуск действительно повторяет заявленный путь пользователя.
  5. Сформулируйте одну гипотезу о границе. Укажите, какой независимый сигнал должен измениться, если гипотеза верна.
  6. Проведите один обратимый эксперимент. Измените одну переменную и сохраните прежний пакет evidence для сравнения.
  7. Сделайте вывод только по заранее объявленному критерию. Если evidence не хватает, оставьте результат неопределённым и запишите следующий измеримый шаг.
\n

Этот порядок защищает от отрицательного пути. Если среда не совпала, не стоит чинить SQL. Если сценарий неполон, не стоит увеличивать нагрузку. Если latency в запуске не собиралась, нельзя восстановить её из количества rejected units. Если причина не подтверждена, не следует превращать гипотезу в рекомендацию по архитектуре.

\n

Ограничения

\n

Учебный код не моделирует ваш сервер, сеть, DNS, TLS, браузер, базу, кэш, планировщик, очередь, пользователей или capacity. Учебные числа не являются benchmark. Порог из примера не переносите в production без владельца SLO. Документация k6 объясняет синтаксис thresholds, но не знает контракта вашего endpoint. OpenTelemetry описывает инструменты и модель метрик, но сама спецификация не создаёт наблюдение там, где приложение его не экспортирует.

\n

Нагрузочный тест также не заменяет функциональную проверку. Успешный статус может скрывать неверное тело ответа. Низкая задержка может быть следствием кэша, которого нет у пользователя. Стабильный p95 не доказывает отсутствие редких отказов. Поэтому критерий должен включать нужные assertions, окно наблюдения, допустимый error rate и условия восстановления. Их значения задаёт владелец сервиса, а не пример из этой статьи.

\n

Проверяемый критерий готовности

\n

Расследование готово, когда другой инженер может открыть один пакет и ответить на пять вопросов: какой endpoint проверяли, в какой среде, с какими данными, каким профилем и по какому критерию приняли результат. В пакете есть raw output, версия инструмента, конфигурация, разрез по фазам и статусам, выбранная гипотеза, проверяющий сигнал и результат одного эксперимента. Для подтверждённой причины действие связано с этим сигналом. Для неподтверждённой причины verdict остаётся «недостаточно evidence».

\n

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

\n

Проверяемые источники

\n" }