From d227071e33dc0d012d83af71ef19144e7296493a Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 20:21:44 +0300 Subject: [PATCH] editorial: refine load-testing article 220 --- editorial/agent-rewrites/220.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editorial/agent-rewrites/220.json b/editorial/agent-rewrites/220.json index 5ad9abf..4f379fe 100644 --- a/editorial/agent-rewrites/220.json +++ b/editorial/agent-rewrites/220.json @@ -1,7 +1,7 @@ { "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" + "title": "Нагрузочный тест: как отличить дрейф среды от узкого места", + "excerpt": "Рост ошибок или задержка после нагрузочного теста ещё не доказывают узкое место. Разбираем контекст запуска, профиль нагрузки и границы измерения, а затем выбираем одно проверяемое действие.", + "contentHtml": "

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

\n

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

\n

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

\n

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

\n

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

\n

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

\n

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

\n

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

\n

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

\n
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}
\n

Перед запуском сохраните файл, например, как load-test.js, и выполните TARGET_URL=https://test.example.invalid/catalog/neutral-item k6 run load-test.js только после замены адреса на разрешённый стенд. Здесь домен оставлен условным: пример нельзя случайно принять за готовую команду для чужого сервиса.

\n

Порог отвечает на вопрос «прошёл ли тест по заданному правилу». Он не отвечает на вопрос «почему правило нарушено». В примере http_req_failed использует стандартную классификацию k6: по умолчанию ожидаемыми считаются статусы 200–399. Если контракт endpoint требует ровно 200, добавьте перед экспортом сценария http.setResponseCallback(http.expectedStatuses(200));. Тогда неожидаемый HTTP-статус попадёт в этот показатель. Не смешивайте это решение с молчаливой проверкой в логе: запись сообщения не меняет pass/fail.

\n

В таком фрагменте лучше явно выбрать контракт. Вариант с callback проверяет только статус, а проверку тела ответа можно добавить отдельно через check() и собственный порог checks. Это разные вопросы: HTTP-ошибка, ожидаемый статус и корректность содержимого не являются одной метрикой.

\n

Порог для http_req_duration тоже требует аккуратной трактовки. В k6 это сумма времени отправки, ожидания ответа и получения тела; начальные DNS, установка соединения и TLS handshake учитываются отдельными метриками. Поэтому p95 в примере не равен полной задержке браузерного пути. Если проблема похожа на DNS, connection pool или TLS, ищите соответствующий разрез, а не меняйте порог duration.

\n

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

\n

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

\n

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

\n

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

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Ошибка выросла после запускаДругая среда или зависимостьСверить commit, config, лимиты и версии по manifestНе менять код; повторить сравнение в одной identity среды
p95 хуже, но total похожИзменился профиль или маршрутРазложить результат по endpoint, фазе, статусу и даннымВосстановить workload и повторить одну фазу
Отказы начинаются только на stepОчередь, pool или лимит зависимостиСопоставить время отказов с wait, saturation и статусамиПроверить одну границу отдельным прогоном
После нагрузки сигнал не исчезаетНакопившееся состояние или неполный recoveryПроверить очистку, backlog, кэш и фазу recoveryОстановить расширение нагрузки и восстановить состояние
Есть только один totalНедостаточно 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" }