8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 220,
|
||
"slug": "editorial-2021-11-field-load-testing",
|
||
"title": "Нагрузочный сигнал: как отличить проблему среды от узкого места",
|
||
"excerpt": "Рост ошибок или задержки после нагрузочного теста ещё не указывает на bottleneck. Разбираем evidence, workload и границы измерения, а затем выбираем одно проверяемое действие.",
|
||
"contentHtml": "<p>После нагрузочного теста команда видит знакомую картину: доля ошибок выросла, p95 стал хуже, а график нагрузки похож на вчерашний. Кто-то сразу увеличивает пул соединений. Кто-то меняет таймаут. Кто-то запускает тест ещё раз с большим числом виртуальных пользователей. Через час система получила несколько изменений, но причина сигнала осталась неизвестной.</p>\n<p>Цена ошибки — не только лишние минуты на расследование. Неверный лимит может скрыть отказ зависимости. Повторный прогон в другой среде создаёт ложное сравнение. Исправление кода по агрегату без сценария может ухудшить обычный путь пользователя. В итоге команда получает красивое число без доказательства того, что именно оно измерило.</p>\n<p>Тезис простой: нагрузочный сигнал становится основанием для изменения только тогда, когда его можно связать с конкретным endpoint, workload, средой, данными и критерием завершения. До этого signal — повод для проверки. Диагностика должна разделить три ветки: изменилась среда, неполон сценарий или проявилась граница системы. Учебный пример ниже показывает порядок рассуждения. Он не является результатом production-прогона.</p>\n<h2>Механизм: один aggregate скрывает несколько причин</h2>\n<p>Итоговый error rate складывает ответы разных маршрутов, фаз и наборов данных. То же происходит с latency. В одном запуске p95 мог вырасти из-за медленного endpoint. В другом — из-за короткого burst, который пришёл в другой момент. В третьем — из-за retry в клиенте. Если в отчёте есть только total, эти случаи выглядят одинаково.</p>\n<p>Сначала зафиксируйте intent запроса: метод, путь, параметры и ожидаемый ответ. Затем запишите identity среды: build, конфигурацию, версию зависимостей, лимиты и внешний сервис, к которому обращается приложение. Для данных укажите версию набора и правило изменения. Наконец, разделите профиль на фазы. Warm-up проверяет договор запуска. Steady удерживает повторяемый участок. Step проверяет заранее выбранное изменение. Recovery показывает, что система или модель вернулась к меньшему плану.</p>\n<p>Эта структура нужна не для красивого отчёта. Она связывает наблюдение с действием. Если identity среды отличается, нельзя делать вывод о коде. Если пропущен step, нельзя объяснить переход. Если нет recovery, нельзя утверждать, что после пика система вернулась в исходное состояние. Если сигнал не содержит измеряемой метрики, его нельзя называть latency или error rate.</p>\n<h2>Конкретный пример: порог и контекст должны жить рядом</h2>\n<p>Ниже — учебный фрагмент для Grafana k6. Он показывает контракт теста: две именованные метрики, два порога и один endpoint. Адрес замените на разрешённый тестовый стенд. Числа выбраны только для объяснения синтаксиса. Они не описывают допустимые значения вашего сервиса.</p>\n<pre><code>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}</code></pre>\n<p>Порог отвечает на вопрос «прошёл ли тест по заданному правилу». Он не отвечает на вопрос «почему правило нарушено». Если threshold упал, сохраните сырые результаты и контекст запуска. Не меняйте одновременно маршрут, число виртуальных пользователей, таймаут и конфигурацию базы. Иначе следующий прогон уже не проверит исходную гипотезу.</p>\n<p>В этом примере нет собственного результата. Условный домен не предназначен для настоящего вызова, а значения 1% и 400 мс — учебные. Реальный threshold должен следовать из контракта сервиса или SLO. Отдельно проверьте, что метрика собрана именно для нужного маршрута, а не для суммы всех запросов. В k6 порог привязан к имени метрики и агрегированию; это полезная граница между критерием pass/fail и объяснением причины.</p>\n<h2>Как читать сигнал</h2>\n<p>Начните с environment drift. Сверьте manifest запуска с manifest ожидаемого стенда. Важны не только слова stage и preprod. Проверьте commit, переменные окружения, лимиты контейнера, размер пула, версии базы и поведение внешних зависимостей. Если один из этих элементов изменился, сохраните различие и остановите вывод о приложении. Это отрицательный путь: иногда результат нельзя классифицировать, и честное «недостаточно evidence» лучше неподтверждённого bottleneck.</p>\n<p>Затем ищите scenario gap. Сравните endpoint, порядок фаз, данные, интенсивность и время каждой фазы. Общее число запросов не заменяет arrival pattern. Пять тысяч запросов за короткий burst и пять тысяч равномерных запросов проверяют разные очереди. Данные из пустого кэша и данные из прогретого кэша тоже создают разные условия. Если запуск не записывает эти параметры, сначала восстановите карточку workload.</p>\n<p>Третья ветка — граница системы. Для неё нужны независимые признаки: распределение задержки по маршруту, доля ответов по статусу, загрузка CPU и памяти, очередь, pool wait, обращения к базе и состояние зависимости. Один график не доказывает причину. Даже совпадение по времени — только гипотеза. Проверка должна изменить одну переменную или добавить один сигнал, который способен подтвердить или опровергнуть ветку.</p>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Ошибка выросла после запуска</td><td>Другая среда или зависимость</td><td>Сверить build, config, лимиты и версии по manifest</td><td>Не менять код; повторить сравнение в одной identity среды</td></tr><tr><td>p95 хуже, но total похож</td><td>Изменился профиль или маршрут</td><td>Разложить результат по endpoint, фазе, статусу и данным</td><td>Восстановить workload и повторить одну фазу</td></tr><tr><td>Отказы начинаются только на step</td><td>Очередь, pool или лимит зависимости</td><td>Сопоставить время отказов с wait, saturation и статусами</td><td>Проверить одну границу отдельным прогоном</td></tr><tr><td>После нагрузки сигнал не исчезает</td><td>Накопившееся состояние или неполный recovery</td><td>Проверить очистку, backlog, кэш и фазу recovery</td><td>Остановить расширение нагрузки и восстановить состояние</td></tr><tr><td>Есть только один aggregate</td><td>Недостаточный evidence</td><td>Проверить наличие intent, среды, данных, фаз и stop criterion</td><td>Оставить verdict неизвестным и собрать недостающие поля</td></tr></tbody></table>\n<figure><img src=\"/assets/editorial/2021/load-test-diagnosis-2021.svg\" alt=\"Диагностическое дерево сигнала при нагрузке: проверка среды, сценария и границы системы перед выбором действия\" loading=\"lazy\" /><figcaption>Схема показывает порядок чтения сигнала. Учебная ветка не объявляет production-причину: её нужно подтвердить отдельным измерением.</figcaption></figure>\n<h2>Порядок расследования</h2>\n<ol><li>Сохраните raw output, время запуска, commit, конфигурацию и версию инструмента. Не перезаписывайте первый результат.</li><li>Назовите сигнал точно: error rate, статус ответа, p95, pool wait или другой измеряемый показатель. Не подменяйте его общим словом «тормозит».</li><li>Сверьте identity среды и внешних зависимостей. При расхождении остановите сравнение и зафиксируйте drift.</li><li>Разложите workload по маршрутам, фазам, данным и интенсивности. Проверьте, что запуск действительно повторяет заявленный путь пользователя.</li><li>Сформулируйте одну гипотезу о границе. Укажите, какой независимый сигнал должен измениться, если гипотеза верна.</li><li>Проведите один обратимый эксперимент. Измените одну переменную и сохраните прежний пакет evidence для сравнения.</li><li>Сделайте вывод только по заранее объявленному критерию. Если evidence не хватает, оставьте результат неопределённым и запишите следующий измеримый шаг.</li></ol>\n<p>Этот порядок защищает от отрицательного пути. Если среда не совпала, не стоит чинить SQL. Если сценарий неполон, не стоит увеличивать нагрузку. Если latency в запуске не собиралась, нельзя восстановить её из количества rejected units. Если причина не подтверждена, не следует превращать гипотезу в рекомендацию по архитектуре.</p>\n<h2>Ограничения</h2>\n<p>Учебный код не моделирует ваш сервер, сеть, DNS, TLS, браузер, базу, кэш, планировщик, очередь, пользователей или capacity. Учебные числа не являются benchmark. Порог из примера не переносите в production без владельца SLO. Документация k6 объясняет синтаксис thresholds, но не знает контракта вашего endpoint. OpenTelemetry описывает инструменты и модель метрик, но сама спецификация не создаёт наблюдение там, где приложение его не экспортирует.</p>\n<p>Нагрузочный тест также не заменяет функциональную проверку. Успешный статус может скрывать неверное тело ответа. Низкая задержка может быть следствием кэша, которого нет у пользователя. Стабильный p95 не доказывает отсутствие редких отказов. Поэтому критерий должен включать нужные assertions, окно наблюдения, допустимый error rate и условия восстановления. Их значения задаёт владелец сервиса, а не пример из этой статьи.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Расследование готово, когда другой инженер может открыть один пакет и ответить на пять вопросов: какой endpoint проверяли, в какой среде, с какими данными, каким профилем и по какому критерию приняли результат. В пакете есть raw output, версия инструмента, конфигурация, разрез по фазам и статусам, выбранная гипотеза, проверяющий сигнал и результат одного эксперимента. Для подтверждённой причины действие связано с этим сигналом. Для неподтверждённой причины verdict остаётся «недостаточно evidence».</p>\n<p>Практический финальный тест прост: удалите из отчёта график total и попробуйте восстановить решение по остальным данным. Если решение исчезло, измерение было слишком грубым. Если маршрут, среда, причина и следующий шаг остаются видимыми, нагрузочный сигнал стал рабочим evidence, а не поводом для случайной настройки.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://grafana.com/docs/k6/latest/using-k6/thresholds/\" target=\"_blank\" rel=\"noopener noreferrer\">Grafana k6: Thresholds</a> — официальное описание pass/fail-критериев, метрик и выражений порога.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/metrics/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Metrics</a> — официальная спецификация модели метрик, instruments, aggregation и связи сигналов.</li></ul>"
|
||
}
|