Files

8 lines
20 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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&lt;0.01'],\n http_req_duration: ['p(95)&lt;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>"
}