8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 220,
|
||
"slug": "editorial-2021-11-field-load-testing",
|
||
"title": "Нагрузочный тест: как отличить дрейф среды от узкого места",
|
||
"excerpt": "Рост ошибок или задержка после нагрузочного теста ещё не доказывают узкое место. Разбираем контекст запуска, профиль нагрузки и границы измерения, а затем выбираем одно проверяемое действие.",
|
||
"contentHtml": "<p>Через несколько минут после нагрузочного теста команда видит знакомую картину: доля ошибок выросла, p95 стал хуже, а график нагрузки похож на вчерашний. Кто-то сразу увеличивает пул соединений. Кто-то меняет таймаут. Кто-то запускает тест ещё раз с большим числом виртуальных пользователей. Через час система получила несколько изменений, но причина сигнала осталась неизвестной.</p>\n<p>Цена ошибки — не только лишние минуты на расследование. Неверный лимит может скрыть отказ зависимости. Повторный прогон в другой среде создаёт ложное сравнение. Исправление кода по сводному показателю без сценария может ухудшить обычный путь пользователя. В итоге команда получает красивое число без доказательства того, что именно оно измерило.</p>\n<p>Нагрузочный сигнал становится основанием для изменения только тогда, когда его можно связать с конкретным endpoint, профилем нагрузки, средой, данными и критерием завершения. До этого signal — повод для проверки. Диагностика должна разделить три ветки: изменилась среда, неполон сценарий или проявилась граница системы. Учебный пример ниже показывает порядок рассуждения. Он не является результатом production-прогона.</p>\n<h2>Механизм: сводный показатель скрывает несколько причин</h2>\n<p>Итоговый error rate объединяет ответы разных маршрутов, фаз и наборов данных. То же происходит с latency. В одном запуске p95 мог вырасти из-за медленного endpoint. В другом — из-за короткого burst, который пришёл в другой момент. В третьем — из-за повторной попытки в клиенте. Если в отчёте есть только total, эти случаи выглядят одинаково.</p>\n<p>Сначала зафиксируйте intent запроса: метод, путь, параметры и ожидаемый ответ. Затем запишите identity среды: commit, конфигурацию, версию зависимостей, лимиты и внешний сервис, к которому обращается приложение. Для данных укажите версию набора и правило его изменения. Наконец, разделите профиль на фазы. Warm-up проверяет договор запуска. Steady удерживает повторяемый участок. Step проверяет заранее выбранное изменение. Recovery показывает, что система вернулась к меньшему плану и не оставила незамеченную очередь.</p>\n<p>Эта структура связывает наблюдение с действием. Если identity среды отличается, нельзя делать вывод о коде. Если пропущен step, нельзя объяснить переход. Если нет recovery, нельзя утверждать, что после пика система вернулась в исходное состояние. Если сигнал не содержит измеряемой метрики, его нельзя называть latency или error rate.</p>\n<h2>Конкретный пример: порог и контекст должны жить рядом</h2>\n<p>Ниже — учебный фрагмент для Grafana k6. Он показывает контракт теста: два порога для HTTP-метрик и один endpoint, заданный через переменную окружения. Адрес должен указывать на разрешённый тестовый стенд. Числа выбраны только для объяснения синтаксиса и не описывают допустимые значения вашего сервиса.</p>\n<pre><code>import http from 'k6/http';\n\nconst targetUrl = __ENV.TARGET_URL;\n\nexport const options = {\n thresholds: {\n http_req_failed: ['rate<0.01'],\n http_req_duration: ['p(95)<400'],\n },\n};\n\nexport function setup() {\n if (!targetUrl) {\n throw new Error('TARGET_URL is required');\n }\n}\n\nexport default function () {\n http.get(targetUrl);\n}</code></pre>\n<p>Перед запуском сохраните файл, например, как <code>load-test.js</code>, и выполните <code>TARGET_URL=https://test.example.invalid/catalog/neutral-item k6 run load-test.js</code> только после замены адреса на разрешённый стенд. Здесь домен оставлен условным: пример нельзя случайно принять за готовую команду для чужого сервиса.</p>\n<p>Порог отвечает на вопрос «прошёл ли тест по заданному правилу». Он не отвечает на вопрос «почему правило нарушено». В примере <code>http_req_failed</code> использует стандартную классификацию k6: по умолчанию ожидаемыми считаются статусы 200–399. Если контракт endpoint требует ровно 200, добавьте перед экспортом сценария <code>http.setResponseCallback(http.expectedStatuses(200));</code>. Тогда неожидаемый HTTP-статус попадёт в этот показатель. Не смешивайте это решение с молчаливой проверкой в логе: запись сообщения не меняет pass/fail.</p>\n<p>В таком фрагменте лучше явно выбрать контракт. Вариант с callback проверяет только статус, а проверку тела ответа можно добавить отдельно через <code>check()</code> и собственный порог <code>checks</code>. Это разные вопросы: HTTP-ошибка, ожидаемый статус и корректность содержимого не являются одной метрикой.</p>\n<p>Порог для <code>http_req_duration</code> тоже требует аккуратной трактовки. В k6 это сумма времени отправки, ожидания ответа и получения тела; начальные DNS, установка соединения и TLS handshake учитываются отдельными метриками. Поэтому p95 в примере не равен полной задержке браузерного пути. Если проблема похожа на DNS, connection pool или TLS, ищите соответствующий разрез, а не меняйте порог duration.</p>\n<h2>Как читать сигнал</h2>\n<p>Начните с дрейфа среды. Сверьте manifest запуска с manifest ожидаемого стенда. Важны не только слова stage и preprod. Проверьте commit, переменные окружения, лимиты контейнера, размер пула, версии базы и поведение внешних зависимостей. Если один из этих элементов изменился, сохраните различие и остановите вывод о приложении. Это отрицательный путь: иногда результат нельзя классифицировать, и честное «недостаточно evidence» лучше неподтверждённого bottleneck.</p>\n<p>Затем ищите разрыв сценария. Сравните 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>Сверить commit, 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>Есть только один total</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://grafana.com/docs/k6/latest/using-k6/metrics/reference/\" target=\"_blank\" rel=\"noopener noreferrer\">Grafana k6: Built-in metrics</a> — состав <code>http_req_duration</code>, <code>http_req_failed</code> и отдельных сетевых метрик.</li><li><a href=\"https://grafana.com/docs/k6/latest/javascript-api/k6-http/set-response-callback/\" target=\"_blank\" rel=\"noopener noreferrer\">Grafana k6: setResponseCallback</a> — классификация ожидаемых HTTP-статусов через <code>expectedStatuses</code>.</li><li><a href=\"https://opentelemetry.io/docs/specs/otel/metrics/\" target=\"_blank\" rel=\"noopener noreferrer\">OpenTelemetry: Metrics</a> — модель метрик, разделение API/SDK и роль агрегации.</li></ul>"
|
||
}
|