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Сначала отделите намерение создать вход от факта выполненной работы. В плане есть запланированный слот: например, одна операция чтения каталога в сегменте steady. В реальном запуске слот может стать HTTP-запросом, ошибкой клиента, таймаутом или вообще не стартовать. Эти события нельзя складывать в одну величину и называть её throughput.
Вторая граница проходит между учебной моделью и настоящим тестом. В учебном примере можно материализовать слоты в принятые и отклонённые units, чтобы проверить порядок и остановку. Такой код не создаёт часы, сеть, сервер, очередь, базу или telemetry. Он проверяет форму рассуждения. Чтобы говорить о latency, error rate или capacity, нужен отдельный запуск с реальным источником времени, конфигурацией инструмента, данными и raw output.
\nТретья граница — между профилем и результатом. Профиль задаёт переход warm-up → steady → step → recovery. Результат показывает, что произошло на каждом переходе. Один total скрывает порядок. Два запуска с одинаковым total могут иметь разные входы и разные причины ухудшения.
Запишите пять полей до запуска. endpointIntent ограничивает предмет и метод. dataSetup описывает набор данных и его identity. environment фиксирует build, конфигурацию и внешние зависимости. segments задают порядок и объём входа. stopCriterion объясняет, когда эксперимент заканчивается и какой результат считается достаточным.
К этим полям добавьте evidencePacket. Он связывает план с наблюдением: хранит identity среды и данных, имена сегментов, число запланированных слотов, принятые и отклонённые units, состояние остановки и диагностические пометки. Пакет не заменяет результат инструмента. Он не делает fixture trace и не создаёт production evidence. Он не даёт потерять условия, пока команда читает результат.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Есть только «много запросов» и один total | Aggregate выдали за 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 | Оставить один измеримый критерий и повторить тест |
Ниже учебная модель для одного нейтрального endpoint. Она намеренно не выполняет HTTP-запрос. Сегмент step планирует пять слотов, но принимает только три по заранее объявленному synthetic limit. Два слота остаются отклонёнными. Это позволяет проверить отрицательный путь: нагрузка не исчезла между планом и packet, а recovery идёт после границы.
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);\nAssertion в конце не измеряет производительность. Она защищает термин. Объект с одним счётчиком не получает статус нагрузочной модели. В коде нет fetch, часов, процесса и внешнего состояния. Поэтому его результат нельзя читать как latency, throughput, error rate или capacity. Если добавить реальный вызов, это станет уже другой проверкой с другими условиями.
Arrival — это правило появления следующего входа. Оно не равно completed work. При модели с фиксированным числом виртуальных пользователей следующая итерация может зависеть от завершения предыдущей. При модели с открытым потоком инструмент старается поддержать заданный arrival rate и отдельно показывает, успевает ли система обрабатывать вход. Значение зависит от конкретного executor, версии и конфигурации. Переносить смысл одного параметра между инструментами нельзя.
\nПоэтому сначала назовите вопрос. Проверяете поведение endpoint при росте входа? Проверяете сохранение времени ответа при фиксированном потоке? Проверяете прохождение порога ошибок? Для каждого вопроса нужны свои единицы и свой stop criterion. Слово «нагрузка» без этого выбора слишком широко.
\nХорошая метрика имеет смысл до того, как попадёт на dashboard. Назовите endpoint, service, environment, segment и единицу. Не называйте synthetic rejection ошибкой HTTP. Не называйте число слотов RPS, если у него нет времени. Не смешивайте клиентский timeout с ответом сервера. Такие запреты короче, чем последующее расследование неверного графика.
\nСмысл полей должен сохраняться при агрегации. Если два результата нельзя сопоставить по endpoint, данным, среде и профилю, их нельзя честно сравнить по одному p95 или total. Пакет свидетельств нужен именно для этого: он показывает, какие условия совпали, а какие изменились.
\nwarm-up, steady, step и recovery либо объясните другой порядок.Эта статья не сообщает, какую нагрузку выдержит конкретный сервис. Учебный пример не создаёт пользователей, соединения, DNS, TLS, сеть, очередь, CPU, память, базу, cache, browser или production incident. Synthetic limit показывает только заранее заданную границу модели. Rejected unit не означает HTTP error. Recovery в списке не доказывает восстановление реальной системы.
\nОфициальная документация инструмента всё равно нужна перед запуском. Сценарии k6 описывают разные профили workload, а thresholds задают pass/fail criteria для конкретных метрик. OpenTelemetry задаёт общие имена и смысл атрибутов, но не превращает любую локальную цифру в корректную телеметрию. Эти источники помогают выбрать термин и критерий. Они не заменяют проверку вашей среды и данных.
\nМатериал и реальный запуск готовы к анализу, если независимый инженер может по packet ответить на шесть вопросов: какой endpoint проверяли; какие данные использовали; в какой среде; как появлялся вход; что считалось выполненной работой; почему тест остановился. Для каждого сегмента сходятся planned = accepted + rejected. Для каждой заявленной метрики указаны источник, единица и окно. Если хотя бы один ответ отсутствует, результат — не verdict о системе, а незавершённый эксперимент.
\nПроблема фразы «мы дали много запросов» в том, что она описывает объём шума, но не модель нагрузки. В ней не видно, когда возникал input, какой endpoint он представлял, какой набор данных использовался, что считалось завершением и кто наблюдал последствия. Цена — ложная причинность: aggregate меняется, а команда приписывает его очереди, базе или коду, хотя могла измениться сама среда или сценарий.
\nДля механизма достаточно одной строгой границы. В этой статье arrival означает намерение поставить planned request slot в сегмент профиля. Это не отправка запроса и не скорость. Fixture материализует slots в accepted/rejected synthetic units внутри local objects. Она не создаёт clock, HTTP, сеть, процесс, нагрузочный инструмент или telemetry. Поэтому слова latency-signal и error-signal ниже — диагностические ярлыки, а не измеренные метрики.
\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| Свойство | Профиль fixture | «Много запросов без сценария» | Инженерское последствие |
|---|---|---|---|
| Endpoint intent | один нейтральный путь и метод | не указан | нельзя проверить контракт входа |
| Arrival intent | slots принадлежат named segment | есть только total | нельзя увидеть переходы |
| Среда | identity и synthetic constraint записаны | не указана | любой drift маскируется под результат |
| Accepted / rejected | разложены по segment | не определены | неясно, что именно не прошло |
| Stop criterion | evidence после recovery | нет | конец наблюдения произволен |
| Interpretation | только training model | обычно звучит как verdict | риск выдать счётчик за throughput |
В реальном инструменте способ моделировать arrival зависит от executor, версии и конфигурации. Нельзя переносить значение одного параметра между tool без чтения его документации. В k6 v0.35.0 release notes отдельно связывают stage tags с конкретными executors; это исторический факт о версии, а не лицензия назвать любой массив slots её сценарием. Наша fixture специально не повторяет API инструмента. Она показывает только вопрос, который нужно сформулировать до выбора API: что означает появление следующего planned slot и как эта попытка будет отделена от результата обработки.
Разделение полезно и для закрытой модели пользователей, и для открытой модели arrival. В первом случае нужно назвать, кто ждёт завершения предыдущей итерации. Во втором — как tool ведёт себя при невозможности начать следующую работу. Но оба случая остаются неполными без data setup и environment. Число на оси не заменяет эту информацию. Поэтому article не предлагает универсальный executor и не выдаёт synthetic acceptance limit за реальную настройку генератора.
\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 исчезла между профилем и таблицей, автор больше не может честно сказать, что сравнил одно и то же. Именно такие мелкие несовпадения потом превращают нагрузочную заметку в набор несвязанных сигналов.
Наблюдаемость здесь не означает автоматически подключённый 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; он не может быть основанием приписать старой системе готовую телеметрию.
Evidence packet полезен потому, что связывает две шкалы: plan и observation. План отвечает, что хотели проверить. Observation отвечает, какие учебные units были materialized. Если packet не содержит environment или criterion, визуализация всё равно может существовать, но reader уже не знает, какую именно гипотезу она проверяет. Поэтому packet не экспортируется как trace и не изображает работу настоящего мониторинга: это object для детерминированной проверки редакционной модели.
\nStep в fixture получил пять slots при явном limit в три accepted synthetic units. Модель оставляет два rejected units и поднимает единственный syntheticBottleneckFlag. Это намеренно скучный результат. Его задача — проверить четыре свойства: rejected units видны, flag находится в правильном segment, recovery идёт после step, stop criterion не срабатывает раньше. Если бы модель всегда принимала всё, она не проверяла бы путь, в котором author обязан объяснить границу.
Неправильный вывод звучал бы так: «мы нашли bottleneck и latency выросла». У fixture нет времени, сервера или наблюдаемой очереди, поэтому такой вывод нельзя получить. Правильный вывод уже: «план содержит заранее отмеченную synthetic boundary; для реального расследования нужны environment manifest, raw output выбранного tool и отдельный источник latency-signal». Скромная формулировка оставляет место для следующего эксперимента, а не подменяет его.
\nЭтот пример читает mechanism, а не запускает testing software. Он выводит путь как intent, явное ограничение среды и stop object. Последняя assertion возвращает false для aggregate, где есть только synthetic total. Так мы проверяем не способность «создать много», а способность не называть нагрузочной моделью то, что не содержит сценария.
\nconst 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.
Fixture не сообщает, как ведут себя executors k6, как рассчитываются thresholds, как именно агрегирует метрики OpenTelemetry или какие свойства имеет конкретный server. Ссылки на k6 v0.35.0 и его samples/thresholds.js зафиксированы, чтобы не ссылаться на mutable current documentation. Они нужны только для historical boundary и для требования называть metric вместе с порогом. Пакет не исполняет k6, не создаёт VU и не использует его API.
Не моделируются 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