diff --git a/editorial/agent-rewrites/113.json b/editorial/agent-rewrites/113.json index d77f70f..e134572 100644 --- a/editorial/agent-rewrites/113.json +++ b/editorial/agent-rewrites/113.json @@ -1,7 +1,7 @@ { "index": 113, "slug": "editorial-2024-11-mechanism-deprecation", - "title": "Deprecation API: почему пустая telemetry не разрешает удаление", - "excerpt": "Пустой график вызовов не доказывает отсутствие потребителей API. Разбираем пять типов evidence, границы deprecation и removal gate на ограниченном примере.", - "contentHtml": "
Команда собирается удалить старый endpoint. За последние две недели в dashboard нет вызовов. В access log видны только запросы к новой версии. Кто-то делает вывод: потребителей больше нет.
\nСимптом выглядит убедительно, но он описывает только выбранный источник наблюдения. В него могли не попасть редкие клиенты, другой gateway, кэш, batch-задача или credential с отдельной политикой доступа. Цена ошибки — несовместимый релиз для неизвестного caller. Ошибка проявится после удаления, когда старый контракт уже не с чем сравнивать.
\nТезис статьи простой: deprecation — это управляемый переход, а не доказательство отсутствия пользователей. Сначала нужно разделить типы сведений и их границы. Затем объявить замену, предупредить потребителей, определить sunset boundary и только после отдельной проверки открыть removal gate. Ни один сигнал сам по себе не превращает пустую telemetry в полный список клиентов.
\nНачните с одной операции: method, URI template, operationId и версия контракта. Формулировка «удаляем старый API» слишком широка. Для POST /v1/ledger/entries вопрос звучит точнее: какие callers ещё зависят от поведения этой операции, кто отвечает за замену и какие наблюдения покрывают маршрут?
Source usage показывает вызов в заданном дереве исходников. Он помогает найти известный сервис, но не видит закрытый репозиторий, скомпилированный клиент или deployed версию, которая не совпала с локальной веткой. Declared dependency показывает объявленный SDK, schema или contract. Зависимость может остаться в manifest после миграции и не доказывает runtime-вызов.
\nExposure or traffic показывает observed requests в конкретном инструменте, маршруте, периоде и sampling policy. Запрос не равен пользователю. Один сервис может отправить тысячу запросов, а редкий клиент — один запрос за месяц. Нулевое значение означает «в этом scope сигнал не найден», а не «caller отсутствует».
\nAuthorization показывает, какой credential class или permission способен обратиться к resource. Способность не равна активности. Если политика допускает партнёрский ключ, это ещё не доказывает, что партнёр вызывает операцию. Но если такой класс не учтён, removal имеет слепую зону.
\nUnknown consumer — не ошибка заполнения таблицы. Это честное состояние, когда scope нельзя замкнуть. Его нужно хранить отдельно от zero и migrated. Именно unknown должен блокировать автоматическое решение об удалении.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| График вызовов равен нулю | Окно или route не покрывает всех callers | Записать инструмент, период, sampling, cache и auth boundary | Оставить consumer unknown и расширить разрешённую проверку |
| В manifest осталась зависимость | Declared dependency приняли за runtime usage | Сверить версию, owner и фактический call path | Назначить migration owner, не удалять endpoint по одной записи |
| В OpenAPI стоит deprecated | Декларацию приняли за завершённую миграцию | Найти replacement и проверить его контракт | Опубликовать warning и сохранить совместимое поведение |
| Наступила дата Sunset | Дату приняли за доказательство, что все ушли | Провести removal review по отдельному scope | Остановить change при active или unknown row |
Warning делает замену видимой. Документация должна назвать replacement, owner, область действия и способ задать вопрос. Warning не подтверждает, что клиент его прочитал. Поэтому он не меняет функциональное поведение endpoint.
\nDeprecation сообщает, что операция больше не является предпочтительной. В OpenAPI это поле deprecated: true. В HTTP можно использовать Deprecation header и ссылку на документацию, если такой сигнал поддерживают ваши клиенты. Эти механизмы помогают обнаружить новые зависимости, но не выключают ресурс и не перечисляют callers.
Sunset задаёт планируемую границу возможной недоступности. RFC 8594 описывает её как сигнал о том, что конкретный URI, вероятно, станет недоступен в указанное время. Это hint, а не доказательство миграции и не гарантия, что сервер исчезнет ровно в timestamp. До этой границы всё равно нужна проверка остаточного риска.
\nRemoval — отдельное несовместимое изменение. Оно должно иметь узкий scope, owner, stop condition и restore boundary. Если одна строка consumer map имеет состояние active или unknown, автоматическое удаление нельзя считать безопасным. Состояние migrated разрешает только review: нужно проверить замену, данные и поведение, а не просто закрыть старый маршрут.
Ниже приведён синтетический пример. Имена, даты и строки не получены из production, telemetry или списка клиентов. Они нужны, чтобы показать логику решения на одном endpoint.
\noperation: POST /v1/ledger/entries\nreplacement: POST /v2/ledger/entries\nowner: ledger-team\nsunset: 2025-02-03\n\nconsumer evidence state\ncheckout-service source usage active\nmobile-sdk declared dependency migrated\npartner-gateway authorization unknown\n\nremoval: block\nreason: active and unknown consumers remain\nПервая строка содержит наблюдаемый вызов. Она блокирует removal. Вторая подтверждает план перехода, но сама по себе не доказывает, что старый endpoint больше не вызывается. Третья показывает capability boundary: gateway может иметь доступ, однако его активность не установлена. Вердикт должен остаться block.
Если заменить значение unknown на zero, факты не изменятся. Изменится только видимость риска. Поэтому consumer map должна хранить не только state, но и evidence scope: tool, period, route, sampling, auth boundary, owner и blind zone. Без этих полей строка не воспроизводится.
Иногда все named consumers мигрировали, а неизвестный класс доступа остался. Это не повод объявить карту полной. Сохраните старый контракт совместимым, сузьте следующую проверку до разрешённой auth boundary и назначьте срок пересмотра. Если нужное наблюдение нельзя получить законно или технически, риск остаётся unknown. Дата Sunset не превращает его в zero.
\nДругой отрицательный путь — replacement меняет semantics: новые обязательные поля, другой порядок побочных эффектов или иные коды ошибок. Даже полный список callers не делает такое удаление безопасным. Сначала нужно сравнить контракты и решить, как клиент переживёт несовместимость. Если restore невозможен после записи новых данных, rollback старого route не восстановит прежнее состояние.
\nЭта схема не создаёт универсальный consumer map. Она не отменяет sampling, кэширование, batch-вызовы, закрытые сети, задержку доставки логов и различия между deployed и исходным кодом. Она также не говорит, сколько дней нужно наблюдать. Окно зависит от частоты вызовов и допустимого риска, а его границы нужно обосновать.
\nУчебный код выше не вызывает сеть, не читает файлы и не утверждает production-результаты. Официальные спецификации описывают смысл сигналов, но не проверяют ваш API. Реальную готовность устанавливает только evidence с понятным scope и ответственным владельцем.
\nRemoval готов к отдельному рассмотрению, если выполнены все условия: одна операция имеет названный replacement; у старого и нового контрактов есть owners; warning и deprecation опубликованы; каждая строка consumer map содержит тип evidence, scope, период и blind zone; active и unknown строки либо закрыты разрешённой проверкой, либо явно приняты владельцем как residual risk; replacement проверен на критичных входах; определены stop condition и restore boundary. Если хотя бы одно условие неизвестно, вердикт — block, а не «пользователей нет».
deprecated в описании операции.Команда хочет удалить POST /v1/ledger/entries: за последние две недели на одном графике нет вызовов, а в access log видна только версия /v2. Такое наблюдение легко превратить в фразу «потребителей больше нет». Но график описывает не API, а выбранный инструмент, маршрут и окно времени. Редкий партнёр, batch-задача, другой gateway или закэшированный запрос могли в него не попасть.
Цена ошибки — несовместимый релиз для caller, о котором команда не знает. После удаления уже нельзя сравнить старое поведение с новым, а восстановление маршрута не исправит побочный эффект, если запрос успел записать данные. Поэтому deprecation — не команда «удалить позже», а переход с отдельными сигналами, владельцами, evidence и условием остановки.
\nНиже используется синтетический пример: имена, дата и строки таблицы придуманы для объяснения метода. Они не сообщают о реальных клиентах или production telemetry. Цель — показать, как из пустого сигнала получить проверяемое решение, а не ложное доказательство отсутствия пользователей.
\nФраза «устарел старый API» слишком широкая. В карту нужно занести method, URI-шаблон, operationId, версию контракта и replacement. Например: POST /v1/ledger/entries заменяется на POST /v2/ledger/entries. Если в одной строке смешать ещё GET, другой ресурс и несколько версий, результат нельзя будет связать с конкретным запросом.
Затем назначьте две ответственности. Владелец старой операции отвечает за совместимость и решение о её отключении. Владелец replacement отвечает за различия входных полей, кодов ошибок и побочных эффектов. У одного человека или команды может быть обе роли, но это должно быть явно записано.
\n| Тип evidence | Что он подтверждает | Чего он не подтверждает | Следующий шаг |
|---|---|---|---|
| Source usage | В доступном дереве исходников найден call path | Закрытые репозитории и deployed-версия | Назвать owner и проверить фактический релиз caller |
| Declared dependency | SDK, схема или контракт объявлены зависимостью | Что библиотека действительно вызывает старую операцию | Сверить версию и runtime call path |
| Traffic | Запросы видны в конкретном маршруте, окне и sampling policy | Редкие, кэшированные и неохваченные запросы | Записать blind zone и расширить наблюдение |
| Authorization | Credential class имеет право обратиться к resource | Что этот класс обращается к нему сейчас | Проверить активность в разрешённом auth scope |
| Unknown | Граница наблюдения не позволила классифицировать caller | Что caller отсутствует | Остановить автоматическое удаление |
Состояния zero и unknown нельзя склеивать. Zero означает ноль найденных событий внутри заранее описанного scope. Unknown означает, что scope недостаточен, недоступен или не связывает событие с caller. Второе состояние не является отрицательным результатом поиска.
В OpenAPI 3.1 у Operation Object есть поле deprecated. Значение true сообщает потребителям описания, что операцию следует перестать использовать. Это полезно для документации, генераторов клиента и review контракта. Оно не удаляет endpoint, не проверяет обновление сгенерированного SDK и не показывает фактический трафик.
openapi: 3.1.0\ninfo:\n title: Ledger API\n version: 2.4.0\npaths:\n /v1/ledger/entries:\n post:\n operationId: createLedgerEntryV1\n deprecated: true\n description: Use POST /v2/ledger/entries. The v1 contract remains available until the removal review.\n responses:\n '201':\n description: Entry created\n /v2/ledger/entries:\n post:\n operationId: createLedgerEntryV2\n responses:\n '201':\n description: Entry created\nТакой фрагмент можно проверить в локальном файле без доступа к реальному API:
\njq -e '.paths[\"/v1/ledger/entries\"].post.deprecated == true' openapi.json\n# Для YAML сначала преобразуйте файл вашим валидатором OpenAPI.\ncurl -fsSI https://api.example.test/v1/ledger/entries | grep -i '^Sunset:'\nПервая команда ожидает JSON-документ; для YAML нужен валидатор или конвертер, принятый в проекте. Во второй замените домен на тестовый endpoint. Если тестовый endpoint требует авторизацию, добавьте её безопасным способом и не помещайте секрет в shell history.
\nRFC 8594 определяет Sunset как response header, который указывает, что URI вероятно станет недоступен в заданный момент. «Вероятно» здесь существенно: это объявление жизненного цикла, а не гарантия доступности до даты, не расписание миграции и не список callers. В RFC также описан link relation sunset для документации политики или вариантов смягчения.
На дату этой статьи отдельный HTTP-сигнал deprecation ещё нельзя выдавать за устоявшийся RFC-контракт: опубликованный документ был Internet-Draft, поэтому команда может использовать только этот draft или внутреннюю policy, которую реально поддерживают её клиенты. Безопаснее считать OpenAPI-декларацию и документацию основными каналами, а response headers вводить после проверки совместимости gateway, SDK и кэшей.
\nWarning делает replacement заметным. В документации должны быть ссылка, owner, различия контрактов и канал поддержки. Warning не меняет поведение операции: клиент может его не прочитать.
\nDeprecation фиксирует, что операция больше не является предпочтительной. Это момент обновления контракта и инструкций, а не момент отключения. Если проект применяет SemVer к публичному API, спецификация рекомендует выпустить minor-версию при объявлении deprecated-функциональности.
\nSunset задаёт плановую границу возможной недоступности. Дату нужно согласовать с владельцами callers и явно связать с часовым поясом. Наличие даты не превращает unknown в zero: редкий ежемесячный вызов не становится безопасным только потому, что календарь прошёл.
\nRemoval — отдельное несовместимое изменение. Оно требует узкого change scope, human review, stop condition и понятной границы восстановления. В SemVer удаление функциональности после её deprecation обычно относится к major-изменению, но это правило действует только для проектов, которые действительно следуют SemVer и имеют объявленный public API.
\nНиже тот же endpoint с пятью типами evidence. Обозначение active значит, что вызов подтверждён в установленном scope. migrated значит, что конкретный caller переведён и его replacement проверен. unknown оставляет вопрос открытым.
operation: POST /v1/ledger/entries\nreplacement: POST /v2/ledger/entries\nowner: ledger-team\nsunset: 2025-02-03T00:00:00Z\n\nconsumer evidence state\ncheckout-service source + traffic active\nmobile-sdk declared dependency migrated\npartner-gateway authorization unknown\nnightly-export batch inventory unknown\n\nremoval: block\nstop_condition: any active or unknown row\nreason: replacement and consumer scope are not closed\nДля checkout-service removal блокируется сразу: активный вызов важнее пустого среднего графика. mobile-sdk не блокирует решение только после проверки, что новая операция принимает нужные поля и сохраняет требуемые ошибки. Две строки unknown нельзя заменить на zero без нового evidence с описанной границей.
Практическая форма карты должна хранить не только state, но и инструмент, период, маршрут, sampling, auth boundary, ссылку на артефакт и owner. Например: «traffic, gateway A, 2024-10-01—2024-10-31, sampled 1:100, без batch-сети». Такая запись не доказывает отсутствие callers, зато позволяет увидеть, какую именно дыру нужно закрыть.
\nДаже полный список callers не спасает несовместимую замену. Сравните обязательные поля, формат идентификатора, коды ответа, идемпотентность и порядок побочных эффектов. Для записи в ledger особенно важны повтор запроса, таймаут после записи и поведение при частичном отказе.
\nblock.Первый сценарий — «все named consumers мигрировали». Это хороший результат инвентаризации, но не доказательство полной видимости. Если партнёрский ключ всё ещё имеет право на маршрут, capability нужно либо сузить, либо проверить активность в разрешённой системе. Пока ни одно действие не выполнено, состояние остаётся unknown.
Второй сценарий — replacement отвечает быстро, но иначе обрабатывает повторный запрос. Клиент мог рассчитывать на идемпотентный ключ старой операции, а новая версия создаёт вторую запись. В этом случае статистика перехода и отсутствие вызовов старого маршрута не компенсируют разницу semantics. Сначала исправляется контракт или адаптер, затем пересматривается gate.
\nМетод не даёт универсального числа дней для наблюдения. Окно зависит от частоты вызовов, сезонности, SLA и допустимого риска. Для ежемесячного batch две недели явно не покрывают полный цикл. Sampling, кэш, offline-клиенты, задержка логов и несколько gateway могут скрывать обращения.
\nМетод также не разрешает искать данные там, где у команды нет права доступа. Если нужный auth или traffic source недоступен, это ограничение нужно записать как blind zone, а не замаскировать уверенным нулём. Синтетический пример выше не делает сетевых запросов и не описывает реальный релиз; его можно воспроизвести только как шаблон карты и проверки собственного API.
\nRemoval можно выносить на отдельное рассмотрение, когда у операции есть названный replacement и owners, опубликованы warning и deprecation, карта callers содержит тип evidence и его границы, а replacement проверен на критичных входах и ошибках. Active и unknown строки должны быть закрыты разрешённым наблюдением или явно принятым residual risk. Должны быть определены stop condition и граница восстановления.
\nЕсли хотя бы одно из этих условий неизвестно, честный результат — block. Это не означает, что endpoint нужно поддерживать навсегда. Это означает, что следующий шаг должен уменьшить конкретную blind zone: найти batch inventory, связать credential с caller, сравнить контракт или назначить владельца.
Sunset и link relation sunset. Документ информационный: он не задаёт ваш процесс миграции и не перечисляет потребителей.deprecated и его значение для потребителей описания операции.В задаче стоит дата: после 3 февраля путь /v1/posting удалят. Наступает день релиза. В графике вызовов пусто, в OpenAPI уже стоит deprecated: true, а команда не видит владельца старого клиента. Кто-то открывает pull request и удаляет обработчик. Через час внешний интегратор получает 404 или 410. У команды нет ответа на три вопроса: кому сообщили, какой контракт предложили взамен и что именно доказало безопасность удаления.
Это не редкий сбой календаря. Дата задаёт границу планирования, но не подтверждает отсутствие потребителей. Пустой график описывает только выбранный инструмент, маршрут, период и набор сигналов. Пометка в схеме сообщает о жизненном цикле операции, но не мигрирует SDK. Реальная депрекация должна разделять объявление, миграцию и удаление.
\nТезис: не удаляйте устаревший API по дате или одному зелёному индикатору. Сначала ограничьте одну операцию, составьте consumer map и укажите границу каждого доказательства. Если остался активный или неизвестный потребитель, автоматическое удаление запрещено. Для полностью подготовленного случая открывают отдельный human review, а не превращают ревью депрекации в незаметный production change.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Есть дата удаления, но нет списка владельцев | Дедлайн приняли за доказательство последнего consumer | Проверить contract, scope, owner и evidence boundary | Заблокировать removal и открыть consumer map |
| В telemetry нет запросов | Пустой сигнал ограничен периодом, sampling, proxy или credential boundary | Записать инструмент, окно, маршрут и слепые зоны | Назвать результат not observed in this scope, а не «никто не использует» |
В OpenAPI стоит deprecated: true | Декларацию смешали с миграцией | Найти replacement, владельца и путь перехода | Опубликовать notice и migration guide |
| Все известные клиенты мигрировали | Named rows приняли за полную population | Проверить unknown scope и остаточный риск | Разрешить только отдельный human review |
Фраза «удаляем v1» слишком широкая. Она может означать один HTTP route, несколько методов, SDK-функцию, callback или весь набор ресурсов. Выберите одну operation. Запишите HTTP method, URI template, operationId, request и response shape, authentication boundary и replacement. Если новый путь меняет семантику, сравните не только URL. Проверьте idempotency key, коды ошибок, pagination, ретраи и правила авторизации.
\nТакой scope снижает риск ложного согласия. Владелец сервиса может подтвердить удаление одного метода, но не всего API. Владелец SDK может выпустить новую функцию, но не контролировать старые бинарные клиенты. Внешний потребитель может получать notice через документацию, но не читать вашу схему. Контракт, владелец и канал объявления должны совпадать по scope.
\nOpenAPI помогает объявить операцию устаревшей. Поле deprecated отвечает на вопрос «рекомендуется ли эта операция дальше?». Оно не отвечает на вопросы «кто вызывает её сейчас?» и «может ли replacement принять тот же сценарий?». Поэтому schema и consumer map — разные артефакты. Один сообщает о намерении. Второй связывает намерение с людьми, зависимостями и проверками.
Минимальная строка карты содержит consumer, класс сведения, contract, owner, migration path, deadline, announcement и evidence boundary. Не скрывайте неизвестность. Строка unknown consumer честнее, чем пустая таблица. Она означает, что область ещё не замкнута и автоматический removal нельзя считать безопасным.
| Потребитель | Сведение | Владелец и переход | Граница доказательства |
|---|---|---|---|
synthetic-web-checkout | source usage | synthetic-checkout-owner; fixed v1 call → fixed v2 contract | Учебная строка, не результат поиска кода |
synthetic-sdk-package | declared dependency | synthetic-sdk-owner; выпустить v2 SDK surface | Учебная запись зависимости, не package inventory |
synthetic-unknown-integrator | unknown consumer | synthetic-api-owner; сохранить notice и ограничить scope | Не customer list и не доказательство отсутствия вызовов |
Разные классы evidence нельзя складывать в один count. Source usage показывает вызов в разрешённой области исходного кода. Declared dependency показывает объявленную связь пакета, схемы или SDK. Observed traffic показывает запросы в конкретном маршруте и окне. Authorization показывает, какой credential class имеет право обратиться. Ни один класс сам по себе не доказывает полную population клиентов.
\nНапример, пустой access report может не видеть запросы через gateway, другой hostname, старый credential или редкий batch. Кодовый поиск может не найти вызов, спрятанный в сгенерированном клиенте или внешнем binary. Manifest может хранить уже неиспользуемую зависимость. Поэтому рядом с каждой строкой пишите не только результат, но и то, чего он не доказывает.
\nУведомление должно дать потребителю понятный replacement, владельца и ссылку на инструкцию. HTTP-заголовок Deprecation может сообщить, что ресурс устарел или станет устаревшим. Sunset сообщает ожидаемую будущую недоступность ресурса. Оба сигнала улучшают обнаруживаемость решения. Они не заставляют клиент мигрировать и не подтверждают, что клиент получил, понял или применил notice.
\nСохраните в записи lifecycle отдельные поля. Дата депрекации описывает статус контракта. Sunset boundary описывает ожидаемое изменение доступности. Removal gate описывает условия допуска к отдельному изменению. Не называйте Sunset жёсткой гарантией: RFC 8594 формулирует его как указание на ожидаемую недоступность, а не как доказательство фактического поведения каждого клиента.
\n{\n \"contract\": \"synthetic-ledger-v1-posting-path\",\n \"owner\": \"synthetic-ledger-owner\",\n \"replacement\": \"synthetic-ledger-v2-posting-path\",\n \"deprecationDate\": \"synthetic-2024-11-04\",\n \"sunsetBoundary\": \"synthetic-2025-02-03\",\n \"announcement\": \"synthetic-public-deprecation-page\",\n \"unknownConsumerRule\": \"unknown blocks automatic removal\",\n \"restoreBoundary\": \"stop proposal before a real removal change\"\n}\nКод выше — учебный объект. Он не является production-конфигурацией, не содержит реальную дату, токен, route table или список клиентов. Его задача — показать обязательные связи. Если у replacement нет владельца или у unknown нет границы проверки, объект не готов к removal review.
\nХороший gate формулируют как список причин не удалять. Есть active consumer — остановиться. Есть unknown scope без принятого остаточного риска — остановиться. Replacement не сохраняет важную семантику — остановиться. Notice не связан с affected scope — остановиться. Restore boundary не описана — остановиться. Ранняя дата не компенсирует ни один из этих пробелов.
\nСлово «мигрировал» тоже требует проверки. Оно должно означать, что конкретный consumer получил replacement, проверил совместимость и больше не зависит от старой операции в согласованной границе. Если строка лишь помечена как migrated в таблице, это статус документа, а не runtime evidence. Оставьте исходный тип сведения и ссылку на проверку.
\nЕсли перед удалением найден active consumer, не меняйте одновременно route, authorization, документацию и fallback. Остановите proposal. Зафиксируйте конфликт, сохраните текущую compatibility boundary и назначьте владельца миграции. Если неизвестный потребитель остаётся, владелец должен выбрать одно из трёх действий: сузить разрешённую проверку, продлить совместимость или принять residual risk отдельным решением.
\nЭта схема не делает неизвестных потребителей видимыми автоматически. Она не заменяет разрешённый source search, анализ access logs, inventory клиентов, review авторизации или проверку replacement в реальной среде. Учебные строки в статье не доказывают наличие или отсутствие клиентов. Они показывают, как сохранить класс риска в модели.
\nНе пытайтесь получить «нулевой риск» из одного сигнала. Даже полный на вид отчёт имеет scope: конкретный период, route, credential, sampling и доступность данных. Отрицательный путь должен быть первым классом результата. Если проверка не может замкнуть область, ответ — unknown, а действие — сохранить совместимость или провести отдельное решение с владельцем остаточного риска.
Restore boundary также ограничивает обещание. Для draft достаточно сказать: proposal остановлен, старый контракт не изменён, черновик удалён. После реального удаления нужен другой план: кто возвращает route, какие schema и credentials ещё совместимы и как проверяется восстановление. Фраза «rollback available» без этих условий не является проверяемым планом.
\nДепрекация готова к отдельному removal review, когда одна operation однозначно названа; replacement имеет владельца и описанную compatibility boundary; каждая известная строка consumer map содержит migration path; unknown scope либо ограничен разрешённой проверкой, либо явно принят владельцем; notice и дедлайн доступны; stop condition и restore boundary записаны. Итоговый вердикт должен быть одним из трёх: block: active, block: unknown или allow human review only. Ни один из них не означает, что endpoint уже можно удалить.
Проверяемый результат — не пустой график и не дата в календаре. Это воспроизводимая запись, в которой другой инженер видит scope, доказательство, его границу, владельца и причину следующего действия. Если он не может повторить проверку или назвать условие остановки, контракт ещё не готов к удалению.
\ndeprecated объявляет операцию устаревшей, но не описывает её потребителей.В задаче на удаление API обычно есть убедительная дата: после 3 февраля старый путь должен исчезнуть. В день релиза график вызовов пуст, в OpenAPI стоит deprecated: true, а владельца старого клиента никто не знает. Обработчик удаляют. Через час внешний интегратор получает 404, а команда не может ответить, кто предупредил потребителя и где описан переход.
Причина не в том, что календарь составили плохо. Дата задаёт план, но не доказывает отсутствие потребителей. Пустой график описывает только выбранный маршрут, период, прокси, credentials и качество телеметрии. Пометка в схеме сообщает о жизненном цикле операции, но не обновляет SDK и не доставляет уведомление. Без карты зависимостей удаление превращается в догадку.
\nРабочее правило: сначала докажите границы проверки, затем принимайте решение об удалении одной операции. Результат проверки может быть observed, not observed in scope или unknown. Последние два результата нельзя склеивать с фразой «никто не использует». При активном или неизвестном потребителе автоматический removal gate закрыт.
Фраза «убираем v1» слишком широка для безопасного изменения. Она может означать один метод, несколько ресурсов, SDK-функцию, webhook или всю версию API. Начните с одной операции и запишите её как контракт: HTTP-метод, URI-шаблон, operationId, форму запроса, ответы, коды ошибок и границу авторизации.
Такой scope нужен не для бюрократии. Владелец сервиса может подтвердить удаление POST /v1/posting, но не всего /v1. Новый URL может принимать тот же JSON, но иначе обрабатывать повторную доставку, идемпотентность, пагинацию или права. Поэтому рядом с replacement фиксируйте семантические отличия. Совпадение URL и формата ещё не означает совместимость.
| Поле | Что записать | Почему это ограничивает риск |
|---|---|---|
| Operation | POST /v1/posting и operationId | Не даёт распространить решение на соседние методы |
| Replacement | Новый метод, схема, ошибки и правила повторов | Показывает, что переход — не простая замена строки |
| Population | Известные, внешние и неизвестные классы клиентов | Не маскирует неполноту инвентаризации |
| Evidence boundary | Инструмент, окно, маршрут, credentials, sampling и пропуски | Отделяет наблюдение от доказательства полноты |
| Restore boundary | Что возвращается и кто проверяет восстановление | Превращает rollback из обещания в проверяемое условие |
Карта потребителей должна разделять классы сведений. Поиск исходников показывает вызовы в просмотренной области. Manifest показывает объявленную зависимость. Access log показывает запросы, дошедшие до конкретного слоя. Сведения от владельца подтверждают намерение команды, но не заменяют runtime-проверку. У каждого сигнала свой blind spot, поэтому итоговая строка хранит и результат, и границу.
\n| Результат | Что он действительно означает | Чего он не доказывает | Следующее действие |
|---|---|---|---|
source: found | В просмотренном репозитории найден вызов | Что это единственный потребитель | Назначить владельца и запланировать миграцию |
traffic: zero | За окном не было видимых запросов | Что нет batch, другого gateway или редкого клиента | Расширить окно и сверить маршрут и credentials |
declared: absent | Зависимость не записана в проверенном manifest | Что внешний binary или сгенерированный SDK отсутствует | Проверить inventory и канал объявления |
scope: unknown | Полнота области не доказана | Что endpoint безопасно удалять | Сохранить совместимость или принять риск отдельно |
Например, отсутствие записи в репозитории не видит закрытый исходный код, сгенерированный клиент или старый бинарный клиент. Нулевой access log не видит запрос, который прошёл через другой hostname, не попал в выбранный gateway или пришёл раз в квартал. Даже полный на вид отчёт относится к своему окну и credential class. Именно поэтому строка unknown consumer полезнее пустой ячейки: она сохраняет незамкнутую область в решении.
В OpenAPI 3.1 поле deprecated у Operation Object объявляет операцию устаревшей и рекомендует потребителям прекратить её использование. Это описание контракта. Оно не ищет клиентов, не выпускает новую версию SDK и не меняет ответ сервера.
Runtime-уведомление решает другую задачу. Заголовок Deprecation из RFC 9745 сообщает клиенту дату депрекации; его значение — structured date, например Deprecation: @1688169599. Тот же RFC подчёркивает: сам факт депрекации не меняет поведение ресурса. Для документации можно добавить Link с отношением deprecation, где указаны replacement и миграционная инструкция.
Sunset из RFC 8594 сообщает, что ресурс ожидается недоступным после указанного HTTP-времени. Это подсказка для клиента, а не гарантия того, что до даты всё будет работать, а после неё обязательно появится конкретный код. В RFC 9745 также зафиксировано, что Sunset не должен быть раньше даты Deprecation. Проверяйте эти заголовки на фактическом ответе и не выдавайте их за доказательство миграции.
BASE_URL=https://api.example.test; curl --fail-with-body -sS -D response.headers -o response.body \"$BASE_URL/v1/posting\"; awk 'BEGIN{IGNORECASE=1} /^deprecation:|^sunset:|^link:/{print}' response.headers\nКоманда предназначена для собственного стенда: замените api.example.test адресом среды, где разрешена такая проверка, и не помещайте токены в shell history или публикацию. Если endpoint требует авторизацию, задайте её способом, принятым в проекте, и отдельно запишите, какой класс credentials был покрыт. Ответ одного proxy не подтверждает поведение другого слоя.
Минимальная запись содержит consumer, класс evidence, owner, replacement, migration path, срок, канал уведомления и evidence boundary. Для известного клиента нужна не только строка «migrated», а ссылка на проверку: версия SDK, тест совместимости, успешный запрос или подтверждённый rollout. Статус в таблице — это утверждение, его источник и область должны быть видны рядом.
\nconsumer,kind,owner,replacement,status,evidence_boundary; checkout-web,source-and-traffic,team-checkout,POST /v2/posting,migrating,repo=checkout; partner-batch,traffic-only,partner-team,POST /v2/posting,unknown,logs=gw-a; unknown-external,unknown,api-owner,none,block,public-route\nЭто CSV-образец, а не список реальных клиентов. В рабочем процессе его можно хранить в системе, где есть история изменений и доступ владельцев. Не записывайте секреты, токены и персональные данные: для связи достаточно идентификатора системы, команды и ссылки на разрешённое доказательство.
\nОтдельно проверяйте четыре направления: код и сгенерированные клиенты; реестр SDK и зависимостей; сетевые логи на ingress и gateway; коммуникацию с внешними владельцами. Дублируйте результат только там, где сигналы независимы. Три отчёта из одного прокси не превращаются в три доказательства.
\nХороший gate формулируется отрицательными условиями. Удаление запрещено, если найден активный consumer; если неизвестная область не ограничена и остаточный риск не принят; если replacement меняет значимую семантику без плана; если notice не связан с affected scope; если не описано восстановление. Ранняя дата не отменяет ни одного из этих условий.
\nРазделяйте два решения. Первое — можно ли объявить операцию устаревшей и начать миграцию. Второе — можно ли менять runtime так, чтобы старый путь перестал отвечать. Между ними должны быть окно совместимости, подтверждённые владельцы и отдельное human review. Не объединяйте удаление route, изменение прав, чистку документации и удаление SDK в один непроверяемый коммит.
\n| Вердикт | Условие | Разрешённое действие |
|---|---|---|
block: active | Есть запросы или подтверждённая зависимость | Остановить removal, назначить миграцию |
block: unknown | Область проверки не замкнута | Расширить наблюдение или продлить совместимость |
allow: review | Scope, replacement, owners, notice и restore boundary описаны | Открыть отдельный review; не удалять автоматически |
allow: change | Политика проекта допускает изменение после review | Применить узкий change и проверить соседние операции |
Эта схема не делает неизвестных клиентов видимыми автоматически. Она не заменяет договор с внешним партнёром, анализ закрытого кода, юридические сроки уведомления, требования к доступности или политику change management. Конкретное окно наблюдения и достаточная полнота зависят от трафика: для ежедневного запроса 30 дней может быть полезным сигналом, для квартального batch — нет.
\nЗаголовки тоже имеют границы. Клиент может их не читать, промежуточный кеш может изменить наблюдаемую картину, а Deprecation и Sunset не доказывают, что владелец клиента получил notice. RFC 8594 не определяет, будет ли после Sunset 4xx, 3xx или другой отказ. Поэтому не стройте единственную защиту на runtime-заголовке и не обещайте точный код ответа, если он не закреплён вашим контрактом.
Если потребитель активен, безопасное действие — остановить удаление и дать ему совместимый путь. Если потребитель неизвестен, безопасное действие — расширить scope или сохранить endpoint. Residual risk можно принять только тем владельцем и в том процессе, которые отвечают за последствия; запись «вроде никто не использует» таким решением не является.
\nОперация готова к removal review, когда её scope однозначен, replacement проверен по семантике, известные потребители имеют владельцев и миграционные доказательства, неизвестная область либо закрыта, либо явно принята, а notice и restore boundary доступны. Решение должно быть воспроизводимым: другой инженер может повторить запрос, понять границу логов, найти источник строки карты и назвать условие остановки.
\nИтогом не обязательно будет удаление. Иногда лучший результат — продлить совместимость, добавить телеметрию или сузить контракт. Дата в календаре становится полезной только после того, как рядом появились scope, доказательство, владелец и понятный следующий шаг.
\ndeprecated объявляет операцию устаревшей и не описывает список потребителей.deprecation, связь с документацией и границы runtime-сигнала. RFC опубликован в 2025 году, поэтому для процесса, зафиксированного раньше, отдельно проверьте поддерживаемый набор заголовков.На разборе ёмкости команда видит знакомую картину: p95 снизился после перехода на 4 CPU, а счёт и запас зарезервированных ресурсов выросли. Вторая карточка с 1,6 CPU показывала очередь и почти касалась SLO, поэтому большой инстанс кажется очевидным ответом. Цена ошибки — закрепить дорогой reservation без доказанного эффекта, принять одну удачную latency-точку за решение и потерять понятный путь возврата.
\nТезис: ёмкость выбирают не по лучшему p95, а по следующему проверяемому пределу. Сравните performance, модель стоимости и операционный риск в одном scope. Запишите условие остановки до выбора размера. Если хотя бы одна ось не объяснена, остановитесь и запросите недостающие данные.
\nСначала зафиксируйте workload, период, единицы и формулу стоимости. Затем разделите четыре слоя. request описывает ресурс, который workload просит у планировщика. limit задаёт верхнюю границу для контейнера. quota ограничивает суммарное потребление в области политики. Ни один из этих терминов не является ценой сам по себе.
Стоимость требует отдельной формулы. В ней могут участвовать базовая плата, объём выделенного ресурса, единица измерения, период, ступени тарифа и дополнительные услуги. Если цена неизвестна, используйте только обозначения. Не подменяйте счёт процентом CPU. Низкая загрузка говорит о наблюдаемом использовании, но не отвечает, какая часть счёта исчезнет после изменения.
\ncost(period) = fixed_base\n + allocated_cpu * cpu_rate\n + allocated_memory * memory_rate\n + traffic * traffic_rate\n\ncompare only one changed input\nstop when SLO headroom <= threshold\nstop when the return boundary is unknown\nКод выше — учебная формула. Она не содержит тариф, валюту, скидку или счёт реального проекта. Её задача — показать, какие переменные нужно назвать до арифметики. Если период или единица расходятся, сравнение двух инстансов не имеет смысла.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Большой инстанс дал лучший p95 | Latency сравнили без стоимости и риска возврата | Сопоставить SLO headroom, allocation, период и owner | Не выбирать размер; проверить меньший шаг |
| CPU utilisation ниже 40% | Использование приняли за цену и доступный запас | Найти billing unit, fixed base и лимиты ресурса | Не обещать экономию; запросить billing evidence |
| В quota ещё есть CPU | Разрешённый aggregate приняли за доступную ёмкость | Проверить request, limit, admission и cluster capacity | Отделить policy-проверку от performance-проверки |
| p95 почти достиг SLO | Следующий шаг хотят выбрать до stop condition | Вычислить headroom и проверить сигнал очереди | Остановить ветку и открыть отдельный разбор |
Рассмотрим учебные данные для одного workload и 24 часов. У текущего состояния 1,2 CPU, p95 248 ms и условная стоимость 11,424 единицы. Следующий предел поднимает request до 1,4 CPU, даёт p95 248 ms в измеренном окне и условную стоимость 11,856 единицы. Это не доказательство улучшения. Это маленький шаг, в котором изменено ограниченное число входов.
\nВторая точка использует 1,6 CPU. Её p95 равен 296 ms при SLO 300 ms, а сигнал queue-growth показывает рост очереди. Headroom равен 4 ms. Стоимость — 12,288 условной единицы за 24 часа. Ветка останавливается. Ещё больший request не становится объяснением причины очереди.
Третья точка резервирует 4 CPU и 6 GiB памяти. p95 снижается до 208 ms, а наблюдаемая загрузка CPU составляет 31%. Условная стоимость — 17,76 единицы за 24 часа. Такой результат показывает, что latency можно купить резервом. Он не доказывает, что резерв нужен, что цена рассчитана по реальному тарифу или что его безопасно уменьшить.
\n| Вариант | Performance | Стоимость за 24 ч | Риск и решение |
|---|---|---|---|
next-limit | p95 248 ms; запас 52 ms | 11,856 u | low; сравнить следующий малый шаг |
saturation-stop | p95 296 ms; запас 4 ms; queue-growth | 12,288 u | stop; сначала выяснить причину сигнала |
large-reservation | p95 208 ms; CPU 31% | 17,76 u | medium; не считать размер базовой стратегией |
Таблица не превращает три оси в общий score. Такой score потребовал бы отдельного владельца и обоснованной формулы. Здесь важнее сохранить отрицательный путь. Если performance хорош, но стоимость или возврат не объяснены, ответом становится остановка. Если стоимость ниже, но SLO почти нарушен, ответом также становится остановка.
\nПланировщик Kubernetes учитывает requests при размещении Pod. Limit задаёт отдельное ограничение исполнения. ResourceQuota ограничивает суммарные requests или limits в namespace. LimitRange может задать значения по умолчанию и минимальные или максимальные границы на этапе admission. Эти механизмы отвечают на разные вопросы: можно ли принять объект, сколько ресурса он просит и какие пределы действуют. Они не говорят, сколько стоит час работы и выдержит ли сервис нагрузку.
\nПоэтому фраза «в quota ещё есть 6 CPU» недостаточна. Она может означать, что объект проходит одну policy-проверку. Она не подтверждает свободную ёмкость кластера, отсутствие конкуренции, нужный запас по SLO или экономический эффект. Сначала проверьте policy. Потом проверьте runtime-сигналы. Затем сопоставьте allocation с разрешённой моделью billing.
\nStop condition защищает от решения под давлением. Пример правила: остановиться, если запас до SLO меньше 15 ms; остановиться при сигнале роста очереди; остановиться, если новый вариант меняет одновременно CPU, память, класс машины, сеть и concurrency; остановиться, если никто не назвал owner и границу возврата.
\nПредел считается следующим только тогда, когда он меняет один основной контролируемый параметр. Для него известны исходное значение, новое значение, период наблюдения и обратная граница. Если вместе с CPU меняются storage, network и commitment, это уже набор решений. Его нельзя объяснить одной строкой «увеличили ёмкость».
\nУчебные числа в этой статье не описывают конкретный кластер, provider, invoice, telemetry или production workload. Они показывают форму рассуждения. Нельзя переносить 11,856 u, 17,76 u, 31% CPU или p95 208 ms в бюджет и SLO другой системы. Нельзя выводить экономию из разницы между двумя условными суммами.
\nМодель также не учитывает автоматически cold start, autoscaling, burst, noisy neighbor, storage, network egress, commitments, скидки, налоги и стоимость сопровождения. Эти факторы могут изменить решение. Если хотя бы один из них влияет на выбор, добавьте его в scope или остановите сравнение.
\nЕсли после изменения сигнал ухудшился, отрицательный путь прост: не скрывайте его усреднением, верните исходную границу только в рамках разрешённого процесса и сохраните причину остановки. Если реальная команда не описала, кто и как выполняет возврат, статья не даёт команды на rollback. Она требует сначала закрыть этот пробел.
\nРазбор готов к решению, когда одна строка связывает workload, resource allocation, performance signal, billing unit, owner, stop condition и return boundary. Для выбранного шага известны изменяемое поле и период проверки. Policy не смешана с runtime-измерением. Стоимость либо подтверждена официальной формулой и собственными данными, либо явно помечена неизвестной.
\nПроверяемый результат — не согласие на самый большой инстанс. Это запись, в которой другой инженер может пересчитать условие, увидеть отрицательную ветку и ответить, почему выбран именно следующий предел. Если он не может назвать, что остановит сравнение, ёмкость ещё не готова к изменению.
\nНа разборе ёмкости команда видит знакомую картину: после перехода на 4 CPU p95 снизился, а резерв и счёт выросли. Карточка с 1,6 CPU уже показывала очередь и почти касалась SLO, поэтому большой инстанс выглядит очевидным ответом. Цена ошибки — закрепить дорогую конфигурацию без доказанного эффекта, принять одну удачную latency-точку за решение и потерять понятную границу возврата.
\nТезис: следующий размер выбирают не по лучшему p95, а по следующему проверяемому пределу. В одной карточке нужно связать workload, выделенный ресурс, сигнал производительности, единицу тарификации и условие остановки. Если хотя бы одно звено неизвестно, результатом должна быть остановка или отдельный разбор, а не заказ самого большого инстанса.
\nВопрос «сколько CPU нам нужно?» слишком короткий. Для проверки запишите: какой workload измеряем, в каком окружении, за какой период, какой SLO защищаем и что именно меняем. Для сервиса это может быть поток HTTP-запросов, число реплик, requests и limits контейнера, p95 задержки, ошибки и сигнал очереди. Для финансовой части добавьте billing unit: час виртуальной машины, CPU-hour, GiB-hour, запрос или трафик.
Так появляются разные, но связанные факты. Workload описывает работу. Allocation описывает обещанный системе резерв. Usage показывает фактическое потребление. Performance показывает результат для пользователя. Billing expression объясняет, как провайдер переводит использование или резерв в деньги. Низкий usage не заменяет allocation и не доказывает экономию. Зелёный SLO не объясняет тариф.
\nСитуация считается описанной только после фиксации scope. Запись «p95 стал лучше» не воспроизводится без маршрута, нагрузки, числа реплик, периода, версии и способа измерения. Запись «стоимость выросла на 20%» не воспроизводится без счёта, валюты, региона, скидки и правила агрегации.
\nВ Kubernetes эти слова отвечают на разные вопросы. Scheduler использует resource request при выборе узла. Limit задаёт ограничение выполнения контейнера: CPU может быть ограничен throttling, а превышение memory limit может привести к вмешательству OOM-механизма. ResourceQuota ограничивает суммарные ресурсы или объекты в namespace. LimitRange задаёт допустимые minimum, maximum и default для объектов на этапе admission.
\nНи один из этих механизмов не является тарифом. Если в namespace осталось 6 CPU quota, это говорит о политике допуска, но не о свободной ёмкости кластера, состоянии соседних workloads, задержке сервиса или цене часа. Если контейнер использует 31% CPU, это говорит об usage относительно выбранной базы, но не о том, сколько провайдер выставит в счёте. Сначала подтверждают policy и runtime, затем отдельно сверяют billing.
\n| Сигнал | Что он подтверждает | Чего он не подтверждает | Следующая проверка |
|---|---|---|---|
| CPU usage 31% | Наблюдаемое потребление в выбранном окне | Безопасный request и цену | Peak, throttling, p95 и billing unit |
| Quota ещё не исчерпана | Ограничение namespace не достигнуто | Доступный узел и запас по SLO | Admission, cluster capacity и saturation |
| p95 ниже SLO | Метрика укладывается в заданное окно | Экономичность и причинность улучшения | Одинаковый workload, период и allocation |
| Счёт вырос | Изменился итог billing-периода | Причину в одном ресурсе | SKU, rate, replicas, storage и traffic |
Практический вывод отрицательный: нельзя уменьшать request только потому, что средний CPU низкий, и нельзя увеличивать limit только потому, что quota это допускает. Для памяти редкий пик важнее среднего значения. Для CPU полезно посмотреть throttling и очередь. Для latency — выяснить, не сидит ли время в сети, блокировке или внешнем сервисе.
\nФормула нужна не для имитации счёта, а для проверки причинной цепочки. В учебной модели отдельно обозначим фиксированную часть, резерв CPU, резерв памяти и ставки. Если провайдер считает инстанс целиком, а не request контейнера, формула должна отражать инстанс. Если billing unit неизвестна, арифметику нужно остановить, а не заменить процентом utilization.
\nnode - <<'NODE'\nconst model = {\n fixedPerHour: 0.36,\n cpuHour: 0.08,\n memoryGiBHour: 0.01,\n hours: 24,\n replicas: 1\n};\n\nconst variants = [\n { name: 'current', cpu: 1.2, memoryGiB: 2, p95Ms: 248, queueGrowth: false },\n { name: 'next-limit', cpu: 1.4, memoryGiB: 2, p95Ms: 248, queueGrowth: false },\n { name: 'saturation-stop', cpu: 1.6, memoryGiB: 2, p95Ms: 296, queueGrowth: true },\n { name: 'large-instance', cpu: 4, memoryGiB: 6, p95Ms: 208, queueGrowth: false }\n];\n\nfor (const variant of variants) {\n const hourly = model.fixedPerHour\n + variant.cpu * model.cpuHour\n + variant.memoryGiB * model.memoryGiBHour;\n const daily = hourly * model.hours * model.replicas;\n const headroomMs = 300 - variant.p95Ms;\n console.log({ ...variant, hourly, daily, headroomMs });\n}\nNODE\nКоманда не обращается к кластеру, облаку или telemetry. Она воспроизводит только учебную арифметику на Node.js и печатает одинаковые результаты на машине с установленным Node. Для current получится 0,476 условной единицы в час и 11,424 за сутки; для next-limit — 0,492 в час и 11,808 за сутки. Это не валюта и не реальный invoice.
В рабочей карточке рядом с формулой должны лежать источник значения, единица и период. Ставка может быть tiered, а расход — агрегироваться по проекту или аккаунту. Поэтому сравнение двух конфигураций по одной строке «CPU × ставка» безопасно только как черновая модель, пока billing export или rate card не подтверждают остальные компоненты.
\nРассмотрим фиксированный workload за 24 часа, одну реплику и SLO p95 не выше 300 мс. Текущая точка использует 1,2 CPU и 2 GiB памяти. Следующий малый предел меняет CPU до 1,4, сохраняя память и окно. В обоих учебных случаях p95 равен 248 мс, поэтому улучшение от изменения не доказано: новая точка всего лишь остаётся сравнимой.
\nВариант saturation-stop с 1,6 CPU показывает p95 296 мс и рост очереди. Headroom равен 4 мс. Даже если quota позволяет такой request, сравнение нужно остановить. Условная стоимость по скрипту равна 12,192 за сутки. Следующий рост ресурса не объясняет, почему растёт очередь.
Большой вариант резервирует 4 CPU и 6 GiB. p95 равен 208 мс, CPU usage в карточке — 31%, а модельная стоимость — 17,76 за сутки. Это показывает, что latency можно получить за счёт большого резерва. Это не доказывает, что резерв нужен, что применена правильная ставка или что его можно без риска вернуть назад.
\n| Вариант | Ресурс | Performance | Стоимость за 24 ч | Решение |
|---|---|---|---|---|
current | 1,2 CPU; 2 GiB | p95 248 мс; запас 52 мс | 11,424 u | База сравнения |
next-limit | 1,4 CPU; 2 GiB | p95 248 мс; запас 52 мс | 11,808 u | Проверить соседний шаг |
saturation-stop | 1,6 CPU; 2 GiB | p95 296 мс; запас 4 мс; растёт очередь | 12,192 u | Остановить сравнение |
large-instance | 4 CPU; 6 GiB | p95 208 мс; CPU 31% | 17,76 u | Не выбирать по одному p95 |
Таблица не превращает performance, cost и risk в общий score. Для такого score пришлось бы заранее определить веса, владельца и допустимые trade-off. Здесь важна сохранённая отрицательная ветка: хороший p95 при неизвестной цене не даёт согласия, а низкая цена при headroom 4 мс не даёт разрешения продолжать.
\nRequest влияет на планирование, но не обещает, что приложение всегда получит ровно это количество CPU. Limit задаёт верхнюю границу исполнения и имеет собственные последствия. Quota и LimitRange могут не пропустить объект или назначить ему default, но не проверяют пользовательский SLO и не рассчитывают итоговый счёт.
\nИз этого следует последовательность владельцев. Платформенная policy подтверждает, что объект допустим. Scheduler и runtime показывают, что workload размещён и исполняется. Сервисные метрики показывают latency, ошибки и saturation. Финансовый источник показывает SKU, usage unit, base unit, tier и период. Один владелец или один dashboard не заменяют четыре вида evidence.
\nОсобенно опасна фраза «в quota есть запас, значит можно уменьшить инстанс». Quota — это потолок политики, а не рекомендация по размеру и не прогноз поведения. И обратная фраза тоже неверна: превышение request не означает автоматически, что надо покупать большой инстанс. Сначала нужно установить bottleneck и проверить малый соседний шаг.
\nStop condition защищает решение от давления одного удачного графика. Для учебного примера зададим остановку при headroom меньше 15 мс, при росте очереди, при неизвестной billing unit или при одновременной смене CPU, памяти, сети и класса машины. Эти условия не выбирают победителя. Они говорят, когда текущая гипотеза больше не проверяется тем же экспериментом.
\nВозврат тоже имеет границу. Запись «при ухудшении откатить» неполна, пока не названы владелец, изменение, разрешённый способ возврата и сигнал успешного восстановления. Эта статья не даёт команды rollback для чужого кластера: команда зависит от вашего deployment-процесса, прав и политики изменений. Если такой путь не описан, это причина остановить change, а не повод скрыть риск.
\nВсе значения в примере — учебные: 0,36, 0,08, 0,01, p95 248/296/208 мс, 31%, 1,2/1,4/1,6/4 CPU и 2/6 GiB. Они не описывают конкретный provider, cluster, invoice, telemetry или production workload. Их можно использовать, чтобы проверить форму расчёта и смысл stop condition, но нельзя переносить их в бюджет, SLO или capacity plan.
\nМодель не включает автоматически autoscaling, burst, cold start, noisy neighbor, storage, network egress, commitments, скидки, налоги, стоимость лицензий и сопровождения. Они могут изменить решение. Если хотя бы один фактор существенен, добавьте его в ту же billing-модель или остановите сравнение до получения данных.
\nИсточники Kubernetes подтверждают семантику ресурсов и policy, а источник Cloud Billing — форму описания SKU и тарифных ступеней. Ни один источник не знает вашу нагрузку, не обещает нужный p95 и не доказывает экономию. Доказательство для своего решения появляется только после выравнивания scope, измерения и финансового источника.
\nРешение о следующем пределе готово, когда одна карточка связывает workload, allocation, performance signal, billing unit, stop condition, owner и return boundary. В ней видно, какой параметр изменился, за какой период проведено сравнение и какой сигнал остановит эксперимент. Policy не выдана за runtime, usage не выдан за цену, а лучший p95 не выдан за универсальное решение.
\nЕсли хотя бы одного поля нет, полезный результат — не самый большой инстанс, а список недостающих доказательств. Такой отказ от поспешного изменения сохраняет возможность сравнить соседний шаг и делает следующую итерацию проверяемой.
\nГрафик показывает CPU 31%. p95 равен 208 ms при SLO 300 ms. Команда делает вывод: инстанс слишком большой, его можно уменьшить. Через неделю тот же график используют как доказательство экономии. Но в расчёте нет базовой ставки, единицы тарификации, memory allocation и правила quota. Ошибка стоит дороже одного неверного числа: можно получить очередь, нарушить SLO или принять учебную арифметику за счёт провайдера.
\nТезис простой: utilisation, capacity и cost отвечают на разные вопросы. Низкий процент означает только, что наблюдаемая нагрузка мала относительно выбранной базы. Цена зависит от формулы, периода, fixed части и единицы расчёта. Quota ограничивает допустимый объём. Limit задаёт верхнюю границу ресурса. Saturation показывает, что система приближается к отказу. Эти величины связаны, но ни одна не заменяет другую.
\nСначала запишите факт без вывода. Например: «CPU 31%, p95 208 ms, model cost 17,76 u/24h». Это одна строка наблюдений, а не рекомендация. Затем разделите вопросы. Почему latency хорошая? Сколько ресурса зарезервировано? Какая часть формулы фиксирована? Какую единицу умножает тариф? Есть ли stop condition для следующего изменения?
\nТакой порядок защищает от короткой, но неверной стрелки «низкая загрузка → уменьшить инстанс». Usage измеряют относительно allocation. Allocation может влиять на модель стоимости. Saturation зависит от нагрузки и запаса latency. Между наблюдением и действием лежат ещё resource policy, billing expression и граница риска.
\nObserved utilisation — измерение использования относительно выбранного ресурса. CPU 31% не говорит, был ли выбранный request разумным и сколько стоит час. Fixed resource — постоянная часть учебной формулы, например base 0,36 u/h. Она остаётся в scope, пока существует сама модель.
\nVariable resource — часть, которая меняется с allocation. В примере это CPU request и memory request, умноженные на учебные ставки за core-hour и GiB-hour. Billing unit — единица, к которой относится формула: час, GiB-hour или другая unit из конкретного контракта. Без неё разность двух чисел не имеет смысла.
\nQuota и limit задают ограничения ресурса, а не цену. Quota может ограничивать суммарные requests и limits в namespace. Limit задаёт верхнюю границу для workload. Saturation — сигнал, что очередь, p95 или retry risk приближаются к принятой границе. Saturation может остановить выбор, но не пересчитывает billing formula.
\nНиже — ограниченный учебный пример. Он не читает облачный счёт и не предлагает менять рабочую систему. Формула нужна, чтобы сделать промежуточные величины видимыми:
\nconst card = {\n basePerHour: 0.36,\n cpuRequest: 4,\n memoryGiB: 6,\n cpuUnitPerCoreHour: 0.08,\n memoryUnitPerGiBHour: 0.01,\n hours: 24,\n};\n\nconst hourly =\n card.basePerHour +\n card.cpuRequest * card.cpuUnitPerCoreHour +\n card.memoryGiB * card.memoryUnitPerGiBHour;\n\nconst modelCost = hourly * card.hours;\n// 17.76 model units for this fixed educational card\nЗначение 17,76 — результат именно этой формулы. Оно не является тарифом, счётом или прогнозом. Если поменять CPU с 4 до 1,6, нужно сравнить не только cost. Нужны одинаковые period и unit, а также p95, workload, memory, quota и operational risk. В соседней учебной карточке 1,6 CPU и 2,4 GiB дают 0,512 u/h. При p95 296 ms и сигнале queue-growth меньшая цена не доказывает безопасное уменьшение.
Marginal cost — разность двух сопоставимых формул. Например, переход с 1,4 до 1,6 CPU при ставке 0,08 u/core-hour добавляет (1,6 - 1,4) × 0,08 × 24 = 0,384 u/24h. Это размер следующего вопроса, а не команда увеличить request. Если вместе с CPU меняются memory, region, commitment или shared base, одна разность уже не объясняет решение.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| CPU ниже 40% | Usage сравнили с allocation и сразу назвали ресурс лишним | Сверить request, workload, p95 и период | Не менять размер; сформулировать следующий контролируемый вопрос |
| Model cost выросла | Изменились fixed base, unit или allocation | Разложить формулу по полям и одинаковому периоду | Проверить billing contract; не называть число счётом |
| Quota ещё не исчерпана | Quota приняли за доступную безопасную capacity | Проверить aggregate rule, admission и saturation | Остановить изменение, если p95 или очередь уже у границы |
| Большой инстанс даёт лучший p95 | Одну метрику использовали вместо performance, cost и risk | Сравнить соседний limit и return boundary | Сохранить stop и запросить отдельное решение владельца |
| Два расчёта не совпадают | Смешаны scope, unit или interval | Сверить workload, resource, formula и часы | Не сравнивать карточки до выравнивания входов |
Quota отвечает на вопрос «какой aggregate объём policy допускает?». Она не отвечает на вопрос «выдержит ли сервис следующий request?». В Kubernetes quota может ограничивать суммарные requests и limits, но сама по себе не обещает свободную ёмкость кластера. LimitRange может задать default, minimum или maximum на admission. Это правила формы ресурса, а не доказательство latency.
\nВ учебной карточке quota равна 6 CPU, а request большого варианта равен 4 CPU. Из этого нельзя вывести, что сервис безопасно выдержит 4 CPU или что его следует уменьшить. Если p95 почти касается SLO и растёт очередь, saturation требует остановки даже при доступной quota. Если p95 стабилен, это всё равно не доказывает стоимость: нужна отдельная billing expression.
\nХорошая проверка должна уметь остановиться. Для large-instance-unjustified учебная ветка видит CPU 31%, p95 208 ms и большую reservation. Она возвращает stop-and-compare-smaller-limit. Ветка не уменьшает request и не создаёт rollback. Она запрещает два одинаково слабых вывода: «низкая загрузка означает лишний ресурс» и «лучший p95 оправдывает самый большой ресурс».
Остановка нужна и при подмене входа. Если отчёт содержит неизвестное поле вроде invoice, другой scope, sparse array или изменённую model cost, его нельзя молча принять. Сначала нужно вернуть форму к согласованному контракту. Если unit или период не подтверждены, вычисление marginal cost прекращается. Отрицательный путь защищает границу примера, а не реальный API и не рабочую систему.
Все числа в примере учебные. Модель не видит provider invoice, SKU, скидки, commitment, tax, network charge, storage, cluster state, runtime, trace или реальную нагрузку. Она не знает owner, полномочия, rollback policy и последствия изменения. Поэтому статья не обещает savings, не выбирает размер instance и не переносит unit из одного provider в другой.
\nПроверка готова, если читатель может ответить на четыре вопроса: что наблюдалось; какая формула и unit применены; какое правило остановит изменение; какой реальный источник подтвердит следующий шаг. Дополнительно должны быть видны request, limit, quota, p95 и период. Если ответ держится только на CPU percent или на красивом model cost, вывод не готов к operational решению.
\nСимптом виден сразу: сервис держит p95 ниже целевого значения, а счёт за инфраструктуру растёт. На графике CPU занято 35%, поэтому первая гипотеза звучит логично: ресурсов слишком много. Но процент загрузки описывает только наблюдаемое использование. Он не говорит, сколько ресурса зарезервировано, за какую единицу считает провайдер и какая часть стоимости фиксирована. Если перепутать эти слои, команда уменьшит резерв без доказательства безопасности или, наоборот, купит большой инстанс без объяснимого эффекта.
\nГлавный вопрос этой статьи — почему низкая загрузка не означает низкую стоимость. Ответ можно проверить цепочкой workload → resource → billing unit → decision. Сначала фиксируем поток работы и его качество, затем отделяем выделение ресурса от фактического использования, после этого читаем модель тарификации и только в конце выбираем изменение. Числа ниже учебные: они показывают метод расчёта, а не счёт конкретного облака.
Workload — поток работы: запросы в секунду, размер сообщения, фоновые задания и период наблюдения. Resource allocation — то, что система выделила или зарезервировала: число реплик, CPU и память в request, limit, размер виртуальной машины или другой контролируемый параметр. Usage — фактическое потребление в конкретном окне. Billing unit — единица, к которой привязан тариф: час инстанса, CPU-hour, GiB-hour, запрос, байт или составная модель.
\nЭти величины связаны, но не заменяют друг друга. Низкий usage может быть полезным запасом для пика. Высокий usage может не менять счёт при фиксированной оплате инстанса. SLO отвечает за доступность или задержку, а не за цену. Quota отвечает за допустимый агрегат в политике платформы, а не за свободную ёмкость и не за финансовый результат.
\n| Слой | Пример значения | Что он показывает | Чего он не доказывает |
|---|---|---|---|
| Workload | 120 RPS, пик 180 RPS | Какой поток нужно обслужить и в каком окне | Сколько стоит ресурс без правила тарификации |
| Allocation | 2 реплики, request 1,2 CPU | Какой резерв заявлен системе | Фактическое потребление и цену без billing unit |
| Usage | CPU 35%, память 61% | Наблюдаемое использование за период | Что можно безопасно убрать на пике |
| SLO | p95 < 300 мс | Качество обслуживания в заданной границе | Экономичность решения |
| Billing unit | CPU-hour и GiB-hour | К чему применяются ставки и ступени | Реальную сумму без тарифа, региона и счётных данных |
Практическая ошибка начинается с подмены: «CPU 35%, значит оплачиваем 35%». Это может быть верно только при конкретном контракте тарификации. Если провайдер продаёт целый инстанс, график использования не уменьшит его часовую цену. Если тариф зависит от резервирования, процент usage также не станет входом формулы автоматически.
\nВ Kubernetes эти границы хорошо видны. Scheduler использует resource request при выборе узла: сумма requests должна помещаться в доступную ёмкость узла, даже если фактическая загрузка сейчас мала. Limit задаёт другую границу исполнения; для CPU он связан с throttling, а превышение memory limit может привести к OOM-убийству при давлении на память. Следовательно, request, limit и usage нельзя складывать в одну метрику «занято».
\nResourceQuota ограничивает агрегатное потребление в namespace. Если создание или обновление объекта нарушает квоту, control plane может отклонить запрос с HTTP 403. Это проверка политики, а не доказательство того, что в кластере есть свободный узел или что вариант дешевле. Наличие места до quota не отменяет проверки scheduler, runtime-сигналов и биллинга.
\nНа другой платформе названия будут иными, но вопрос остаётся тем же: кто принимает решение о размещении, кто ограничивает исполнение, кто измеряет usage и кто выставляет счёт. Нельзя переносить Kubernetes-семантику на managed database или serverless-функцию без документации конкретного сервиса.
\nВозьмём один сервис с двумя репликами. На реплику заявлены request 1,2 CPU и 2 GiB памяти. Для учебной модели считаем, что фиксированная часть равна 0,36 условной единицы за час, CPU стоит 0,08 за CPU-hour, а память — 0,01 за GiB-hour. Эти ставки специально обозначены как условные: в реальном проекте их нужно заменить на тариф, регион, скидку, commitment и правило распределения общих затрат.
\nconst model = {\n replicas: 2,\n requestCpu: 1.2,\n requestMemoryGiB: 2,\n fixedPerHour: 0.36,\n cpuHour: 0.08,\n memoryGiBHour: 0.01,\n};\n\nconst perReplicaHour =\n model.fixedPerHour\n + model.requestCpu * model.cpuHour\n + model.requestMemoryGiB * model.memoryGiBHour;\nconst daily = perReplicaHour * 24 * model.replicas;\n\nconsole.log({\n perReplicaHour,\n daily,\n deltaAfterResize: (0.36 + 1.4 * 0.08 + 2.2 * 0.01)\n * 24 * model.replicas - daily,\n});\n// { perReplicaHour: 0.476, daily: 22.848, deltaAfterResize: 0.864 }\nРасчёт даёт 0,476 условной единицы за реплику-час и 22,848 за сутки для двух реплик. Если request изменить на 1,4 CPU и 2,2 GiB, модель даст 23,712 за сутки; разница составит 0,864 условной единицы. Это воспроизводимая арифметика, но не обещание экономии или роста счёта. Она станет рабочим расчётом только после проверки, что тариф действительно считает reservation, а не usage, узел, инстанс или другой объект.
\nGoogle Cloud Billing Catalog, например, описывает для SKU usage unit, base unit и tiered rates. Само наличие этих полей ещё не выбирает нужный SKU. Нужно сопоставить ресурс, регион, период, уровень тарифа и способ экспорта usage с конкретным счётом. При неизвестной ставке правильный результат — «стоимость не установлена», а не ноль.
\nПредставим учебное наблюдение: p95 равен 235 мс при SLO 300 мс, CPU usage — 35%, память — 61%. Эти данные говорят, что в выбранном окне запас по задержке равен 65 мс и среднее потребление CPU невелико. Они не говорят, что request можно уменьшить: пик мог быть выше, память могла быть близка к пределу, а тариф мог быть фиксированным.
\nДля причинного сравнения меняйте один основной вход за раз и сохраняйте одинаковые условия. Если одновременно уменьшить request, число реплик, класс машины и лимит concurrency, после результата нельзя будет понять, какой рычаг помог. Если окно содержит только спокойный час, вывод не покрывает ежедневный пик. Если billing период и окно метрик различаются, это разные наблюдения, а не одна дельта.
\n| Симптом | Гипотеза | Проверка | Вывод до изменения |
|---|---|---|---|
| CPU usage ниже 40%, счёт не меняется | Оплата фиксирована за инстанс | Найти billing unit и строку SKU в счёте | Низкий usage не даёт основания уменьшать ресурс |
| p95 зелёный, replicas растут | Запас куплен масштабированием | Сравнить peak RPS, replicas, allocation и период | Отделить надёжность от стоимости резерва |
| Pod Pending, quota ещё не исчерпана | Не хватает ёмкости узлов или сработала другая политика | Проверить Events, requests, allocatable и LimitRange | Quota не равна свободной ёмкости |
| CPU низкий, p95 высокий | Ожидание сети, блокировка или throttling другого слоя | Сопоставить latency, очередь, ошибки и trace | Не уменьшать CPU по одному графику |
| После resize цена не совпала с моделью | Изменён не тот объект тарификации | Сверить SKU, регион, tier, скидку и количество часов | Расчёт вернуть в статус гипотезы |
Метрика без периода — число без проверяемого смысла. OpenTelemetry описывает metric event как измерение, время его фиксации и связанные метаданные. Поэтому рядом с p95 или usage нужно хранить хотя бы сервис, окружение, регион, версию, workload, окно и единицу. Иначе сравнение «до/после» может оказаться сравнением разных маршрутов или разных пиков.
\nДля CPU полезно видеть usage вместе с throttling и очередью. Для памяти — пик, OOM-события и рабочий набор. Для сервиса — p95/p99, ошибки и поток запросов. Набор зависит от системы: эти поля не являются универсальным дашбордом. Смысл проверки в том, чтобы связывать качество с нагрузкой и выделением ресурса, а не объявлять причиной первую зелёную или красную линию.
\nЕсть и стоимость самой наблюдаемости. В OpenTelemetry число уникальных комбинаций атрибутов определяет cardinality; атрибуты вроде user ID или необработанного URL могут раздувать состояние метрик. Поэтому при добавлении labels нужно одновременно проверить объём хранения, лимиты backend и полезность разреза. «Добавим больше измерений» тоже имеет ресурсную и финансовую цену.
\nЭта модель не заменяет capacity planning, нагрузочное тестирование, договор с облачным провайдером или финансовый разбор. Она не учитывает автоматически autoscaling, cold start, сетевой egress, хранилище, резервирование, налоги, скидки, простой, стоимость лицензий и сопровождения. Каждый фактор может изменить итоговую цену или безопасный ресурсный предел.
\nУчебные значения 120 RPS, 180 RPS, 235 мс, 1,2 CPU, 2 GiB, 0,36 и остальные числа не описывают реальный сервис. Их нельзя переносить в бюджет, SLO или заявку на изменение. Иллюстрация также не является инвойсом. Для реального решения нужны собственные метрики, разрешённый тариф и подтверждённое правило распределения общих расходов.
\nНельзя делать вывод о свободной ёмкости из одной квоты, о цене — из одного usage-графика, а о безопасности уменьшения — из среднего значения. Если период, единица или владелец неизвестны, остановка — полноценный результат диагностики. Следующий шаг должен вернуть недостающий факт, а не маскировать его приблизительным числом.
\nРазбор готов к решению, когда в одной записи видны workload, окно, allocation, usage, SLO, billing unit, формула, stop condition и обратная граница. Другой инженер должен воспроизвести арифметику, понять, какой параметр меняется, и назвать сигнал, который остановит эксперимент. Если он видит только «CPU 35%» и «p95 зелёный», стоимость и безопасность изменения ещё не доказаны.
\nТакой порядок не обещает минимальный счёт. Он делает причину изменения проверяемой: можно увидеть, был ли куплен резерв, что именно считается провайдером и какой риск принимает команда. Низкая загрузка после этой проверки может стать аргументом для уменьшения ресурса, но только вместе с пиком, политикой, качеством и подтверждённой моделью тарификации.
\nСервис держит p95 на уровне 235 мс при целевом SLO 300 мс, но резерв CPU и памяти растёт каждую неделю. На графике задержка зелёная. В счёте появляется лишняя базовая ёмкость. Если принять зелёный SLO за доказательство эффективности, команда платит за резерв, которого не связывает ни с нагрузкой, ни с единицей тарификации.
\nОшибка возникает в месте, где смешивают четыре разные величины: поток запросов, выделенный ресурс, фактическое использование и цену. SLO отвечает за задержку или долю успешных запросов. Он не объясняет, сколько CPU зарезервировано и как провайдер выставляет счёт. Тезис статьи простой: стоимость ёмкости проверяют цепочкой workload → resource → billing unit → stop condition. Если звено пропущено, число на дашборде остаётся симптомом.
Workload описывает поток работы: requests per second, размер сообщения, число фоновых задач и период измерения. Resource описывает выделение: replicas, CPU request, memory request, limit и квоту. Usage показывает фактическое потребление за интервал. Billing unit говорит, за что считают деньги: час инстанса, CPU-hour, GiB-hour, запрос, байт или составную единицу.
\nЭти поля связаны, но не заменяют друг друга. Низкий usage не доказывает, что request можно уменьшить: запас может защищать от пика. Высокий usage не доказывает рост цены: тариф может быть фиксированным. Quota ограничивает суммарное потребление пространства имён, но сама по себе не является счётом. Сначала нужно назвать роль каждого значения.
\nПредставьте один сервис с 120 запросами в секунду в обычный час и 180 в пиковый. Его p95 равен 235 мс. На каждый экземпляр задано 1,2 CPU и 2 ГиБ памяти, а предел равен 1,6 CPU и 3 ГиБ. Для учебного расчёта возьмём фиксированную часть 0,36 условной единицы в час, 0,08 за CPU-hour и 0,01 за GiB-hour.
\nЕсли модель считает зарезервированный request, стоимость часа равна 0.36 + 1.2 * 0.08 + 2 * 0.01 = 0.476. За 24 часа это 11,424 условной единицы. Это не валюта и не счёт провайдера. Числа нужны, чтобы показать границу: формула использует allocation rule, а не процент CPU на графике. При двух репликах результат удваивается. При изменении тарифа меняется billing unit, а не SLO.
Теперь увеличим request до 1,4 CPU и 2,2 ГиБ. p95 в учебном сценарии остаётся ниже 300 мс, но дневная стоимость становится (0.36 + 1.4 * 0.08 + 2.2 * 0.01) * 24 = 11.856. Разница равна 0,432 условной единицы за сутки на одну реплику. Нельзя назвать её экономией или потерей в реальной среде: для этого нужны настоящий тариф, число реплик, период, скидки, shared overhead и подтверждённая нагрузка.
const card = {\n scope: 'checkout-api',\n period: '24h',\n workload: { baselineRps: 120, peakRps: 180, p95Ms: 235, sloMs: 300 },\n resource: { replicas: 2, requestCpu: 1.2, requestMemoryGiB: 2, limitCpu: 1.6 },\n billing: { fixedPerHour: 0.36, cpuHour: 0.08, memoryGiBHour: 0.01 },\n stop: 'p95 headroom < 10 ms or saturation is observed'\n};\n\nconst hourly = card.billing.fixedPerHour\n + card.resource.requestCpu * card.billing.cpuHour\n + card.resource.requestMemoryGiB * card.billing.memoryGiBHour;\nconst daily = hourly * 24 * card.resource.replicas;\nconsole.log({ hourly, daily, sloHeadroomMs: card.workload.sloMs - card.workload.p95Ms });\nКод — учебный пример. Он не читает Kubernetes API, облачный биллинг или метрики. В нём намеренно видны период, replicas и правило тарификации. В рабочем контуре эти значения нужно получить из разрешённых источников и сохранить вместе с timestamp. Если источник не различает request и usage, расчёт нельзя выдавать за стоимость резервирования.
\n| Симптом | Причина-кандидат | Проверка | Действие |
|---|---|---|---|
| SLO зелёный, расход растёт | Увеличили request или replicas | Сравнить deployment, период и число реплик | Разделить изменение ресурса и изменение нагрузки |
| CPU usage низкий, уменьшение не проходит | Нужен запас на пик или действует quota | Сверить peak RPS, p95, eviction и admission policy | Проверить один меньший request в одинаковом интервале |
| Процент CPU вырос, цена не изменилась | Фиксированная тарификация | Прочитать billing unit и rate card | Не считать usage заменой счёта |
| Цена сравнивается у двух сервисов | Разные scope или периоды | Сверить регион, replicas, shared overhead и окно | Остановить сравнение до выравнивания входа |
| Новый limit отклонён | Quota или LimitRange запрещает значение | Проверить policy и сообщение admission | Сначала исправить контракт ресурса, затем считать стоимость |
Таблица задаёт порядок проверки, а не автоматическое решение. Один симптом может иметь несколько причин. Проверка должна исключить хотя бы очевидные альтернативы. Например, низкий CPU при большом p95 может указывать на ожидание сети или блокировку, а не на свободный запас вычислений. Уменьшение request в таком случае меняет риск, но не устраняет задержку.
\nВ Kubernetes scheduler учитывает requests при размещении Pod. Limits задают отдельные ограничения выполнения. Поэтому запись «сервис использует 60% CPU» не говорит, что он занимает 60% оплачиваемой ёмкости. Нужно знать, от какой базы рассчитан процент и какую величину использует финансовая модель.
\nПамять требует отдельной осторожности. Краткий средний usage может скрыть редкий пик. Если процесс получает OOMKilled, зелёный средний график не спасает запросы. Для памяти полезнее сверять peak, рабочий набор, ошибки и время окна. Для CPU важны throttling, очередь и p95. Один общий порог utilisation не подходит обоим ресурсам.
\nQuota и LimitRange задают допустимый диапазон на уровне политики. Они могут отклонить Pod с большим request или назначить значения по умолчанию. Но policy не знает цену часа инстанса. Она проверяет допустимость ресурса, а billing-система применяет свою формулу. Смешение этих слоёв рождает ложный вывод: «quota равна capacity» или «limit равен тарифу».
\nСравнение нельзя продолжать бесконечно. До расчёта назовите условие остановки. Для latency это может быть запас p95 до SLO. Для ресурса — сигнал saturation, throttling или нехватка памяти. Для стоимости — максимально допустимая дневная дельта. Для политики — отказ admission. Stop condition не говорит, какой вариант выбрать. Он говорит, когда следующий вариант нельзя считать продолжением того же эксперимента.
\nВ учебном примере зададим запас 10 мс. При p95 235 мс запас до SLO равен 65 мс, и сравнение двух соседних reservation допустимо как расчётная иллюстрация. Если p95 стал 295 мс, запас равен 5 мс. Следующий рост request уже требует отдельного решения о надёжности. Нельзя спрятать этот риск в таблице стоимости.
\nТакая карточка не заменяет capacity planning. Она не моделирует autoscaling, cold start, сеть, хранилище, резервирование, скидки, burst-кредиты, простои и общие узлы. Она не доказывает, что меньший request безопасен. Она только не даёт связать цену с SLO напрямую.
\nОтрицательный путь важнее удачного расчёта. Если метрики собраны за разные окна, остановитесь. Если тариф относится к узлу, а request — к Pod, не складывайте их без правила распределения. Если неизвестно число реплик в пике, не называйте дневную стоимость точной. Если policy изменилась после замера, пересчитайте вход. Не подставляйте ноль вместо неизвестного поля: это превращает отсутствие данных в ложную экономию.
\nНе стоит запускать уменьшение ресурса только потому, что модель дала меньшую цифру. Сначала проверьте peak и p95 на том же окне, затем выполните изменение по обычному безопасному процессу, а после него сравните ошибки, задержку, throttling и billing. В этой статье нет production-результата и нет обещания экономии. Есть только критерии, по которым такой результат можно будет подтвердить.
\nПроверка готова, если второй инженер может по одной карточке ответить на пять вопросов: какой workload измеряли; какой resource зарезервирован; что означает usage; какая billing unit применена; при каком сигнале сравнение останавливается. Формула должна воспроизводиться на том же входе. Ссылки на тариф и политику должны быть доступны владельцу. При отсутствии любого ответа статус должен быть «данных недостаточно», а не «дешевле».
\nСервис обрабатывает почти столько же запросов, что и месяц назад. Его p95 — 235 мс при целевом значении 300 мс, ошибки не выросли, но ежемесячный счёт за вычисления увеличился. На дашборде всё зелёное, поэтому первая гипотеза звучит неправильно: «раз SLO выполнен, система уже достаточно экономна».
\nЗелёный SLO доказывает только соответствие выбранному показателю и окну измерения. Он не говорит, сколько ресурсов зарезервировано, сколько фактически потреблено и по какой единице провайдер выставляет счёт. Разберём учебный сценарий, в котором сначала разделим эти величины, затем сверим их командами Kubernetes и посчитаем два соседних варианта. Результат — не обещание экономии, а воспроизводимый способ понять, каких данных не хватает для решения.
\nДо изменения Deployment запишите четыре значения. Workload — сколько работы приходит: requests per second (RPS), размер сообщений, фоновые задачи и окно наблюдения. Allocation — что системе выделено: число реплик, CPU и memory в requests и limits. Usage — фактическое потребление за тот же интервал. Billing unit — что именно превращает ресурс в деньги: час виртуальной машины, CPU-hour, GiB-hour, запрос, байт или составной тариф.
Каждый вопрос имеет другого владельца. Нагрузку подтверждает владелец сервиса или аналитики, allocation — манифест и платформа, usage — система метрик, billing unit — тариф и счёт провайдера. Если два источника используют одно слово «CPU», это ещё не делает их значения сопоставимыми. В частности, низкий usage не доказывает, что request можно без риска уменьшить, а высокий usage не доказывает, что цена вырастет: тариф может считать зарезервированный ресурс или фиксированный инстанс.
\nSLI (service level indicator) — измеряемый показатель услуги, например задержка или доля успешных запросов. SLO (service level objective) — целевое значение этого показателя. Если SLO сформулирован как «99% запросов быстрее 300 мс за сутки», он отвечает на вопрос о качестве для пользователя. Он не отвечает на вопрос, оплачивается ли CPU по факту потребления, по резерву или как часть фиксированного инстанса.
\nПроцентиль тоже требует контекста. p95 в 235 мс может быть рассчитан по одному региону и только по успешным запросам, а счёт — по всем регионам и часам работы. Поэтому рядом с p95 храните окно, фильтр запросов, регион и число реплик. Иначе зелёный показатель создаёт ложное ощущение сравнимости: мы сопоставляем качество одного среза с ценой другого.
\nУ SLO есть полезная роль в решении о ёмкости: он задаёт границу, после которой уменьшение ресурса нельзя считать приемлемым. Но граница должна включать и другие сигналы — ошибки, очередь, throttling CPU, пики памяти и время восстановления. SLO — контроль качества сервиса, а не доказательство низкой себестоимости.
\nВ Kubernetes scheduler учитывает requests, когда выбирает узел: сумма запросов размещённых контейнеров должна помещаться в доступную ёмкость узла. Фактическое потребление в момент планирования может быть ниже. Это объясняет, почему свободный CPU на графике не превращается автоматически в возможность уменьшить request.
Limit задаёт другой контракт. Для CPU это верхняя граница, применение которой может проявляться throttling. Для памяти превышение лимита может закончиться убийством процесса механизмом OOM. Поэтому предел нельзя использовать как синоним usage или стоимости. В некоторых конфигурациях admission-механизм подставляет request из limit, если request не задан; итоговый объект нужно читать после применения политик, а не угадывать по исходному YAML.
ResourceQuota ограничивает суммарное потребление ресурсов и объектов в namespace. Квота может не пропустить новый Pod или изменение Deployment, но не сообщает цену часа. Она отвечает на вопрос «разрешён ли такой объём в namespace», а billing отвечает на вопрос «как этот объём тарифицируется». Эти проверки полезно выполнять вместе, но не смешивать их результаты.
Сначала снимите allocation и политику, затем usage, и только после этого сравнивайте с инвойсом. Команды ниже не меняют кластер. Переменные задают namespace и Deployment; kubectl top требует установленного Metrics Server и показывает текущую оценку usage, а не платёжный документ.
NS=checkout\nDEPLOY=checkout-api\n\n# Фактические requests/limits контейнеров в шаблоне Deployment\nkubectl -n \"$NS\" get deploy \"$DEPLOY\" -o jsonpath='{range .spec.template.spec.containers[*]}{.name}{\"\\t\"}{.resources.requests.cpu}{\"\\t\"}{.resources.requests.memory}{\"\\t\"}{.resources.limits.cpu}{\"\\t\"}{.resources.limits.memory}{\"\\n\"}{end}'\n\n# Реплики и доступное состояние rollout\nkubectl -n \"$NS\" get deploy \"$DEPLOY\" -o wide\n\n# Квота namespace\nkubectl -n \"$NS\" get resourcequota\n\n# Текущий usage; это не billing unit\nkubectl -n \"$NS\" top pod -l app=\"$DEPLOY\" --containers\nСохраните вывод вместе с timestamp, окном метрик и commit манифеста. Если label-селектор или Metrics Server в конкретном кластере устроены иначе, команда не даст данных — это повод уточнить интерфейс, а не подставить нули. Для счёта дополнительно сохраните регион, размер инстанса, число часов, скидку и строку тарифа. Без этих полей стоимость нельзя честно распределить на Pod.
\nВозьмём две реплики одного сервиса. В первом варианте каждая получает request 1,2 vCPU и 2 GiB памяти. Во втором — 1,4 vCPU и 2,2 GiB. Фиксированная часть модели — 0,36 условной единицы на реплику в час, CPU — 0,08 за vCPU-hour, память — 0,01 за GiB-hour. Все числа вымышлены для проверки арифметики; их нельзя переносить в счёт конкретного облака.
\nЦена одной реплики в час для первого варианта равна 0,36 + 1,2 × 0,08 + 2 × 0,01 = 0,476. Для двух реплик за сутки — 0,476 × 24 × 2 = 22,848. Во втором варианте получаем 0,36 + 1,4 × 0,08 + 2,2 × 0,01 = 0,494, или 0,494 × 24 × 2 = 23,712. Разница модели — 0,864 условной единицы за сутки.
const scenario = {\n replicas: 2,\n hours: 24,\n fixedPerReplicaHour: 0.36,\n cpuHour: 0.08,\n memoryGiBHour: 0.01,\n variants: [\n { name: 'current', requestCpu: 1.2, requestMemoryGiB: 2.0, p95Ms: 235 },\n { name: 'larger-request', requestCpu: 1.4, requestMemoryGiB: 2.2, p95Ms: 235 }\n ]\n};\n\nfunction dailyCost(variant) {\n const replicaHour = scenario.fixedPerReplicaHour\n + variant.requestCpu * scenario.cpuHour\n + variant.requestMemoryGiB * scenario.memoryGiBHour;\n return replicaHour * scenario.hours * scenario.replicas;\n}\n\nfor (const variant of scenario.variants) {\n console.log(variant.name, {\n dailyModelCost: Number(dailyCost(variant).toFixed(3)),\n sloHeadroomMs: 300 - variant.p95Ms\n });\n}\nОжидаемый вывод: current — 22.848 и запас 65 мс; larger-request — 23.712 и тот же запас. Код показывает связь только внутри заранее выбранной модели. Он не учитывает autoscaling, стоимость узла, shared overhead, storage, сеть, налоги, резервирование и скидки. Если провайдер считает целый инстанс, подстановка CPU request в формулу будет неверной даже при безошибочной арифметике.
| Наблюдение | Гипотеза | Что сверить | Следующее действие |
|---|---|---|---|
| SLO зелёный, счёт вырос | Изменились requests, реплики или тариф | Манифест, число реплик, регион, строку инвойса | Разделить изменение allocation и изменение цены |
| Usage CPU ниже request | Request оставляет запас на пик или задаёт размещение | Пиковый RPS, очередь, throttling, расписание | Сравнить один меньший request на том же окне |
| Memory usage в среднем низкий | Редкий пик скрыт агрегацией | Максимум, OOM, eviction и длительность окна | Не уменьшать memory только по среднему |
| Цена не изменилась после роста usage | Тариф фиксирован по инстансу или tier | Billing unit и границы tier | Не заменять счёт метрикой usage |
| Новый Pod не создаётся | Quota, LimitRange или admission отклонили объект | Событие namespace и итоговый Pod spec | Исправить допустимый диапазон до расчёта цены |
Таблица задаёт порядок исключения гипотез, а не готовую оптимизацию. Низкий CPU при высоком p95 может означать ожидание внешнего сервиса или блокировку. В таком случае меньший request не устраняет причину задержки и лишь уменьшает запас. Наоборот, рост стоимости при неизменном usage может быть полностью ожидаемым, если оплата идёт за фиксированный инстанс.
\nСравнивайте соседние варианты, пока у эксперимента остаётся одинаковый workload и понятен риск. Для сценария задайте заранее: SLO p95 не выше 300 мс, error rate не растёт, нет нового throttling, memory peak не приближается к лимиту, а дневная дельта модели не выше согласованного порога. Это не универсальные значения, а поля конкретного эксперимента.
\nВ учебном расчёте p95 235 мс оставляет запас 65 мс. Если после изменения он стал 295 мс, запас сократился до 5 мс, даже если модель обещает экономию. Если выросли ошибки или появился OOM, эксперимент нужно остановить независимо от зелёного среднего графика. Граница должна быть измерима другим инженером: «кажется, стало хуже» для неё недостаточно.
\nСхема подходит для первичной диагностики расхождения между качеством сервиса и затратами на вычисления. Она не заменяет capacity planning, load test или финансовую сверку. Она не моделирует нелинейный autoscaling, cold start, сетевой трафик, хранилище, стоимость control plane, shared nodes и минимальную плату провайдера.
\nЕё нельзя применять как доказательство, что меньший request безопасен. Нужны пиковые данные, запас по SLO, проверка memory pressure и обычная процедура изменения с откатом. Не переносите формулу между провайдерами без подтверждённого rate card. Не сравнивайте usage из одного часового окна с инвойсом за календарный месяц и не распределяйте стоимость узла на Pod без правила аллокации.
\nОтрицательный результат тоже полезен. Если окна, регионы, реплики или единицы различаются, статус проверки — «данных недостаточно». Это лучше, чем назвать нулевую неизвестную величину экономией. Точный вывод появляется только тогда, когда второй инженер может повторить сбор входов, расчёт и проверку stop condition.
\nРешение можно передавать на изменение, если по одной карточке видны workload, allocation, usage и billing unit; у каждого поля есть источник и окно; формула воспроизводится; SLO и технические stop condition определены; quota и admission не блокируют вариант; владелец тарифа подтвердил финансовую часть. Если хотя бы один пункт отсутствует, разрешённый результат — продолжить сбор данных, а не объявить систему дешёвой.
\nЧерез несколько месяцев после принятия ADR команда открывает его перед изменением сервиса. В документе описан синхронный экспорт для небольшого запроса. В коде уже появился фоновый worker и endpoint со статусом операции. Один разработчик предлагает просто исправить старый текст: заменить «синхронный экспорт» на «фоновый экспорт» и оставить прежнюю дату.
\nСимптом виден сразу: ADR, код и текущий вопрос описывают разные границы системы. Цена ошибки выше, чем кажется. Команда теряет причину прежнего выбора, не видит принятые компромиссы и может повторно принять уже отвергнутый вариант. При аварии становится неясно, к какому решению возвращаться. При проверке доступа или миграции нельзя отличить старое обязательство от нового предложения.
\nТезис прост: ADR фиксирует принятое решение в конкретном контексте, но не проверяет систему сам. Когда меняется assumption или constraint, старую запись нужно сохранить, а новое решение оформить как successor. Только после его принятия старый ADR можно связать с ним статусом Superseded. Так история остаётся читаемой, а проверка не маскируется под редактирование документа.
\nADR отвечает на четыре вопроса: какая проблема наблюдалась, какое решение выбрали, какие альтернативы рассмотрели и какие последствия приняли. Поля Status, Context, Decision и Consequences образуют минимальный каркас. В расширенном шаблоне рядом появляются владельцы, факторы выбора и способ проверки.
\nЗапись не является приказом навсегда. Она говорит: «при этих условиях мы выбрали этот вариант». Условие может измениться из-за нового контракта, класса данных, требования к времени ответа, стоимости отказа или исчезновения владельца. Сам факт изменения кода ещё ничего не доказывает. Нужно показать, какое условие перестало выполняться.
\nПолезно разделять три объекта. ADR хранит rationale и границу решения. Тест или запрос к метрикам даёт evidence по конкретному вопросу. План изменения описывает выкладку, откат и наблюдение. Ни один объект не заменяет два других.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Код больше не совпадает с границей ADR | Изменился контракт или способ выполнения | Сопоставить diff с разделами Context и Decision | Создать Proposed successor, старый текст не менять |
| В записи есть assumption, но нет факта о её состоянии | ADR приняли за автоматическую проверку | Назвать источник, период, среду и результат, который опровергнет assumption | Открыть отдельную validation task |
| Наступила дата review, но новых данных нет | Календарный срок подменил сигнал | Проверить ссылки, владельца, ограничения и evidence gap | Reconfirm или запланировать проверку; не ставить Superseded |
| Старое решение кажется «неудобным» | Последствия стали дороже или изменился приоритет | Заново сравнить альтернативы по текущим критериям | Записать новый компромисс и цену миграции |
| Нужно отменить изменение | Successor ещё не принят или его проверка не закрыта | Проверить статус и ссылку на прежний Accepted ADR | Вернуться к известной записи, не стирать историю |
Начните с исходной границы. Запишите её одним предложением: «экспорт выполняется синхронно, если размер запроса не превышает установленный предел, а вызывающая сторона ждёт ответ». Затем назовите новый факт: например, вызывающая сторона должна видеть статус завершения, а время выполнения больше не ограничено коротким запросом.
\nНовый факт ещё не выбирает архитектуру. Сравните варианты: оставить синхронный путь, добавить очередь со статусом или запустить неограниченную фоновую работу. У каждого варианта есть владелец статуса, путь восстановления, поведение при повторе и цена поддержки. Если критерий «вызывающая сторона должна видеть статус» обязателен, первый вариант отпадает. Если нет владельца очереди и проверки восстановления, второй остаётся только предложением.
\nОтрицательный путь важен. Если проверка показала, что новый код не отменяет исходную границу, не создавайте successor ради даты review. Зафиксируйте результат проверки и подтвердите прежнее решение. Если evidence отсутствует, не превращайте отсутствие ошибки в доказательство пригодности. Статус должен остаться Proposed, пока ответственный не согласовал контекст и способ проверки.
\nНиже приведена условная модель. Она не читает репозиторий, не меняет ADR и не доказывает свойства реальной очереди. Функции только показывают порядок состояний: сначала формируется предложение, затем отдельная проверка возвращает его без автоматического принятия.
\nconst oldRecord = {\n id: 'adr-0012',\n status: 'accepted',\n boundary: 'small synchronous export'\n};\n\nconst successor = {\n id: 'adr-0013',\n status: 'proposed',\n supersedes: oldRecord.id,\n decision: 'queued export with visible status',\n validation: 'caller observes pending, completed and failed states'\n};\n\nconst review = validateSuccessor(successor);\n\nif (review.accepted) {\n oldRecord.status = 'superseded';\n} else {\n oldRecord.status = 'accepted';\n successor.status = 'proposed';\n}\nГлавная защита находится в ветке else. Ошибка проверки не должна автоматически закрывать старую опору. В реальной системе функция проверки была бы тестом, ручным review, проверкой схемы или запросом с известным scope. Само поле accepted в примере не означает, что такой контроль уже существует.
Ссылка из ADR должна вести к устойчивой границе: контракту, схеме, модулю или отдельному тесту. Ссылка не подтверждает соответствие сама по себе. Для каждого важного assumption задайте проверяемый вопрос. Например: «видит ли вызывающая сторона три состояния операции?» Ответ должен иметь источник, среду, период и критерий остановки.
\nМетрика отвечает только на тот вопрос, для которого её собрали. Низкая доля ошибок не подтверждает путь восстановления. Высокая пропускная способность не доказывает корректность прав доступа. Тест схемы не доказывает стоимость эксплуатации. Если один источник не покрывает риск, запишите это как ограничение, а не как скрытое условие готовности.
\nНе восстанавливайте прошлые мотивы по одному фрагменту кода. Составьте текущую запись: что система делает, какие факты доступны, какие неизвестны и кто отвечает за следующий вопрос. Доступный commit или тикет можно указать источником наблюдения. Нельзя выдавать его за доказательство первоначального rationale.
\nПосле срочного исправления особенно легко назвать временный обход Accepted архитектурой. Запишите срок действия, риск и условие удаления. Если команда не может назвать альтернативы и последствия, решение ещё не готово. Это честнее, чем создавать уверенную историю задним числом.
\nADR не запускает миграцию, не заменяет threat model, benchmark, тест-план, runbook или incident review. MADR и исходная форма Nygard предлагают структуру, но не устанавливают универсальные сроки review, роли согласования и веса критериев. Периодическая дата полезна только вместе с сигналом. Нельзя объявлять решение устаревшим из-за одной даты или одной метрики.
\nПроверяемый критерий готовности таков: для одного текущего ADR видны исходная assumption, подтверждающий источник, владелец проверки, отрицательный результат и действие при нём. Если нужен successor, он содержит ссылку назад, альтернативы, последствия, способ проверки и статус Proposed. Связь Superseded появляется только после явного принятия successor. После этого отдельный change plan проходит свои тесты и имеет путь отката.
\nЕсли хотя бы одного элемента нет, результатом должна быть открытая проверка или статус Proposed. Это не незавершённость документа. Это точное описание границы знания команды.
\nПроблема в актуальности ADR проявляется, когда репозиторий открывают перед изменением сервиса. В записи зафиксирован синхронный экспорт: клиент ждёт ответ с готовым файлом. В коде уже появился worker, а endpoint возвращает jobId и состояния pending, completed и failed. Возникает соблазн заменить в старом файле пару слов и оставить прежнюю дату.
Такой diff скрывает две разные операции. Первая — уточнить evidence: решение всё ещё подходит, а наблюдение или ссылка стали точнее. Вторая — изменить архитектурную границу: теперь клиент управляет длительной операцией и должен уметь повторно получить её результат. Во втором случае правка старой записи стирает причину прежнего выбора и лишает команду точки сравнения. Разберём, как отличить эти случаи и оформить successor — новую запись, которая заменяет прежнюю.
\nArchitectural Decision Record — запись одного архитектурно значимого решения. Официальный сайт ADR описывает три опорные идеи: решение отвечает значимому требованию, сохраняет rationale и показывает trade-offs и последствия. Это не снимок текущего кода и не обещание, что выбранный вариант навсегда останется лучшим.
\nПрактический минимальный каркас выглядит так: Status, Context, Decision и Consequences. В Context попадает проблема и условия выбора. Decision называет выбранную границу. Consequences показывает, что стало легче, а что — дороже или рискованнее. MADR 4.0.0 расширяет каркас decision drivers, рассмотренными вариантами и способом confirmation — подтверждения того, что реализация соответствует записи.
Из этого следует полезное разделение. ADR фиксирует rationale. Исходный код и контракт показывают, что система делает сейчас. Тест, запрос к метрике или результат review отвечает на конкретный вопрос о соответствии. План изменения описывает rollout, наблюдение и rollback. Ссылка из ADR на файл не заменяет ни проверку, ни план.
\nСначала выпишите исходное условие, а не название технологии. В нашем примере оно звучит так: «маленький экспорт завершается в рамках запроса, а вызывающая сторона получает файл сразу». Затем соберите новый факт: «время выполнения превышает допустимое ожидание, поэтому сервер возвращает идентификатор операции, а клиент опрашивает её состояние».
\nЕсли новый факт только уточняет ссылку, владельца или измерение, accepted ADR можно дополнить датированной заметкой по правилам команды. Если изменились требование, assumption или граница ответственности, нужен новый ADR. В нём следует сослаться на старый, описать варианты и объяснить цену перехода. Статус старой записи меняется на superseded только после принятия successor. Это правило статьи — безопасная политика append-only; конкретный проект может выбрать другую процедуру и обязан описать её заранее.
| Наблюдение | Что могло измениться | Проверка | Следующий артефакт |
|---|---|---|---|
В ADR синхронный ответ, в контракте jobId | Граница времени и владелец состояния операции | Сравнить версию контракта, обработчик и retry-путь | Successor в статусе Proposed |
| Ссылка ведёт на удалённый модуль | Артефакт переехал, но решение не изменилось | Найти новый устойчивый путь и проверить тот же инвариант | Датированное обновление evidence |
| Появился новый класс данных | Изменились требования безопасности или хранения | Проверить threat model, права и срок хранения | Новый ADR либо отклонённая альтернатива |
| При review нет факта о consequence | Календарная дата подменила проверку | Назначить owner, источник, среду и критерий опровержения | Validation task, а не новый статус |
| Команда хочет «починить текст» после отката | Откат реализации перепутан с отменой решения | Сверить принятый successor и фактический rollback | Сохранённый Accepted ADR или явный Rejected ADR |
Что изменилось? Запишите наблюдаемый сигнал: новый endpoint, лимит времени, обязанность хранить статус, класс данных или исчезнувший владелец. Формула «код стал другим» недостаточна, потому что код мог изменить реализацию внутри прежней границы.
\nКакая assumption нарушена? Assumption — условие, на котором держался выбор. Для синхронного экспорта это может быть верхняя граница времени ответа. Для очереди — наличие владельца retry и понятного срока хранения. Назовите условие числом или проверяемым правилом, если это возможно.
\nКакие варианты остаются? Сравните минимум два реалистичных варианта. Для экспорта это синхронный путь с жёстким лимитом, очередь с видимым статусом или передача работы внешнему сервису. Критерии должны быть связаны с проблемой: время ответа, восстановление после сбоя, права доступа, стоимость сопровождения и обратная совместимость.
\nЧто подтвердит выбор? Confirmation должен отвечать на один конкретный вопрос. Например: «контракт позволяет клиенту получить итог после временного сетевого сбоя, не создавая второй экспорт». Укажите тест, запрос или review, среду, владельца и результат, который заставит пересмотреть решение.
\nНиже — самодостаточная fixture на Node.js. Она не читает репозиторий и не доказывает свойства реальной очереди. Её задача — сделать правило перехода явным: изменение протокола считается сигналом для successor, но старый Accepted ADR не переводится автоматически.
\nnode --input-type=module <<'NODE'\nconst oldAdr = Object.freeze({\n id: 'ADR-0012',\n status: 'accepted',\n boundary: 'small synchronous export',\n maxDurationMs: 3000,\n});\n\nconst observedContract = {\n transport: 'job',\n returns: ['jobId', 'pending', 'completed', 'failed'],\n};\n\nconst boundaryChanged =\n observedContract.transport !== 'sync' ||\n !observedContract.returns.includes('jobId');\n\nconst review = {\n signal: boundaryChanged ? 'successor-required' : 'reconfirm',\n oldStatus: oldAdr.status,\n oldAdrRemains: oldAdr.status === 'accepted',\n confirmationQuestion:\n 'Can the caller resume one job and observe its final state?',\n};\n\nconsole.log(JSON.stringify(review, null, 2));\nNODE\nОжидаемый результат — successor-required, а oldAdrRemains равен true. Это не проверка доступности worker, идемпотентности повторного запроса, авторизации или срока хранения. Для production эти свойства должны появиться в отдельном тесте или change plan. Учебная программа лишь защищает от логической ошибки «новый контракт найден — старую историю можно переписать».
Создайте новую запись рядом со старой и поставьте ей Proposed. В заголовке назовите решаемую проблему и выбранную границу, а не внутреннее имя очереди. Минимальная структура может выглядеть так:
status: proposed\nsupersedes: ADR-0012\ndecision-makers: export team\n\n# Return a status for long-running exports\n\n## Context and Problem Statement\nThe synchronous request exceeds the caller timeout.\n\n## Decision Drivers\n- bounded request time\n- resumable result\n- authenticated access to job state\n\n## Considered Options\n- keep synchronous export\n- queue the export and expose job status\n\n## Decision Outcome\nChosen option: \"queue the export and expose job status\".\n\n## Consequences\n- The client handles pending, completed and failed states.\n- The service owns retention, retry and authorization rules.\n\n## Confirmation\nContract test: one job can be polled after a transient client timeout.\nЭто пример полей, а не обязательный синтаксис для каждого репозитория. Если проект использует YAML front matter, другую нумерацию или отдельный каталог, сохраните локальный contract. Существенны не названия файлов, а обратная ссылка, граница решения, alternatives, consequences и проверка соответствия.
\nПосле review возможны три результата. При принятии successor старую запись помечают Superseded by ADR-0013 и не меняют её исходные Context и Consequences. При отклонении нового варианта старый ADR остаётся Accepted, а причина отказа остаётся в новом Rejected ADR. При недостатке данных обе записи должны честно показать неопределённость: Proposed не является разрешением на rollout.
У evidence есть субъект, область и срок действия. Commit показывает состояние кода в конкретной версии. Контрактный тест показывает допустимые формы запроса и ответа. Метрика показывает измеренное поведение при выбранной нагрузке и окне наблюдения. Review подтверждает согласование, но не заменяет эксплуатационную проверку.
\nНельзя делать следующий скачок без отдельного доказательства: «jobId есть в схеме» не означает, что операция переживает повтор; «ошибок мало» не означает, что recovery безопасен; «ADR связан с PR» не означает, что rollout можно откатить. В ADR полезно писать и отрицательный результат: какой риск не проверен, кто его проверит и какое наблюдение остановит выпуск.
\nДля локального поиска можно начать с таких команд, подставив пути своего проекта:
\nrg -n '^status:|^supersedes:|^## (Context|Decision|Consequences|Confirmation)' docs/adr\nrg -n 'jobId|pending|completed|failed' src test\ngit diff -- docs/adr src test\nКоманды показывают кандидатов для ручного сопоставления. Они не вычисляют архитектурный drift автоматически: совпадение слов не доказывает совпадение смысла, а отсутствие совпадения не доказывает, что решение нарушено.
\nADR не является RFC, тестовым раннером, threat model, benchmark, runbook или системой управления изменениями. Официальные шаблоны предлагают язык и поля, но не назначают универсальные сроки review, обязательный набор ролей или порог для создания записи. Статусы Accepted, Rejected и Superseded должны иметь однозначное значение в политике конкретного репозитория.
Append-only подход снижает риск переписать rationale, но увеличивает число записей и требует навигации между ними. Живой документ может быть удобнее для команды, однако тогда нужны датированные изменения и видимая история. Ни один вариант не спасает от неверного Context или отсутствующего owner. Нельзя объявлять решение корректным по одной ссылке, одной метрике или успешному запуску fixture.
\nДля описанного случая критерий готовности таков: видны исходная assumption, новый сигнал, выбранные варианты, consequence, владелец confirmation и результат, который опровергнет решение. Если не хватает хотя бы одного элемента, честный итог — открытая проверка или Proposed successor, а не отредактированная история.
\n