From 18c59b3b219fdc5bfa23381fecefff555e974130 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 20:32:06 +0300 Subject: [PATCH] =?UTF-8?q?editorial:=20=D1=83=D1=82=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BC=D0=B5=D1=85=D0=B0=D0=BD=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=20=D0=BD=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/221.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/editorial/agent-rewrites/221.json b/editorial/agent-rewrites/221.json index f60598d..c63372c 100644 --- a/editorial/agent-rewrites/221.json +++ b/editorial/agent-rewrites/221.json @@ -2,6 +2,6 @@ "index": 221, "slug": "editorial-2021-11-mechanism-load-testing", "title": "Почему «много запросов» не является нагрузочной моделью", - "excerpt": "Нагрузка помогает найти предел системы только тогда, когда известны сценарий, среда, данные и критерий остановки. Разбираем, как отделить план входа от выполненной работы, проверить отрицательный путь и не выдать учебную модель за benchmark.", - "contentHtml": "

В отчёте после теста появляется одна цифра: «мы отправили 100 000 запросов». Через час команда видит рост времени ответа и начинает менять таймауты, пул соединений или SQL. Но из этой цифры не видно, какой endpoint вызывали, когда возникали запросы, какие данные читали, что считали завершённой работой и в какой среде шёл запуск. Ошибка стоит дорого: можно потратить релиз на исправление несуществующего узкого места и при этом оставить настоящий предел без проверки.

\n

Тезис простой: нагрузочная модель описывает не объём запросов, а договор между входом, системой и наблюдением. Минимальный договор содержит endpoint, данные, среду, порядок сегментов, критерий остановки и пакет свидетельств. Без него aggregate показывает только объём, но не объясняет причину и не подсказывает безопасное действие.

\n

Что именно нужно разделить

\n

Сначала отделите намерение создать вход от факта выполненной работы. В плане есть запланированный слот: например, одна операция чтения каталога в сегменте steady. В реальном запуске слот может стать HTTP-запросом, ошибкой клиента, таймаутом или вообще не стартовать. Эти события нельзя складывать в одну величину и называть её throughput.

\n

Вторая граница проходит между учебной моделью и настоящим тестом. В учебном примере можно материализовать слоты в принятые и отклонённые units, чтобы проверить порядок и остановку. Такой код не создаёт часы, сеть, сервер, очередь, базу или telemetry. Он проверяет форму рассуждения. Чтобы говорить о latency, error rate или capacity, нужен отдельный запуск с реальным источником времени, конфигурацией инструмента, данными и raw output.

\n

Третья граница — между профилем и результатом. Профиль задаёт переход warm-up → steady → step → recovery. Результат показывает, что произошло на каждом переходе. Один total скрывает порядок. Два запуска с одинаковым total могут иметь разные входы и разные причины ухудшения.

\n

Минимальная модель workload

\n

Запишите пять полей до запуска. endpointIntent ограничивает предмет и метод. dataSetup описывает набор данных и его identity. environment фиксирует build, конфигурацию и внешние зависимости. segments задают порядок и объём входа. stopCriterion объясняет, когда эксперимент заканчивается и какой результат считается достаточным.

\n

К этим полям добавьте evidencePacket. Он связывает план с наблюдением: хранит identity среды и данных, имена сегментов, число запланированных слотов, принятые и отклонённые units, состояние остановки и диагностические пометки. Пакет не заменяет результат инструмента. Он не делает fixture trace и не создаёт production evidence. Он не даёт потерять условия, пока команда читает результат.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Есть только «много запросов» и один totalAggregate выдали за workloadНазвать endpoint, данные, сегменты и критерий остановкиОтклонить сравнение и восстановить карточку сценария
На графике выросло время ответаНеизвестно, измерялся ли тот же endpoint в той же средеСверить environment identity, build, конфигурацию и raw signalПовторить один сценарий после фиксации среды
Часть входа исчезла из результатаНе сведены planned slots и completed unitsПроверить равенство planned = accepted + rejectedИсправить reconciliation до анализа причин
Fixture показывает bottleneckУчебный limit приняли за узкое место сервисаПроверить наличие часов, сервера, сети и источника метрикиНазвать результат synthetic boundary и подготовить реальный tool-run
Threshold не пройденКритерий не связан с конкретной метрикой и цельюПроверить имя метрики, единицу, окно и условие abortОставить один измеримый критерий и повторить тест
\n

Пример: отрицательный путь должен быть виден

\n

Ниже учебная модель для одного нейтрального endpoint. Она намеренно не выполняет HTTP-запрос. Сегмент step планирует пять слотов, но принимает только три по заранее объявленному synthetic limit. Два слота остаются отклонёнными. Это позволяет проверить отрицательный путь: нагрузка не исчезла между планом и packet, а recovery идёт после границы.

\n
const profile = {\n  endpointIntent: { method: 'GET', path: '/catalog' },\n  environment: 'isolated-training-envelope-v1',\n  segments: [\n    { name: 'warm-up', planned: 2, accepted: 2, rejected: 0 },\n    { name: 'steady', planned: 4, accepted: 4, rejected: 0 },\n    { name: 'step', planned: 5, accepted: 3, rejected: 2,\n      syntheticLimit: 3 },\n    { name: 'recovery', planned: 2, accepted: 2, rejected: 0 }\n  ],\n  stopCriterion: 'packet-reconciled-after-recovery'\n};\n\nfor (const segment of profile.segments) {\n  if (segment.planned !== segment.accepted + segment.rejected) {\n    throw new Error(`Unreconciled segment: ${segment.name}`);\n  }\n}\n\nconst bareCount = { planned: 100000 };\nconst acceptedAsLoadModel =\n  'endpointIntent' in bareCount && 'stopCriterion' in bareCount;\nconsole.assert(acceptedAsLoadModel === false);
\n

Assertion в конце не измеряет производительность. Она защищает термин. Объект с одним счётчиком не получает статус нагрузочной модели. В коде нет fetch, часов, процесса и внешнего состояния. Поэтому его результат нельзя читать как latency, throughput, error rate или capacity. Если добавить реальный вызов, это станет уже другой проверкой с другими условиями.

\n
\"Схема
Схема показывает границу между планом входа и evidence packet. Красная ветка с одним aggregate отклоняется. Synthetic units не являются latency или throughput.
\n

Как читать arrival

\n

Arrival — это правило появления следующего входа. Оно не равно completed work. При модели с фиксированным числом виртуальных пользователей следующая итерация может зависеть от завершения предыдущей. При модели с открытым потоком инструмент старается поддержать заданный arrival rate и отдельно показывает, успевает ли система обрабатывать вход. Значение зависит от конкретного executor, версии и конфигурации. Переносить смысл одного параметра между инструментами нельзя.

\n

Поэтому сначала назовите вопрос. Проверяете поведение endpoint при росте входа? Проверяете сохранение времени ответа при фиксированном потоке? Проверяете прохождение порога ошибок? Для каждого вопроса нужны свои единицы и свой stop criterion. Слово «нагрузка» без этого выбора слишком широко.

\n

Почему observability начинается с имён

\n

Хорошая метрика имеет смысл до того, как попадёт на dashboard. Назовите endpoint, service, environment, segment и единицу. Не называйте synthetic rejection ошибкой HTTP. Не называйте число слотов RPS, если у него нет времени. Не смешивайте клиентский timeout с ответом сервера. Такие запреты короче, чем последующее расследование неверного графика.

\n

Смысл полей должен сохраняться при агрегации. Если два результата нельзя сопоставить по endpoint, данным, среде и профилю, их нельзя честно сравнить по одному p95 или total. Пакет свидетельств нужен именно для этого: он показывает, какие условия совпали, а какие изменились.

\n

Порядок действий

\n
  1. Сформулируйте вопрос теста одним предложением и назовите endpoint, метод и ожидаемый результат.
  2. Зафиксируйте data setup: identity набора, объём, состояние и способ восстановления.
  3. Опишите environment envelope: build, конфигурацию, зависимости, лимиты и место запуска.
  4. Разложите вход на именованные сегменты warm-up, steady, step и recovery либо объясните другой порядок.
  5. Для каждого сегмента укажите единицу входа и правило arrival. Отдельно запишите, что считается completed work.
  6. Задайте один stop criterion, связанный с конкретной метрикой или с явной проверкой полноты packet.
  7. Запустите инструмент отдельно от учебной модели и сохраните версию, конфигурацию и raw output.
  8. Сведите planned slots с accepted и rejected units. При несовпадении остановите анализ и исправьте данные.
  9. Сравните результат только с запуском, у которого совпадают endpoint, данные, среда и профиль. После изменения условий дайте запуску новую identity.
\n

Ограничения

\n

Эта статья не сообщает, какую нагрузку выдержит конкретный сервис. Учебный пример не создаёт пользователей, соединения, DNS, TLS, сеть, очередь, CPU, память, базу, cache, browser или production incident. Synthetic limit показывает только заранее заданную границу модели. Rejected unit не означает HTTP error. Recovery в списке не доказывает восстановление реальной системы.

\n

Официальная документация инструмента всё равно нужна перед запуском. Сценарии k6 описывают разные профили workload, а thresholds задают pass/fail criteria для конкретных метрик. OpenTelemetry задаёт общие имена и смысл атрибутов, но не превращает любую локальную цифру в корректную телеметрию. Эти источники помогают выбрать термин и критерий. Они не заменяют проверку вашей среды и данных.

\n

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

\n

Материал и реальный запуск готовы к анализу, если независимый инженер может по packet ответить на шесть вопросов: какой endpoint проверяли; какие данные использовали; в какой среде; как появлялся вход; что считалось выполненной работой; почему тест остановился. Для каждого сегмента сходятся planned = accepted + rejected. Для каждой заявленной метрики указаны источник, единица и окно. Если хотя бы один ответ отсутствует, результат — не verdict о системе, а незавершённый эксперимент.

\n

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

\n" + "excerpt": "Разбор механизма workload: arrival intent, профиль сегментов, evidence packet и synthetic bottleneck. Почему aggregate без среды и stop criterion не объясняет ни причину, ни следующее действие.", + "contentHtml": "

Проблема фразы «мы дали много запросов» в том, что она описывает объём шума, но не модель нагрузки. В ней не видно, когда возникал input, какой endpoint он представлял, какой набор данных использовался, что считалось завершением и кто наблюдал последствия. Цена — ложная причинность: aggregate меняется, а команда приписывает его очереди, базе или коду, хотя могла измениться сама среда или сценарий.

\n

Для механизма достаточно одной строгой границы. В этой статье arrival означает намерение поставить planned request slot в сегмент профиля. Это не отправка запроса и не скорость. Fixture материализует slots в accepted/rejected synthetic units внутри local objects. Она не создаёт clock, HTTP, сеть, процесс, нагрузочный инструмент или telemetry. Поэтому слова latency-signal и error-signal ниже — диагностические ярлыки, а не измеренные метрики.

\n

Модель начинается с формы входа

\n

Полезная запись выглядит так: endpoint intent + data setup + environment constraint + ordered segments + stop criterion + evidence packet. Каждый элемент отвечает на отдельный вопрос. Endpoint ограничивает предмет. Data setup не даёт одному и тому же имени скрывать разные записи. Environment объясняет, при каком договоре существует наблюдение. Segments показывают переход. Criterion определяет, когда доказательство достаточно. Packet держит все поля рядом, чтобы вывод не зависел от памяти автора.

\n

Если убрать любой элемент, получаем другой тип неопределённости. Без endpoint нельзя отличить чтение от изменения. Без данных нельзя повторить branch. Без среды невозможно увидеть drift. Без сегментов total не показывает порядок. Без criterion нельзя понять, почему модель закончилась именно здесь. Без packet остаётся пересказ, который невозможно проверить. Это не бюрократия вокруг теста: это минимальная структура причинной связи.

\n
Профиль и плохой aggregate отвечают на разные вопросы
СвойствоПрофиль fixture«Много запросов без сценария»Инженерское последствие
Endpoint intentодин нейтральный путь и методне указаннельзя проверить контракт входа
Arrival intentslots принадлежат named segmentесть только totalнельзя увидеть переходы
Средаidentity и synthetic constraint записаныне указаналюбой drift маскируется под результат
Accepted / rejectedразложены по segmentне определенынеясно, что именно не прошло
Stop criterionevidence после recoveryнетконец наблюдения произволен
Interpretationтолько training modelобычно звучит как verdictриск выдать счётчик за throughput
\n

Arrival не равен completed work

\n

В реальном инструменте способ моделировать arrival зависит от executor, версии и конфигурации. Нельзя переносить значение одного параметра между tool без чтения его документации. В k6 v0.35.0 release notes отдельно связывают stage tags с конкретными executors; это исторический факт о версии, а не лицензия назвать любой массив slots её сценарием. Наша fixture специально не повторяет API инструмента. Она показывает только вопрос, который нужно сформулировать до выбора API: что означает появление следующего planned slot и как эта попытка будет отделена от результата обработки.

\n

Разделение полезно и для закрытой модели пользователей, и для открытой модели arrival. В первом случае нужно назвать, кто ждёт завершения предыдущей итерации. Во втором — как tool ведёт себя при невозможности начать следующую работу. Но оба случая остаются неполными без data setup и environment. Число на оси не заменяет эту информацию. Поэтому article не предлагает универсальный executor и не выдаёт synthetic acceptance limit за реальную настройку генератора.

\n

Профиль хранит намерение и результат рядом

\n

У каждого segment fixture есть именованные slots, accepted units, rejected units, synthetic bottleneck flag и boundary text. Наличие flag не превращает rejection в ошибку приложения. Это самопроверка модели: step был объявлен как учебная граница до materialization, а не задним числом назван узким местом после просмотра результата. Такая разница особенно важна в текстах про производительность, где красивый график часто даёт больше уверенности, чем его исходные условия.

\n

Проверка reconciliation предельно простая: длина plannedRequestSlots должна равняться acceptedUnits + rejectedUnits. Она не измеряет полезную работу и не заменяет server-side evidence. Зато она ловит редакционную ошибку: если часть plan исчезла между профилем и таблицей, автор больше не может честно сказать, что сравнил одно и то же. Именно такие мелкие несовпадения потом превращают нагрузочную заметку в набор несвязанных сигналов.

\n

Observability начинается с семантики данных

\n

Наблюдаемость здесь не означает автоматически подключённый dashboard. Сначала нужны хорошо названные поля: endpoint intent, environment identity, data identity, segment name, planned slots, accepted/rejected units и stop state. Затем выбирается настоящий инструмент, который умеет сохранить требуемый raw signal и его контекст. OpenTelemetry Metrics API в historical tag v1.0.0 подчёркивает, что instrument задаёт смысл measurement, а не только форму числа. При этом документ помечен experimental; он не может быть основанием приписать старой системе готовую телеметрию.

\n

Evidence packet полезен потому, что связывает две шкалы: plan и observation. План отвечает, что хотели проверить. Observation отвечает, какие учебные units были materialized. Если packet не содержит environment или criterion, визуализация всё равно может существовать, но reader уже не знает, какую именно гипотезу она проверяет. Поэтому packet не экспортируется как trace и не изображает работу настоящего мониторинга: это object для детерминированной проверки редакционной модели.

\n
\"Схема
Слева остаётся замысел workload, справа — packet для проверки. Между ними нет HTTP-клиента, генератора, сервера или telemetry pipeline.
\n

Synthetic bottleneck нужен для отрицательной проверки

\n

Step в fixture получил пять slots при явном limit в три accepted synthetic units. Модель оставляет два rejected units и поднимает единственный syntheticBottleneckFlag. Это намеренно скучный результат. Его задача — проверить четыре свойства: rejected units видны, flag находится в правильном segment, recovery идёт после step, stop criterion не срабатывает раньше. Если бы модель всегда принимала всё, она не проверяла бы путь, в котором author обязан объяснить границу.

\n

Неправильный вывод звучал бы так: «мы нашли bottleneck и latency выросла». У fixture нет времени, сервера или наблюдаемой очереди, поэтому такой вывод нельзя получить. Правильный вывод уже: «план содержит заранее отмеченную synthetic boundary; для реального расследования нужны environment manifest, raw output выбранного tool и отдельный источник latency-signal». Скромная формулировка оставляет место для следующего эксперимента, а не подменяет его.

\n

Минимальный пример: проверить packet и отклонить bare count

\n

Этот пример читает mechanism, а не запускает testing software. Он выводит путь как intent, явное ограничение среды и stop object. Последняя assertion возвращает false для aggregate, где есть только synthetic total. Так мы проверяем не способность «создать много», а способность не называть нагрузочной моделью то, что не содержит сценария.

\n
const fixture = runLoadTestingFixture();\nconst packet = fixture.evidencePacket;\n\nconsole.log(packet.endpoint.path);\n// /fixture/neutral-resource — intent only; no HTTP call exists in the fixture\nconsole.log(packet.environment.explicitConstraint);\n// at most three accepted synthetic units in one planned segment\nconsole.log(packet.stop);\n// stopped-by-criterion after recovery evidence is present\n\nif (!fixture.assertions.badAggregateRejected) {\n  throw new Error("a count without scenario was accepted as a model");\n}
\n

В коде нет fetch, http, setTimeout, файла или child process. Это не ограничение языка и не рекомендация для production. Это защита смысла fixture: добавление реального вызова сделало бы её зависимой от внешнего состояния и позволило бы принять случайный ответ за доказательство. Реальный инструмент нужно запускать отдельной операцией с отдельным пакетом условий, а не прятать в редакционный self-check.

\n

Маршрут: симптом → причина → проверка → действие

\n
  1. Симптом. В отчёте есть aggregate или один график, но невозможно назвать endpoint, среду, data setup и момент остановки.
  2. Причина. Total принят за workload model; intention, completed work и наблюдение смешаны в одном числе.
  3. Проверка. Разделите карточку на endpoint, данные, environment, named segments, accepted/rejected units и criterion. Проверьте порядок warm-up → steady → step → recovery.
  4. Проверка смысла. Для каждого поля назовите единицу и запрет. Если unit не имеет времени, не называйте его latency, throughput или RPS.
  5. Действие. Отклоните bare count, пока в нём нет scenario и evidence packet. Затем выберите historical version конкретного tool и сверяйте его semantics с его документацией.
  6. Ожидаемый результат. Следующая диаграмма объясняет не только, что было нарисовано, но и какой вопрос она вправе помогать расследовать.
\n

Ограничения и следующий проверяемый шаг

\n

Fixture не сообщает, как ведут себя executors k6, как рассчитываются thresholds, как именно агрегирует метрики OpenTelemetry или какие свойства имеет конкретный server. Ссылки на k6 v0.35.0 и его samples/thresholds.js зафиксированы, чтобы не ссылаться на mutable current documentation. Они нужны только для historical boundary и для требования называть metric вместе с порогом. Пакет не исполняет k6, не создаёт VU и не использует его API.

\n

Не моделируются HTTP, сеть, TLS, DNS, scheduler, queue, CPU, память процесса, база, cache, browser, пользователь, latency, throughput, error rate, capacity, production incident или результат benchmark. RFC 2330 добавлен как внешняя рамка аккуратного определения метрик, но он относится к IP performance metrics и не определяет готовность application endpoint. Следующий шаг — описать один реальный tool-run отдельно, не смешивая его raw output с объектами этой учебной модели.

\n

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

\n" }