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

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

\n

Начните с одной операции: method, URI template, operationId и версия контракта. Формулировка «удаляем старый API» слишком широка. Для POST /v1/ledger/entries вопрос звучит точнее: какие callers ещё зависят от поведения этой операции, кто отвечает за замену и какие наблюдения покрывают маршрут?

\n

Source usage показывает вызов в заданном дереве исходников. Он помогает найти известный сервис, но не видит закрытый репозиторий, скомпилированный клиент или deployed версию, которая не совпала с локальной веткой. Declared dependency показывает объявленный SDK, schema или contract. Зависимость может остаться в manifest после миграции и не доказывает runtime-вызов.

\n

Exposure or traffic показывает observed requests в конкретном инструменте, маршруте, периоде и sampling policy. Запрос не равен пользователю. Один сервис может отправить тысячу запросов, а редкий клиент — один запрос за месяц. Нулевое значение означает «в этом scope сигнал не найден», а не «caller отсутствует».

\n

Authorization показывает, какой credential class или permission способен обратиться к resource. Способность не равна активности. Если политика допускает партнёрский ключ, это ещё не доказывает, что партнёр вызывает операцию. Но если такой класс не учтён, removal имеет слепую зону.

\n

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

Четыре границы lifecycle

\n

Warning делает замену видимой. Документация должна назвать replacement, owner, область действия и способ задать вопрос. Warning не подтверждает, что клиент его прочитал. Поэтому он не меняет функциональное поведение endpoint.

\n

Deprecation сообщает, что операция больше не является предпочтительной. В OpenAPI это поле deprecated: true. В HTTP можно использовать Deprecation header и ссылку на документацию, если такой сигнал поддерживают ваши клиенты. Эти механизмы помогают обнаружить новые зависимости, но не выключают ресурс и не перечисляют callers.

\n

Sunset задаёт планируемую границу возможной недоступности. RFC 8594 описывает её как сигнал о том, что конкретный URI, вероятно, станет недоступен в указанное время. Это hint, а не доказательство миграции и не гарантия, что сервер исчезнет ровно в timestamp. До этой границы всё равно нужна проверка остаточного риска.

\n

Removal — отдельное несовместимое изменение. Оно должно иметь узкий scope, owner, stop condition и restore boundary. Если одна строка consumer map имеет состояние active или unknown, автоматическое удаление нельзя считать безопасным. Состояние migrated разрешает только review: нужно проверить замену, данные и поведение, а не просто закрыть старый маршрут.

\n
\"Временная
Сигналы идут последовательно, но не заменяют друг друга. Иллюстрация показывает порядок решений; она не содержит telemetry и не задаёт реальную календарную дату.
\n

Учебный пример: одна операция и три состояния

\n

Ниже приведён синтетический пример. Имена, даты и строки не получены из production, telemetry или списка клиентов. Они нужны, чтобы показать логику решения на одном endpoint.

\n
operation: 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.

\n

Если заменить значение unknown на zero, факты не изменятся. Изменится только видимость риска. Поэтому consumer map должна хранить не только state, но и evidence scope: tool, period, route, sampling, auth boundary, owner и blind zone. Без этих полей строка не воспроизводится.

\n

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

\n
  1. Зафиксируйте одну операцию и точный replacement. Не объединяйте в одну карту разные URI, версии и semantics.
  2. Назначьте owner старого контракта и owner замены. Запишите, кто может остановить change.
  3. Добавьте warning в документацию и migration guide. Опишите совместимость, различия ответов и путь поддержки.
  4. Объявите deprecation в OpenAPI или согласованным HTTP-сигналом. Не меняйте поведение endpoint только из-за notice.
  5. Соберите evidence по отдельным типам: source usage, declared dependency, exposure or traffic, authorization и unknown. Для каждой записи укажите scope и слепую зону.
  6. Проверьте replacement на тех же входах и ошибках, которые важны для старого контракта. «Клиент обновился» недостаточно без проверки поведения.
  7. Назначьте sunset boundary как плановую дату и заранее определите stop condition. Active row, unknown row или несовместимый replacement должны останавливать удаление.
  8. Проведите human review removal. Решение должно содержать residual risk, restore boundary и ссылку на evidence. Только затем выполняйте отдельный change.
\n

Отрицательный путь

\n

Иногда все named consumers мигрировали, а неизвестный класс доступа остался. Это не повод объявить карту полной. Сохраните старый контракт совместимым, сузьте следующую проверку до разрешённой auth boundary и назначьте срок пересмотра. Если нужное наблюдение нельзя получить законно или технически, риск остаётся unknown. Дата Sunset не превращает его в zero.

\n

Другой отрицательный путь — replacement меняет semantics: новые обязательные поля, другой порядок побочных эффектов или иные коды ошибок. Даже полный список callers не делает такое удаление безопасным. Сначала нужно сравнить контракты и решить, как клиент переживёт несовместимость. Если restore невозможен после записи новых данных, rollback старого route не восстановит прежнее состояние.

\n

Ограничения

\n

Эта схема не создаёт универсальный consumer map. Она не отменяет sampling, кэширование, batch-вызовы, закрытые сети, задержку доставки логов и различия между deployed и исходным кодом. Она также не говорит, сколько дней нужно наблюдать. Окно зависит от частоты вызовов и допустимого риска, а его границы нужно обосновать.

\n

Учебный код выше не вызывает сеть, не читает файлы и не утверждает production-результаты. Официальные спецификации описывают смысл сигналов, но не проверяют ваш API. Реальную готовность устанавливает только evidence с понятным scope и ответственным владельцем.

\n

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

\n

Removal готов к отдельному рассмотрению, если выполнены все условия: одна операция имеет названный replacement; у старого и нового контрактов есть owners; warning и deprecation опубликованы; каждая строка consumer map содержит тип evidence, scope, период и blind zone; active и unknown строки либо закрыты разрешённой проверкой, либо явно приняты владельцем как residual risk; replacement проверен на критичных входах; определены stop condition и restore boundary. Если хотя бы одно условие неизвестно, вердикт — block, а не «пользователей нет».

\n

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

" + "title": "Deprecation API: как доказать готовность к удалению", + "excerpt": "Пустой график вызовов не доказывает, что API никому не нужен. Разбираем границы deprecation, Sunset и removal gate на воспроизводимом примере.", + "contentHtml": "

Команда хочет удалить POST /v1/ledger/entries: за последние две недели на одном графике нет вызовов, а в access log видна только версия /v2. Такое наблюдение легко превратить в фразу «потребителей больше нет». Но график описывает не API, а выбранный инструмент, маршрут и окно времени. Редкий партнёр, batch-задача, другой gateway или закэшированный запрос могли в него не попасть.

\n

Цена ошибки — несовместимый релиз для caller, о котором команда не знает. После удаления уже нельзя сравнить старое поведение с новым, а восстановление маршрута не исправит побочный эффект, если запрос успел записать данные. Поэтому deprecation — не команда «удалить позже», а переход с отдельными сигналами, владельцами, evidence и условием остановки.

\n

Ниже используется синтетический пример: имена, дата и строки таблицы придуманы для объяснения метода. Они не сообщают о реальных клиентах или production telemetry. Цель — показать, как из пустого сигнала получить проверяемое решение, а не ложное доказательство отсутствия пользователей.

\n

Сначала зафиксируйте одну операцию

\n

Фраза «устарел старый API» слишком широкая. В карту нужно занести method, URI-шаблон, operationId, версию контракта и replacement. Например: POST /v1/ledger/entries заменяется на POST /v2/ledger/entries. Если в одной строке смешать ещё GET, другой ресурс и несколько версий, результат нельзя будет связать с конкретным запросом.

\n

Затем назначьте две ответственности. Владелец старой операции отвечает за совместимость и решение о её отключении. Владелец replacement отвечает за различия входных полей, кодов ошибок и побочных эффектов. У одного человека или команды может быть обе роли, но это должно быть явно записано.

\n
Как читать сигнал о потребителе
Тип evidenceЧто он подтверждаетЧего он не подтверждаетСледующий шаг
Source usageВ доступном дереве исходников найден call pathЗакрытые репозитории и deployed-версияНазвать owner и проверить фактический релиз caller
Declared dependencySDK, схема или контракт объявлены зависимостьюЧто библиотека действительно вызывает старую операциюСверить версию и runtime call path
TrafficЗапросы видны в конкретном маршруте, окне и sampling policyРедкие, кэшированные и неохваченные запросыЗаписать blind zone и расширить наблюдение
AuthorizationCredential class имеет право обратиться к resourceЧто этот класс обращается к нему сейчасПроверить активность в разрешённом auth scope
UnknownГраница наблюдения не позволила классифицировать callerЧто caller отсутствуетОстановить автоматическое удаление
\n

Состояния zero и unknown нельзя склеивать. Zero означает ноль найденных событий внутри заранее описанного scope. Unknown означает, что scope недостаточен, недоступен или не связывает событие с caller. Второе состояние не является отрицательным результатом поиска.

\n

Что объявляют OpenAPI и HTTP

\n

В OpenAPI 3.1 у Operation Object есть поле deprecated. Значение true сообщает потребителям описания, что операцию следует перестать использовать. Это полезно для документации, генераторов клиента и review контракта. Оно не удаляет endpoint, не проверяет обновление сгенерированного SDK и не показывает фактический трафик.

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

\n
jq -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.

\n

RFC 8594 определяет Sunset как response header, который указывает, что URI вероятно станет недоступен в заданный момент. «Вероятно» здесь существенно: это объявление жизненного цикла, а не гарантия доступности до даты, не расписание миграции и не список callers. В RFC также описан link relation sunset для документации политики или вариантов смягчения.

\n

На дату этой статьи отдельный HTTP-сигнал deprecation ещё нельзя выдавать за устоявшийся RFC-контракт: опубликованный документ был Internet-Draft, поэтому команда может использовать только этот draft или внутреннюю policy, которую реально поддерживают её клиенты. Безопаснее считать OpenAPI-декларацию и документацию основными каналами, а response headers вводить после проверки совместимости gateway, SDK и кэшей.

\n

Четыре границы перехода

\n

Warning делает replacement заметным. В документации должны быть ссылка, owner, различия контрактов и канал поддержки. Warning не меняет поведение операции: клиент может его не прочитать.

\n

Deprecation фиксирует, что операция больше не является предпочтительной. Это момент обновления контракта и инструкций, а не момент отключения. Если проект применяет SemVer к публичному API, спецификация рекомендует выпустить minor-версию при объявлении deprecated-функциональности.

\n

Sunset задаёт плановую границу возможной недоступности. Дату нужно согласовать с владельцами callers и явно связать с часовым поясом. Наличие даты не превращает unknown в zero: редкий ежемесячный вызов не становится безопасным только потому, что календарь прошёл.

\n

Removal — отдельное несовместимое изменение. Оно требует узкого change scope, human review, stop condition и понятной границы восстановления. В SemVer удаление функциональности после её deprecation обычно относится к major-изменению, но это правило действует только для проектов, которые действительно следуют SemVer и имеют объявленный public API.

\n
Последовательность жизненного цикла API: warning и replacement, deprecated в контракте, Sunset как плановая граница и отдельный removal gate
Сигналы образуют порядок решений, но не доказывают переход сами по себе. Временная шкала показывает lifecycle, а не реальную дату или состояние конкретного сервиса.
\n

Синтетический removal gate

\n

Ниже тот же endpoint с пятью типами evidence. Обозначение active значит, что вызов подтверждён в установленном scope. migrated значит, что конкретный caller переведён и его replacement проверен. unknown оставляет вопрос открытым.

\n
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 с описанной границей.

\n

Практическая форма карты должна хранить не только state, но и инструмент, период, маршрут, sampling, auth boundary, ссылку на артефакт и owner. Например: «traffic, gateway A, 2024-10-01—2024-10-31, sampled 1:100, без batch-сети». Такая запись не доказывает отсутствие callers, зато позволяет увидеть, какую именно дыру нужно закрыть.

\n

Проверка replacement, а не только списка клиентов

\n

Даже полный список callers не спасает несовместимую замену. Сравните обязательные поля, формат идентификатора, коды ответа, идемпотентность и порядок побочных эффектов. Для записи в ledger особенно важны повтор запроса, таймаут после записи и поведение при частичном отказе.

\n
  1. Запишите одну operation, её replacement, owners и ответственную за остановку change команду.
  2. Добавьте deprecation в OpenAPI и migration guide; рядом перечислите различия входов, ответов и ошибок.
  3. Соберите source usage, declared dependency, traffic, authorization и batch inventory отдельно. Для каждого результата укажите scope, период и blind zone.
  4. Выполните одинаковый набор позитивных и отрицательных запросов против старой и новой операции в тестовом окружении. Сравните статус, тело ответа и побочные эффекты.
  5. Назначьте Sunset только после согласования окна миграции и способа поддержки. Дата должна вести к review, а не автоматически запускать удаление.
  6. Перед removal пересмотрите каждую строку карты. Active, unknown или несовместимый replacement переводят решение в block.
  7. Если gate закрыт, оформите отдельный change с планом наблюдения после релиза и проверенной границей восстановления. Не удаляйте endpoint в том же изменении, которое впервые объявляет deprecation.
\n

Два отрицательных сценария

\n

Первый сценарий — «все named consumers мигрировали». Это хороший результат инвентаризации, но не доказательство полной видимости. Если партнёрский ключ всё ещё имеет право на маршрут, capability нужно либо сузить, либо проверить активность в разрешённой системе. Пока ни одно действие не выполнено, состояние остаётся unknown.

\n

Второй сценарий — replacement отвечает быстро, но иначе обрабатывает повторный запрос. Клиент мог рассчитывать на идемпотентный ключ старой операции, а новая версия создаёт вторую запись. В этом случае статистика перехода и отсутствие вызовов старого маршрута не компенсируют разницу semantics. Сначала исправляется контракт или адаптер, затем пересматривается gate.

\n

Ограничения применимости

\n

Метод не даёт универсального числа дней для наблюдения. Окно зависит от частоты вызовов, сезонности, SLA и допустимого риска. Для ежемесячного batch две недели явно не покрывают полный цикл. Sampling, кэш, offline-клиенты, задержка логов и несколько gateway могут скрывать обращения.

\n

Метод также не разрешает искать данные там, где у команды нет права доступа. Если нужный auth или traffic source недоступен, это ограничение нужно записать как blind zone, а не замаскировать уверенным нулём. Синтетический пример выше не делает сетевых запросов и не описывает реальный релиз; его можно воспроизвести только как шаблон карты и проверки собственного API.

\n

Критерий готовности

\n

Removal можно выносить на отдельное рассмотрение, когда у операции есть названный replacement и owners, опубликованы warning и deprecation, карта callers содержит тип evidence и его границы, а replacement проверен на критичных входах и ошибках. Active и unknown строки должны быть закрыты разрешённым наблюдением или явно принятым residual risk. Должны быть определены stop condition и граница восстановления.

\n

Если хотя бы одно из этих условий неизвестно, честный результат — block. Это не означает, что endpoint нужно поддерживать навсегда. Это означает, что следующий шаг должен уменьшить конкретную blind zone: найти batch inventory, связать credential с caller, сравнить контракт или назначить владельца.

\n

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

\n" } diff --git a/editorial/agent-rewrites/114.json b/editorial/agent-rewrites/114.json index ee7e62a..85e883a 100644 --- a/editorial/agent-rewrites/114.json +++ b/editorial/agent-rewrites/114.json @@ -1,7 +1,7 @@ { "index": 114, "slug": "editorial-2024-11-practice-deprecation", - "title": "Депрекация API: как не удалить контракт вместе с неизвестным потребителем", - "excerpt": "Дата Sunset не доказывает, что API больше никто не вызывает. Разбираем consumer map, границы доказательств и removal gate, который останавливает опасное удаление.", - "contentHtml": "

В задаче стоит дата: после 3 февраля путь /v1/posting удалят. Наступает день релиза. В графике вызовов пусто, в OpenAPI уже стоит deprecated: true, а команда не видит владельца старого клиента. Кто-то открывает pull request и удаляет обработчик. Через час внешний интегратор получает 404 или 410. У команды нет ответа на три вопроса: кому сообщили, какой контракт предложили взамен и что именно доказало безопасность удаления.

\n

Это не редкий сбой календаря. Дата задаёт границу планирования, но не подтверждает отсутствие потребителей. Пустой график описывает только выбранный инструмент, маршрут, период и набор сигналов. Пометка в схеме сообщает о жизненном цикле операции, но не мигрирует SDK. Реальная депрекация должна разделять объявление, миграцию и удаление.

\n

Тезис: не удаляйте устаревший API по дате или одному зелёному индикатору. Сначала ограничьте одну операцию, составьте consumer map и укажите границу каждого доказательства. Если остался активный или неизвестный потребитель, автоматическое удаление запрещено. Для полностью подготовленного случая открывают отдельный human review, а не превращают ревью депрекации в незаметный production change.

\n

Симптом → причина → проверка → действие

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

Сначала зафиксируйте, что именно устаревает

\n

Фраза «удаляем 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.

\n

OpenAPI помогает объявить операцию устаревшей. Поле deprecated отвечает на вопрос «рекомендуется ли эта операция дальше?». Оно не отвечает на вопросы «кто вызывает её сейчас?» и «может ли replacement принять тот же сценарий?». Поэтому schema и consumer map — разные артефакты. Один сообщает о намерении. Второй связывает намерение с людьми, зависимостями и проверками.

\n

Consumer map хранит не только найденных клиентов

\n

Минимальная строка карты содержит consumer, класс сведения, contract, owner, migration path, deadline, announcement и evidence boundary. Не скрывайте неизвестность. Строка unknown consumer честнее, чем пустая таблица. Она означает, что область ещё не замкнута и автоматический removal нельзя считать безопасным.

\n
Учебная карта для одной операции
ПотребительСведениеВладелец и переходГраница доказательства
synthetic-web-checkoutsource usagesynthetic-checkout-owner; fixed v1 call → fixed v2 contractУчебная строка, не результат поиска кода
synthetic-sdk-packagedeclared dependencysynthetic-sdk-owner; выпустить v2 SDK surfaceУчебная запись зависимости, не package inventory
synthetic-unknown-integratorunknown consumersynthetic-api-owner; сохранить notice и ограничить scopeНе customer list и не доказательство отсутствия вызовов
\n
\"Consumer
Карта разделяет известные зависимости и unknown scope. Все имена и даты на схеме учебные.
\n

Разные классы evidence нельзя складывать в один count. Source usage показывает вызов в разрешённой области исходного кода. Declared dependency показывает объявленную связь пакета, схемы или SDK. Observed traffic показывает запросы в конкретном маршруте и окне. Authorization показывает, какой credential class имеет право обратиться. Ни один класс сам по себе не доказывает полную population клиентов.

\n

Например, пустой access report может не видеть запросы через gateway, другой hostname, старый credential или редкий batch. Кодовый поиск может не найти вызов, спрятанный в сгенерированном клиенте или внешнем binary. Manifest может хранить уже неиспользуемую зависимость. Поэтому рядом с каждой строкой пишите не только результат, но и то, чего он не доказывает.

\n

Объявление, Sunset и удаление — разные границы

\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

Removal gate проверяет отрицательные условия

\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

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

\n
  1. Сузьте scope. Назовите одну operation: method, URI, operationId, request, response и authentication boundary.
  2. Опишите replacement. Сравните семантику, ошибки, idempotency, ретраи и права. Один новый URL недостаточен.
  3. Соберите consumer map. Добавьте known rows и отдельную строку unknown, если полнота scope не доказана.
  4. Назначьте владельцев. Для каждой строки укажите owner, migration path, дедлайн и доступное объявление.
  5. Разделите evidence. Запишите тип сигнала, период, инструмент, область и слепые зоны. Не превращайте «не наблюдалось» в «отсутствует».
  6. Опубликуйте lifecycle notice. Свяжите deprecation, Sunset, replacement и migration guide. Проверьте, что ссылка доступна нужным потребителям.
  7. Проверьте stop conditions. Active, unknown без решения, несовместимый replacement и отсутствующий restore boundary закрывают gate.
  8. Откройте отдельный human review. В нём укажите остаточный риск и точный scope. Только после одобрения создавайте authorized removal change.
  9. Проверьте результат тем же контрактом. Отдельно подтвердите, что удаление затронуло только выбранную operation и не изменило соседние пути.
\n

Ограничения и отрицательный путь

\n

Эта схема не делает неизвестных потребителей видимыми автоматически. Она не заменяет разрешённый source search, анализ access logs, inventory клиентов, review авторизации или проверку replacement в реальной среде. Учебные строки в статье не доказывают наличие или отсутствие клиентов. Они показывают, как сохранить класс риска в модели.

\n

Не пытайтесь получить «нулевой риск» из одного сигнала. Даже полный на вид отчёт имеет scope: конкретный период, route, credential, sampling и доступность данных. Отрицательный путь должен быть первым классом результата. Если проверка не может замкнуть область, ответ — unknown, а действие — сохранить совместимость или провести отдельное решение с владельцем остаточного риска.

\n

Restore boundary также ограничивает обещание. Для draft достаточно сказать: proposal остановлен, старый контракт не изменён, черновик удалён. После реального удаления нужен другой план: кто возвращает route, какие schema и credentials ещё совместимы и как проверяется восстановление. Фраза «rollback available» без этих условий не является проверяемым планом.

\n

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

\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 уже можно удалить.

\n

Проверяемый результат — не пустой график и не дата в календаре. Это воспроизводимая запись, в которой другой инженер видит scope, доказательство, его границу, владельца и причину следующего действия. Если он не может повторить проверку или назвать условие остановки, контракт ещё не готов к удалению.

\n

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

\n" + "title": "Депрекация API без аварии: карта потребителей и gate удаления", + "excerpt": "Дата Sunset и пустой график вызовов не доказывают, что endpoint можно удалить. Показываю, как ограничить scope, собрать карту потребителей и остановить опасное удаление.", + "contentHtml": "

В задаче на удаление API обычно есть убедительная дата: после 3 февраля старый путь должен исчезнуть. В день релиза график вызовов пуст, в OpenAPI стоит deprecated: true, а владельца старого клиента никто не знает. Обработчик удаляют. Через час внешний интегратор получает 404, а команда не может ответить, кто предупредил потребителя и где описан переход.

\n

Причина не в том, что календарь составили плохо. Дата задаёт план, но не доказывает отсутствие потребителей. Пустой график описывает только выбранный маршрут, период, прокси, credentials и качество телеметрии. Пометка в схеме сообщает о жизненном цикле операции, но не обновляет SDK и не доставляет уведомление. Без карты зависимостей удаление превращается в догадку.

\n

Рабочее правило: сначала докажите границы проверки, затем принимайте решение об удалении одной операции. Результат проверки может быть observed, not observed in scope или unknown. Последние два результата нельзя склеивать с фразой «никто не использует». При активном или неизвестном потребителе автоматический removal gate закрыт.

\n

Что именно мы собираемся удалить

\n

Фраза «убираем v1» слишком широка для безопасного изменения. Она может означать один метод, несколько ресурсов, SDK-функцию, webhook или всю версию API. Начните с одной операции и запишите её как контракт: HTTP-метод, URI-шаблон, operationId, форму запроса, ответы, коды ошибок и границу авторизации.

\n

Такой scope нужен не для бюрократии. Владелец сервиса может подтвердить удаление POST /v1/posting, но не всего /v1. Новый URL может принимать тот же JSON, но иначе обрабатывать повторную доставку, идемпотентность, пагинацию или права. Поэтому рядом с replacement фиксируйте семантические отличия. Совпадение URL и формата ещё не означает совместимость.

\n
Минимальный контракт removal proposal
ПолеЧто записатьПочему это ограничивает риск
OperationPOST /v1/posting и operationIdНе даёт распространить решение на соседние методы
ReplacementНовый метод, схема, ошибки и правила повторовПоказывает, что переход — не простая замена строки
PopulationИзвестные, внешние и неизвестные классы клиентовНе маскирует неполноту инвентаризации
Evidence boundaryИнструмент, окно, маршрут, credentials, sampling и пропускиОтделяет наблюдение от доказательства полноты
Restore boundaryЧто возвращается и кто проверяет восстановлениеПревращает rollback из обещания в проверяемое условие
\n

Почему одного сигнала недостаточно

\n

Карта потребителей должна разделять классы сведений. Поиск исходников показывает вызовы в просмотренной области. Manifest показывает объявленную зависимость. Access log показывает запросы, дошедшие до конкретного слоя. Сведения от владельца подтверждают намерение команды, но не заменяют runtime-проверку. У каждого сигнала свой blind spot, поэтому итоговая строка хранит и результат, и границу.

\n
Как читать отрицательный результат
РезультатЧто он действительно означаетЧего он не доказываетСледующее действие
source: foundВ просмотренном репозитории найден вызовЧто это единственный потребительНазначить владельца и запланировать миграцию
traffic: zeroЗа окном не было видимых запросовЧто нет batch, другого gateway или редкого клиентаРасширить окно и сверить маршрут и credentials
declared: absentЗависимость не записана в проверенном manifestЧто внешний binary или сгенерированный SDK отсутствуетПроверить inventory и канал объявления
scope: unknownПолнота области не доказанаЧто endpoint безопасно удалятьСохранить совместимость или принять риск отдельно
\n
Схема удаления API: операция связана с картой потребителей, владельцами, replacement и границами доказательств; active или unknown consumer ведёт к остановке удаления
Карта показывает порядок решения: сначала scope и evidence, затем миграция; неизвестная область закрывает автоматическое удаление. Имена на схеме условные.
\n

Например, отсутствие записи в репозитории не видит закрытый исходный код, сгенерированный клиент или старый бинарный клиент. Нулевой access log не видит запрос, который прошёл через другой hostname, не попал в выбранный gateway или пришёл раз в квартал. Даже полный на вид отчёт относится к своему окну и credential class. Именно поэтому строка unknown consumer полезнее пустой ячейки: она сохраняет незамкнутую область в решении.

\n

Депрекация не равна отключению

\n

В OpenAPI 3.1 поле deprecated у Operation Object объявляет операцию устаревшей и рекомендует потребителям прекратить её использование. Это описание контракта. Оно не ищет клиентов, не выпускает новую версию SDK и не меняет ответ сервера.

\n

Runtime-уведомление решает другую задачу. Заголовок Deprecation из RFC 9745 сообщает клиенту дату депрекации; его значение — structured date, например Deprecation: @1688169599. Тот же RFC подчёркивает: сам факт депрекации не меняет поведение ресурса. Для документации можно добавить Link с отношением deprecation, где указаны replacement и миграционная инструкция.

\n

Sunset из RFC 8594 сообщает, что ресурс ожидается недоступным после указанного HTTP-времени. Это подсказка для клиента, а не гарантия того, что до даты всё будет работать, а после неё обязательно появится конкретный код. В RFC 9745 также зафиксировано, что Sunset не должен быть раньше даты Deprecation. Проверяйте эти заголовки на фактическом ответе и не выдавайте их за доказательство миграции.

\n
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 не подтверждает поведение другого слоя.

\n

Как собрать карту потребителей

\n

Минимальная запись содержит consumer, класс evidence, owner, replacement, migration path, срок, канал уведомления и evidence boundary. Для известного клиента нужна не только строка «migrated», а ссылка на проверку: версия SDK, тест совместимости, успешный запрос или подтверждённый rollout. Статус в таблице — это утверждение, его источник и область должны быть видны рядом.

\n
consumer,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

Removal gate должен уметь остановить изменение

\n

Хороший gate формулируется отрицательными условиями. Удаление запрещено, если найден активный consumer; если неизвестная область не ограничена и остаточный риск не принят; если replacement меняет значимую семантику без плана; если notice не связан с affected scope; если не описано восстановление. Ранняя дата не отменяет ни одного из этих условий.

\n

Разделяйте два решения. Первое — можно ли объявить операцию устаревшей и начать миграцию. Второе — можно ли менять runtime так, чтобы старый путь перестал отвечать. Между ними должны быть окно совместимости, подтверждённые владельцы и отдельное human review. Не объединяйте удаление route, изменение прав, чистку документации и удаление SDK в один непроверяемый коммит.

\n
Вердикты перед удалением
ВердиктУсловиеРазрешённое действие
block: activeЕсть запросы или подтверждённая зависимостьОстановить removal, назначить миграцию
block: unknownОбласть проверки не замкнутаРасширить наблюдение или продлить совместимость
allow: reviewScope, replacement, owners, notice и restore boundary описаныОткрыть отдельный review; не удалять автоматически
allow: changeПолитика проекта допускает изменение после reviewПрименить узкий change и проверить соседние операции
\n

Воспроизводимая последовательность

\n
  1. Зафиксируйте scope. Назовите одну operation, метод, URI, operationId, схемы ответа и authentication boundary.
  2. Опишите replacement. Сравните семантику, ошибки, идемпотентность, повторы, лимиты и права, а не только URL.
  3. Соберите карту. Проверьте source usage, manifests, SDK inventory, gateway и ingress. Добавьте отдельную строку unknown, если полнота не доказана.
  4. Разметьте evidence. Для каждого результата сохраните инструмент, окно, маршрут, credentials, sampling и blind spots.
  5. Назначьте владельцев. У каждой известной зависимости должны быть owner, migration path, версия replacement и способ подтвердить переход.
  6. Опубликуйте notice. Свяжите OpenAPI, Deprecation, Sunset, Link-документацию и миграционный план с тем же scope.
  7. Проверьте stop conditions. Active, unknown без принятого риска, несовместимый replacement и отсутствующий restore boundary закрывают gate.
  8. Откройте отдельный review. В решении укажите остаточный риск, затронутую operation и критерии отказа. Только после допуска создавайте runtime change.
  9. Сверьте результат. Повторите проверку после изменения: старый путь изменился ожидаемо, replacement доступен, а соседние методы и правила авторизации не затронуты.
\n

Ограничения применимости

\n

Эта схема не делает неизвестных клиентов видимыми автоматически. Она не заменяет договор с внешним партнёром, анализ закрытого кода, юридические сроки уведомления, требования к доступности или политику change management. Конкретное окно наблюдения и достаточная полнота зависят от трафика: для ежедневного запроса 30 дней может быть полезным сигналом, для квартального batch — нет.

\n

Заголовки тоже имеют границы. Клиент может их не читать, промежуточный кеш может изменить наблюдаемую картину, а Deprecation и Sunset не доказывают, что владелец клиента получил notice. RFC 8594 не определяет, будет ли после Sunset 4xx, 3xx или другой отказ. Поэтому не стройте единственную защиту на runtime-заголовке и не обещайте точный код ответа, если он не закреплён вашим контрактом.

\n

Если потребитель активен, безопасное действие — остановить удаление и дать ему совместимый путь. Если потребитель неизвестен, безопасное действие — расширить scope или сохранить endpoint. Residual risk можно принять только тем владельцем и в том процессе, которые отвечают за последствия; запись «вроде никто не использует» таким решением не является.

\n

Критерий готовности

\n

Операция готова к removal review, когда её scope однозначен, replacement проверен по семантике, известные потребители имеют владельцев и миграционные доказательства, неизвестная область либо закрыта, либо явно принята, а notice и restore boundary доступны. Решение должно быть воспроизводимым: другой инженер может повторить запрос, понять границу логов, найти источник строки карты и назвать условие остановки.

\n

Итогом не обязательно будет удаление. Иногда лучший результат — продлить совместимость, добавить телеметрию или сузить контракт. Дата в календаре становится полезной только после того, как рядом появились scope, доказательство, владелец и понятный следующий шаг.

\n

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

\n" } diff --git a/editorial/agent-rewrites/115.json b/editorial/agent-rewrites/115.json index ba8ae81..611896e 100644 --- a/editorial/agent-rewrites/115.json +++ b/editorial/agent-rewrites/115.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-10-field-capacity-cost", "title": "Ёмкость и стоимость: как выбрать следующий предел, а не самый большой инстанс", "excerpt": "Большой инстанс может улучшить одну latency-метрику и одновременно увеличить расход и операционный риск. Разбираем причинную цепочку, проверку и безопасный критерий следующего шага.", - "contentHtml": "

На разборе ёмкости команда видит знакомую картину: p95 снизился после перехода на 4 CPU, а счёт и запас зарезервированных ресурсов выросли. Вторая карточка с 1,6 CPU показывала очередь и почти касалась SLO, поэтому большой инстанс кажется очевидным ответом. Цена ошибки — закрепить дорогой reservation без доказанного эффекта, принять одну удачную latency-точку за решение и потерять понятный путь возврата.

\n

Тезис: ёмкость выбирают не по лучшему p95, а по следующему проверяемому пределу. Сравните performance, модель стоимости и операционный риск в одном scope. Запишите условие остановки до выбора размера. Если хотя бы одна ось не объяснена, остановитесь и запросите недостающие данные.

\n

Что именно нужно сравнить

\n

Сначала зафиксируйте workload, период, единицы и формулу стоимости. Затем разделите четыре слоя. request описывает ресурс, который workload просит у планировщика. limit задаёт верхнюю границу для контейнера. quota ограничивает суммарное потребление в области политики. Ни один из этих терминов не является ценой сам по себе.

\n

Стоимость требует отдельной формулы. В ней могут участвовать базовая плата, объём выделенного ресурса, единица измерения, период, ступени тарифа и дополнительные услуги. Если цена неизвестна, используйте только обозначения. Не подменяйте счёт процентом CPU. Низкая загрузка говорит о наблюдаемом использовании, но не отвечает, какая часть счёта исчезнет после изменения.

\n
cost(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

Симптом → причина → проверка → действие

\n
Быстрый разбор перед изменением ёмкости
СимптомПричинаПроверкаДействие
Большой инстанс дал лучший p95Latency сравнили без стоимости и риска возвратаСопоставить 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 и проверить сигнал очередиОстановить ветку и открыть отдельный разбор
\n

Пример: три предела на одной шкале

\n

Рассмотрим учебные данные для одного 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 не становится объяснением причины очереди.

\n

Третья точка резервирует 4 CPU и 6 GiB памяти. p95 снижается до 208 ms, а наблюдаемая загрузка CPU составляет 31%. Условная стоимость — 17,76 единицы за 24 часа. Такой результат показывает, что latency можно купить резервом. Он не доказывает, что резерв нужен, что цена рассчитана по реальному тарифу или что его безопасно уменьшить.

\n
Учебное сравнение трёх вариантов; числа не являются production-измерениями
ВариантPerformanceСтоимость за 24 чРиск и решение
next-limitp95 248 ms; запас 52 ms11,856 ulow; сравнить следующий малый шаг
saturation-stopp95 296 ms; запас 4 ms; queue-growth12,288 ustop; сначала выяснить причину сигнала
large-reservationp95 208 ms; CPU 31%17,76 umedium; не считать размер базовой стратегией
\n

Таблица не превращает три оси в общий score. Такой score потребовал бы отдельного владельца и обоснованной формулы. Здесь важнее сохранить отрицательный путь. Если performance хорош, но стоимость или возврат не объяснены, ответом становится остановка. Если стоимость ниже, но SLO почти нарушен, ответом также становится остановка.

\n
\"Кривая
Схема разделяет latency, условную стоимость и риск. Она помогает увидеть, где лучший p95 перестаёт быть достаточным основанием для выбора.
\n

Почему request, limit и quota не заменяют расчёт

\n

Планировщик Kubernetes учитывает requests при размещении Pod. Limit задаёт отдельное ограничение исполнения. ResourceQuota ограничивает суммарные requests или limits в namespace. LimitRange может задать значения по умолчанию и минимальные или максимальные границы на этапе admission. Эти механизмы отвечают на разные вопросы: можно ли принять объект, сколько ресурса он просит и какие пределы действуют. Они не говорят, сколько стоит час работы и выдержит ли сервис нагрузку.

\n

Поэтому фраза «в quota ещё есть 6 CPU» недостаточна. Она может означать, что объект проходит одну policy-проверку. Она не подтверждает свободную ёмкость кластера, отсутствие конкуренции, нужный запас по SLO или экономический эффект. Сначала проверьте policy. Потом проверьте runtime-сигналы. Затем сопоставьте allocation с разрешённой моделью billing.

\n

Остановку нужно определить заранее

\n

Stop condition защищает от решения под давлением. Пример правила: остановиться, если запас до SLO меньше 15 ms; остановиться при сигнале роста очереди; остановиться, если новый вариант меняет одновременно CPU, память, класс машины, сеть и concurrency; остановиться, если никто не назвал owner и границу возврата.

\n

Предел считается следующим только тогда, когда он меняет один основной контролируемый параметр. Для него известны исходное значение, новое значение, период наблюдения и обратная граница. Если вместе с CPU меняются storage, network и commitment, это уже набор решений. Его нельзя объяснить одной строкой «увеличили ёмкость».

\n

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

\n
  1. Зафиксируйте scope. Назовите workload, окружение, период, SLO, единицы и владельца решения.
  2. Опишите текущую точку. Запишите request, limit, quota, p50/p95, error rate, saturation signal и формулу стоимости.
  3. Назначьте stop condition. Укажите числовой запас до SLO и сигналы, которые прекращают сравнение.
  4. Выберите один малый шаг. Измените один основной параметр и сохраните исходный return boundary.
  5. Проверьте policy. Отдельно подтвердите admission, LimitRange, ResourceQuota и возможность размещения.
  6. Проверьте runtime. Сравните тот же workload и период по latency, ошибкам, очередям и фактическому использованию.
  7. Проверьте стоимость. Подтвердите SKU, usage unit, base unit, ступени тарифа и период. Если данных нет, оставьте стоимость неизвестной.
  8. Примите ограниченный outcome. Либо сравните следующий шаг, либо остановитесь с причиной и вопросом владельцу. Большой инстанс не является outcome по умолчанию.
  9. Зафиксируйте критерий готовности. Другой инженер должен повторить расчёт и назвать условие остановки без устных пояснений.
\n

Ограничения и отрицательный путь

\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

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

\n

Разбор готов к решению, когда одна строка связывает workload, resource allocation, performance signal, billing unit, owner, stop condition и return boundary. Для выбранного шага известны изменяемое поле и период проверки. Policy не смешана с runtime-измерением. Стоимость либо подтверждена официальной формулой и собственными данными, либо явно помечена неизвестной.

\n

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

\n

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

\n" + "contentHtml": "

На разборе ёмкости команда видит знакомую картину: после перехода на 4 CPU p95 снизился, а резерв и счёт выросли. Карточка с 1,6 CPU уже показывала очередь и почти касалась SLO, поэтому большой инстанс выглядит очевидным ответом. Цена ошибки — закрепить дорогую конфигурацию без доказанного эффекта, принять одну удачную latency-точку за решение и потерять понятную границу возврата.

\n

Тезис: следующий размер выбирают не по лучшему p95, а по следующему проверяемому пределу. В одной карточке нужно связать workload, выделенный ресурс, сигнал производительности, единицу тарификации и условие остановки. Если хотя бы одно звено неизвестно, результатом должна быть остановка или отдельный разбор, а не заказ самого большого инстанса.

\n

Начните с одного вопроса

\n

Вопрос «сколько CPU нам нужно?» слишком короткий. Для проверки запишите: какой workload измеряем, в каком окружении, за какой период, какой SLO защищаем и что именно меняем. Для сервиса это может быть поток HTTP-запросов, число реплик, requests и limits контейнера, p95 задержки, ошибки и сигнал очереди. Для финансовой части добавьте billing unit: час виртуальной машины, CPU-hour, GiB-hour, запрос или трафик.

\n

Так появляются разные, но связанные факты. Workload описывает работу. Allocation описывает обещанный системе резерв. Usage показывает фактическое потребление. Performance показывает результат для пользователя. Billing expression объясняет, как провайдер переводит использование или резерв в деньги. Низкий usage не заменяет allocation и не доказывает экономию. Зелёный SLO не объясняет тариф.

\n

Ситуация считается описанной только после фиксации scope. Запись «p95 стал лучше» не воспроизводится без маршрута, нагрузки, числа реплик, периода, версии и способа измерения. Запись «стоимость выросла на 20%» не воспроизводится без счёта, валюты, региона, скидки и правила агрегации.

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

Разделите request, limit, quota и цену

\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 не достигнутоДоступный узел и запас по SLOAdmission, cluster capacity и saturation
p95 ниже SLOМетрика укладывается в заданное окноЭкономичность и причинность улучшенияОдинаковый workload, период и allocation
Счёт выросИзменился итог billing-периодаПричину в одном ресурсеSKU, rate, replicas, storage и traffic
\n

Практический вывод отрицательный: нельзя уменьшать request только потому, что средний CPU низкий, и нельзя увеличивать limit только потому, что quota это допускает. Для памяти редкий пик важнее среднего значения. Для CPU полезно посмотреть throttling и очередь. Для latency — выяснить, не сидит ли время в сети, блокировке или внешнем сервисе.

\n

Соберите прозрачную модель стоимости

\n

Формула нужна не для имитации счёта, а для проверки причинной цепочки. В учебной модели отдельно обозначим фиксированную часть, резерв CPU, резерв памяти и ставки. Если провайдер считает инстанс целиком, а не request контейнера, формула должна отражать инстанс. Если billing unit неизвестна, арифметику нужно остановить, а не заменить процентом utilization.

\n
node - <<'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.

\n

В рабочей карточке рядом с формулой должны лежать источник значения, единица и период. Ставка может быть tiered, а расход — агрегироваться по проекту или аккаунту. Поэтому сравнение двух конфигураций по одной строке «CPU × ставка» безопасно только как черновая модель, пока billing export или rate card не подтверждают остальные компоненты.

\n

Пример: три предела и отрицательная ветка

\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 за сутки. Следующий рост ресурса не объясняет, почему растёт очередь.

\n

Большой вариант резервирует 4 CPU и 6 GiB. p95 равен 208 мс, CPU usage в карточке — 31%, а модельная стоимость — 17,76 за сутки. Это показывает, что latency можно получить за счёт большого резерва. Это не доказывает, что резерв нужен, что применена правильная ставка или что его можно без риска вернуть назад.

\n
Учебное сравнение вариантов; числа не являются production-измерениями
ВариантРесурсPerformanceСтоимость за 24 чРешение
current1,2 CPU; 2 GiBp95 248 мс; запас 52 мс11,424 uБаза сравнения
next-limit1,4 CPU; 2 GiBp95 248 мс; запас 52 мс11,808 uПроверить соседний шаг
saturation-stop1,6 CPU; 2 GiBp95 296 мс; запас 4 мс; растёт очередь12,192 uОстановить сравнение
large-instance4 CPU; 6 GiBp95 208 мс; CPU 31%17,76 uНе выбирать по одному p95
\n

Таблица не превращает performance, cost и risk в общий score. Для такого score пришлось бы заранее определить веса, владельца и допустимые trade-off. Здесь важна сохранённая отрицательная ветка: хороший p95 при неизвестной цене не даёт согласия, а низкая цена при headroom 4 мс не даёт разрешения продолжать.

\n

Где проходит граница Kubernetes

\n

Request влияет на планирование, но не обещает, что приложение всегда получит ровно это количество 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 и проверить малый соседний шаг.

\n

Определите stop condition заранее

\n

Stop condition защищает решение от давления одного удачного графика. Для учебного примера зададим остановку при headroom меньше 15 мс, при росте очереди, при неизвестной billing unit или при одновременной смене CPU, памяти, сети и класса машины. Эти условия не выбирают победителя. Они говорят, когда текущая гипотеза больше не проверяется тем же экспериментом.

\n

Возврат тоже имеет границу. Запись «при ухудшении откатить» неполна, пока не названы владелец, изменение, разрешённый способ возврата и сигнал успешного восстановления. Эта статья не даёт команды rollback для чужого кластера: команда зависит от вашего deployment-процесса, прав и политики изменений. Если такой путь не описан, это причина остановить change, а не повод скрыть риск.

\n

Порядок воспроизводимой проверки

\n
  1. Зафиксируйте scope. Назовите workload, окружение, маршрут, версию, число реплик, период и владельца.
  2. Запишите текущую точку. Сохраните request, limit, quota, p50/p95, ошибки, очередь, peak usage и источник каждого значения.
  3. Разделите policy и runtime. Отдельно подтвердите admission, LimitRange и ResourceQuota; отдельно проверьте placement, throttling, memory pressure и saturation.
  4. Назовите billing unit. Зафиксируйте SKU, usage unit, base unit, tier, валюту, регион, скидку и период. Неизвестное поле оставьте неизвестным.
  5. Выберите соседний шаг. Измените один основной управляемый параметр и сохраните workload, окно, версию и способ измерения.
  6. Запустите расчёт. Выполните учебную команду или свой эквивалент, проверьте единицы и пересчитайте marginal cost только для сопоставимых входов.
  7. Проверьте stop condition. Сверьте headroom, очередь, ошибки и согласованность billing. При срабатывании остановитесь и оформите следующий вопрос.
  8. Зафиксируйте outcome. Запишите compare, stop или human review, владельца и границу возврата. Не называйте учебный результат командой для production.
  9. Попросите повторить расчёт. Другой инженер должен восстановить входы, формулу, отрицательную ветку и причину выбора без устного контекста.
\n

Ограничения применимости

\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

Критерий готовности

\n

Решение о следующем пределе готово, когда одна карточка связывает workload, allocation, performance signal, billing unit, stop condition, owner и return boundary. В ней видно, какой параметр изменился, за какой период проведено сравнение и какой сигнал остановит эксперимент. Policy не выдана за runtime, usage не выдан за цену, а лучший p95 не выдан за универсальное решение.

\n

Если хотя бы одного поля нет, полезный результат — не самый большой инстанс, а список недостающих доказательств. Такой отказ от поспешного изменения сохраняет возможность сравнить соседний шаг и делает следующую итерацию проверяемой.

\n

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

\n" } diff --git a/editorial/agent-rewrites/116.json b/editorial/agent-rewrites/116.json index cfff4f8..13baa4b 100644 --- a/editorial/agent-rewrites/116.json +++ b/editorial/agent-rewrites/116.json @@ -1,7 +1,7 @@ { "index": 116, "slug": "editorial-2024-10-mechanism-capacity-cost", - "title": "Почему низкая загрузка не означает низкую стоимость", - "excerpt": "Процент CPU показывает использование выбранного ресурса, но не цену. Разбираем fixed и variable части, quota и limit, saturation и способ проверить вывод до изменения capacity.", - "contentHtml": "

График показывает CPU 31%. p95 равен 208 ms при SLO 300 ms. Команда делает вывод: инстанс слишком большой, его можно уменьшить. Через неделю тот же график используют как доказательство экономии. Но в расчёте нет базовой ставки, единицы тарификации, memory allocation и правила quota. Ошибка стоит дороже одного неверного числа: можно получить очередь, нарушить SLO или принять учебную арифметику за счёт провайдера.

\n

Тезис простой: utilisation, capacity и cost отвечают на разные вопросы. Низкий процент означает только, что наблюдаемая нагрузка мала относительно выбранной базы. Цена зависит от формулы, периода, fixed части и единицы расчёта. Quota ограничивает допустимый объём. Limit задаёт верхнюю границу ресурса. Saturation показывает, что система приближается к отказу. Эти величины связаны, но ни одна не заменяет другую.

\n

Начните с наблюдаемого симптома

\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 и граница риска.

\n

Механизм: пять разных объектов

\n

Observed utilisation — измерение использования относительно выбранного ресурса. CPU 31% не говорит, был ли выбранный request разумным и сколько стоит час. Fixed resource — постоянная часть учебной формулы, например base 0,36 u/h. Она остаётся в scope, пока существует сама модель.

\n

Variable resource — часть, которая меняется с allocation. В примере это CPU request и memory request, умноженные на учебные ставки за core-hour и GiB-hour. Billing unit — единица, к которой относится формула: час, GiB-hour или другая unit из конкретного контракта. Без неё разность двух чисел не имеет смысла.

\n

Quota и limit задают ограничения ресурса, а не цену. Quota может ограничивать суммарные requests и limits в namespace. Limit задаёт верхнюю границу для workload. Saturation — сигнал, что очередь, p95 или retry risk приближаются к принятой границе. Saturation может остановить выбор, но не пересчитывает billing formula.

\n
Причинная схема разделяет usage, allocation, quota и limit, saturation и модельную стоимость
Схема разделяет наблюдаемое использование, выделенный ресурс, ограничения, saturation и модельную стоимость. Она не показывает реальный счёт, кластер или реальную telemetry.
\n

Учебная формула и код

\n

Ниже — ограниченный учебный пример. Он не читает облачный счёт и не предлагает менять рабочую систему. Формула нужна, чтобы сделать промежуточные величины видимыми:

\n
const 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 меньшая цена не доказывает безопасное уменьшение.

\n

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, одна разность уже не объясняет решение.

\n

Симптом → причина → проверка → действие

\n
Как разбирать вывод о capacity и стоимости
СимптомПричинаПроверкаДействие
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 и часыНе сравнивать карточки до выравнивания входов
\n

Почему quota и saturation нельзя менять местами

\n

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

Отрицательный путь

\n

Хорошая проверка должна уметь остановиться. Для large-instance-unjustified учебная ветка видит CPU 31%, p95 208 ms и большую reservation. Она возвращает stop-and-compare-smaller-limit. Ветка не уменьшает request и не создаёт rollback. Она запрещает два одинаково слабых вывода: «низкая загрузка означает лишний ресурс» и «лучший p95 оправдывает самый большой ресурс».

\n

Остановка нужна и при подмене входа. Если отчёт содержит неизвестное поле вроде invoice, другой scope, sparse array или изменённую model cost, его нельзя молча принять. Сначала нужно вернуть форму к согласованному контракту. Если unit или период не подтверждены, вычисление marginal cost прекращается. Отрицательный путь защищает границу примера, а не реальный API и не рабочую систему.

\n

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

\n
  1. Зафиксируйте симптом. Запишите usage, p95, SLO, cost и signal без слов «дёшево», «дорого» или «лишний».
  2. Опишите scope. Назовите workload, resource, период, unit и границу сравнения. Разные интервалы нельзя сводить в одну карточку.
  3. Разложите формулу. Отделите fixed base, variable allocation и billing unit. Если unit неизвестна, остановите расчёт.
  4. Проверьте policy. Сверьте request, limit, quota и admission rules. Ответ «quota позволяет» не закрывает performance branch.
  5. Проверьте saturation. Сопоставьте queue, p95 headroom, retry risk и SLO. При stop condition не выбирайте следующий размер.
  6. Сравните соседний вариант. Меняйте один основной параметр, сохраняйте период и unit, считайте marginal difference только для сопоставимых карт.
  7. Назовите недостающий источник. Для цены это billing contract или export, для performance — telemetry и workload evidence, для policy — действующее правило admission.
  8. Зафиксируйте результат. Выберите compare, stop или human review. Ни один результат учебной модели не становится командой над кластером.
\n

Ограничения и критерий готовности

\n

Все числа в примере учебные. Модель не видит 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

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

" + "title": "Ёмкость и стоимость: как связать нагрузку, ресурс и счёт", + "excerpt": "Низкая загрузка и зелёный SLO не показывают цену сами по себе. Разбираем цепочку от workload до billing unit и даём воспроизводимый способ проверить следующий предел ёмкости.", + "contentHtml": "

Симптом виден сразу: сервис держит p95 ниже целевого значения, а счёт за инфраструктуру растёт. На графике CPU занято 35%, поэтому первая гипотеза звучит логично: ресурсов слишком много. Но процент загрузки описывает только наблюдаемое использование. Он не говорит, сколько ресурса зарезервировано, за какую единицу считает провайдер и какая часть стоимости фиксирована. Если перепутать эти слои, команда уменьшит резерв без доказательства безопасности или, наоборот, купит большой инстанс без объяснимого эффекта.

\n

Главный вопрос этой статьи — почему низкая загрузка не означает низкую стоимость. Ответ можно проверить цепочкой workload → resource → billing unit → decision. Сначала фиксируем поток работы и его качество, затем отделяем выделение ресурса от фактического использования, после этого читаем модель тарификации и только в конце выбираем изменение. Числа ниже учебные: они показывают метод расчёта, а не счёт конкретного облака.

\n

Один график скрывает четыре разные величины

\n

Workload — поток работы: запросы в секунду, размер сообщения, фоновые задания и период наблюдения. Resource allocation — то, что система выделила или зарезервировала: число реплик, CPU и память в request, limit, размер виртуальной машины или другой контролируемый параметр. Usage — фактическое потребление в конкретном окне. Billing unit — единица, к которой привязан тариф: час инстанса, CPU-hour, GiB-hour, запрос, байт или составная модель.

\n

Эти величины связаны, но не заменяют друг друга. Низкий usage может быть полезным запасом для пика. Высокий usage может не менять счёт при фиксированной оплате инстанса. SLO отвечает за доступность или задержку, а не за цену. Quota отвечает за допустимый агрегат в политике платформы, а не за свободную ёмкость и не за финансовый результат.

\n
Что измеряет показатель и какой вывод из него допустим
СлойПример значенияЧто он показываетЧего он не доказывает
Workload120 RPS, пик 180 RPSКакой поток нужно обслужить и в каком окнеСколько стоит ресурс без правила тарификации
Allocation2 реплики, request 1,2 CPUКакой резерв заявлен системеФактическое потребление и цену без billing unit
UsageCPU 35%, память 61%Наблюдаемое использование за периодЧто можно безопасно убрать на пике
SLOp95 < 300 мсКачество обслуживания в заданной границеЭкономичность решения
Billing unitCPU-hour и GiB-hourК чему применяются ставки и ступениРеальную сумму без тарифа, региона и счётных данных
\n

Практическая ошибка начинается с подмены: «CPU 35%, значит оплачиваем 35%». Это может быть верно только при конкретном контракте тарификации. Если провайдер продаёт целый инстанс, график использования не уменьшит его часовую цену. Если тариф зависит от резервирования, процент usage также не станет входом формулы автоматически.

\n

Механизм: request, limit и quota живут в разных границах

\n

В Kubernetes эти границы хорошо видны. Scheduler использует resource request при выборе узла: сумма requests должна помещаться в доступную ёмкость узла, даже если фактическая загрузка сейчас мала. Limit задаёт другую границу исполнения; для CPU он связан с throttling, а превышение memory limit может привести к OOM-убийству при давлении на память. Следовательно, request, limit и usage нельзя складывать в одну метрику «занято».

\n

ResourceQuota ограничивает агрегатное потребление в namespace. Если создание или обновление объекта нарушает квоту, control plane может отклонить запрос с HTTP 403. Это проверка политики, а не доказательство того, что в кластере есть свободный узел или что вариант дешевле. Наличие места до quota не отменяет проверки scheduler, runtime-сигналов и биллинга.

\n

На другой платформе названия будут иными, но вопрос остаётся тем же: кто принимает решение о размещении, кто ограничивает исполнение, кто измеряет usage и кто выставляет счёт. Нельзя переносить Kubernetes-семантику на managed database или serverless-функцию без документации конкретного сервиса.

\n
\"Причинная
Схема разделяет usage, allocation, policy и billing. Стрелки показывают учебную модель связи, а не инвойс конкретного провайдера и не рекомендацию менять ресурс без измерений.
\n

Учебный расчёт: цена начинается с единицы тарификации

\n

Возьмём один сервис с двумя репликами. На реплику заявлены request 1,2 CPU и 2 GiB памяти. Для учебной модели считаем, что фиксированная часть равна 0,36 условной единицы за час, CPU стоит 0,08 за CPU-hour, а память — 0,01 за GiB-hour. Эти ставки специально обозначены как условные: в реальном проекте их нужно заменить на тариф, регион, скидку, commitment и правило распределения общих затрат.

\n
const 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, узел, инстанс или другой объект.

\n

Google Cloud Billing Catalog, например, описывает для SKU usage unit, base unit и tiered rates. Само наличие этих полей ещё не выбирает нужный SKU. Нужно сопоставить ресурс, регион, период, уровень тарифа и способ экспорта usage с конкретным счётом. При неизвестной ставке правильный результат — «стоимость не установлена», а не ноль.

\n

Почему зелёный SLO не закрывает вопрос о стоимости

\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 и LimitRangeQuota не равна свободной ёмкости
CPU низкий, p95 высокийОжидание сети, блокировка или throttling другого слояСопоставить latency, очередь, ошибки и traceНе уменьшать CPU по одному графику
После resize цена не совпала с модельюИзменён не тот объект тарификацииСверить SKU, регион, tier, скидку и количество часовРасчёт вернуть в статус гипотезы
\n

Метрики должны иметь окно и контекст

\n

Метрика без периода — число без проверяемого смысла. OpenTelemetry описывает metric event как измерение, время его фиксации и связанные метаданные. Поэтому рядом с p95 или usage нужно хранить хотя бы сервис, окружение, регион, версию, workload, окно и единицу. Иначе сравнение «до/после» может оказаться сравнением разных маршрутов или разных пиков.

\n

Для CPU полезно видеть usage вместе с throttling и очередью. Для памяти — пик, OOM-события и рабочий набор. Для сервиса — p95/p99, ошибки и поток запросов. Набор зависит от системы: эти поля не являются универсальным дашбордом. Смысл проверки в том, чтобы связывать качество с нагрузкой и выделением ресурса, а не объявлять причиной первую зелёную или красную линию.

\n

Есть и стоимость самой наблюдаемости. В OpenTelemetry число уникальных комбинаций атрибутов определяет cardinality; атрибуты вроде user ID или необработанного URL могут раздувать состояние метрик. Поэтому при добавлении labels нужно одновременно проверить объём хранения, лимиты backend и полезность разреза. «Добавим больше измерений» тоже имеет ресурсную и финансовую цену.

\n

Порядок проверки перед изменением ёмкости

\n
  1. Определите scope. Назовите сервис, окружение, регион, версию, число реплик и период. Не смешивайте production и нагрузочный стенд.
  2. Опишите workload. Запишите baseline и peak, RPS, размер сообщения, фоновые задачи и источник каждого значения.
  3. Разделите allocation и usage. Выпишите request, limit, quota и фактическое потребление по CPU и памяти. Не подставляйте процент usage вместо request.
  4. Зафиксируйте качество. Сохраните p95 или p99, error rate, очередь, throttling и OOM за то же окно. Назовите SLO и допустимый запас.
  5. Проверьте policy. Для Kubernetes посмотрите ResourceQuota, LimitRange, Events и allocatable узлов; для другой платформы найдите эквивалентные ограничения в официальной документации.
  6. Найдите billing unit. Сопоставьте SKU или тариф с ресурсом, регионом, периодом, ступенью, скидкой и общими затратами. Не ставьте неизвестные входы равными нулю.
  7. Сформулируйте один малый шаг. Измените один управляемый параметр, заранее запишите stop condition и обратную границу.
  8. Сравните одинаковые окна. После изменения проверьте тот же workload, качество, usage, allocation и счётные данные. Если условия разошлись, отметьте результат как несопоставимый.
  9. Примите ограниченный результат. Выберите следующий шаг только при сохранённом SLO и понятной цене. Иначе остановите сравнение, запишите пробел и назначьте владельца проверки.
\n

Ограничения применимости

\n

Эта модель не заменяет capacity planning, нагрузочное тестирование, договор с облачным провайдером или финансовый разбор. Она не учитывает автоматически autoscaling, cold start, сетевой egress, хранилище, резервирование, налоги, скидки, простой, стоимость лицензий и сопровождения. Каждый фактор может изменить итоговую цену или безопасный ресурсный предел.

\n

Учебные значения 120 RPS, 180 RPS, 235 мс, 1,2 CPU, 2 GiB, 0,36 и остальные числа не описывают реальный сервис. Их нельзя переносить в бюджет, SLO или заявку на изменение. Иллюстрация также не является инвойсом. Для реального решения нужны собственные метрики, разрешённый тариф и подтверждённое правило распределения общих расходов.

\n

Нельзя делать вывод о свободной ёмкости из одной квоты, о цене — из одного usage-графика, а о безопасности уменьшения — из среднего значения. Если период, единица или владелец неизвестны, остановка — полноценный результат диагностики. Следующий шаг должен вернуть недостающий факт, а не маскировать его приблизительным числом.

\n

Проверяемый результат

\n

Разбор готов к решению, когда в одной записи видны workload, окно, allocation, usage, SLO, billing unit, формула, stop condition и обратная граница. Другой инженер должен воспроизвести арифметику, понять, какой параметр меняется, и назвать сигнал, который остановит эксперимент. Если он видит только «CPU 35%» и «p95 зелёный», стоимость и безопасность изменения ещё не доказаны.

\n

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

\n

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

\n" } diff --git a/editorial/agent-rewrites/117.json b/editorial/agent-rewrites/117.json index 068b81c..cf02225 100644 --- a/editorial/agent-rewrites/117.json +++ b/editorial/agent-rewrites/117.json @@ -2,6 +2,6 @@ "index": 117, "slug": "editorial-2024-10-practice-capacity-cost", "title": "Ёмкость и стоимость: почему зелёный SLO не означает дешёвую систему", - "excerpt": "Как связать нагрузку, резерв ресурсов и единицу тарификации, чтобы рост расхода не маскировался выполненным SLO.", - "contentHtml": "

Сервис держит p95 на уровне 235 мс при целевом SLO 300 мс, но резерв CPU и памяти растёт каждую неделю. На графике задержка зелёная. В счёте появляется лишняя базовая ёмкость. Если принять зелёный SLO за доказательство эффективности, команда платит за резерв, которого не связывает ни с нагрузкой, ни с единицей тарификации.

\n

Ошибка возникает в месте, где смешивают четыре разные величины: поток запросов, выделенный ресурс, фактическое использование и цену. SLO отвечает за задержку или долю успешных запросов. Он не объясняет, сколько CPU зарезервировано и как провайдер выставляет счёт. Тезис статьи простой: стоимость ёмкости проверяют цепочкой workload → resource → billing unit → stop condition. Если звено пропущено, число на дашборде остаётся симптомом.

\n

Сначала разделите наблюдаемые факты

\n

Workload описывает поток работы: requests per second, размер сообщения, число фоновых задач и период измерения. Resource описывает выделение: replicas, CPU request, memory request, limit и квоту. Usage показывает фактическое потребление за интервал. Billing unit говорит, за что считают деньги: час инстанса, CPU-hour, GiB-hour, запрос, байт или составную единицу.

\n

Эти поля связаны, но не заменяют друг друга. Низкий usage не доказывает, что request можно уменьшить: запас может защищать от пика. Высокий usage не доказывает рост цены: тариф может быть фиксированным. Quota ограничивает суммарное потребление пространства имён, но сама по себе не является счётом. Сначала нужно назвать роль каждого значения.

\n
\"Схема
Учебная схема проверки: нагрузка задаёт контекст, ресурс фиксирует резерв, billing unit задаёт формулу, а предел останавливает сравнение. Иллюстрация не показывает реальный кластер, тариф или счёт.
\n

Механизм: SLO и стоимость смотрят на разные слои

\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.

\n

Теперь увеличим 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 и подтверждённая нагрузка.

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

Симптом → причина → проверка → действие

\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Сначала исправить контракт ресурса, затем считать стоимость
\n

Таблица задаёт порядок проверки, а не автоматическое решение. Один симптом может иметь несколько причин. Проверка должна исключить хотя бы очевидные альтернативы. Например, низкий CPU при большом p95 может указывать на ожидание сети или блокировку, а не на свободный запас вычислений. Уменьшение request в таком случае меняет риск, но не устраняет задержку.

\n

Что именно считать резервом

\n

В Kubernetes scheduler учитывает requests при размещении Pod. Limits задают отдельные ограничения выполнения. Поэтому запись «сервис использует 60% CPU» не говорит, что он занимает 60% оплачиваемой ёмкости. Нужно знать, от какой базы рассчитан процент и какую величину использует финансовая модель.

\n

Память требует отдельной осторожности. Краткий средний usage может скрыть редкий пик. Если процесс получает OOMKilled, зелёный средний график не спасает запросы. Для памяти полезнее сверять peak, рабочий набор, ошибки и время окна. Для CPU важны throttling, очередь и p95. Один общий порог utilisation не подходит обоим ресурсам.

\n

Quota и LimitRange задают допустимый диапазон на уровне политики. Они могут отклонить Pod с большим request или назначить значения по умолчанию. Но policy не знает цену часа инстанса. Она проверяет допустимость ресурса, а billing-система применяет свою формулу. Смешение этих слоёв рождает ложный вывод: «quota равна capacity» или «limit равен тарифу».

\n

Как выбрать stop condition

\n

Сравнение нельзя продолжать бесконечно. До расчёта назовите условие остановки. Для latency это может быть запас p95 до SLO. Для ресурса — сигнал saturation, throttling или нехватка памяти. Для стоимости — максимально допустимая дневная дельта. Для политики — отказ admission. Stop condition не говорит, какой вариант выбрать. Он говорит, когда следующий вариант нельзя считать продолжением того же эксперимента.

\n

В учебном примере зададим запас 10 мс. При p95 235 мс запас до SLO равен 65 мс, и сравнение двух соседних reservation допустимо как расчётная иллюстрация. Если p95 стал 295 мс, запас равен 5 мс. Следующий рост request уже требует отдельного решения о надёжности. Нельзя спрятать этот риск в таблице стоимости.

\n

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

\n
  1. Зафиксируйте scope и окно. Запишите сервис, регион, число реплик и интервал. Не сравнивайте сутки одного сервиса с часом другого.
  2. Опишите workload. Сохраните baseline и peak, p95, ошибки, размер сообщения и долю фоновой работы. Назовите источник каждого значения.
  3. Разделите request, limit и usage. Выпишите CPU и память отдельно. Не подставляйте процент utilisation вместо request.
  4. Проверьте политики. Прочитайте quota, LimitRange и правила admission. Убедитесь, что сравниваемый вариант вообще допустим.
  5. Назовите billing unit. Укажите фиксированную и переменную часть, тариф, tier, скидку и период. Если поле неизвестно, оставьте его неизвестным.
  6. Посчитайте два соседних варианта. Меняйте одну величину за раз. Сохраните формулу, вход и результат с единицами.
  7. Проверьте stop condition. Сверьте запас до SLO, saturation и допустимую дельту. При нарушении остановите подбор.
  8. Сформулируйте открытый вопрос. Укажите, какой владелец должен подтвердить тариф, нагрузку или риск. Без этого расчёт остаётся учебным.
\n

Ограничения и отрицательный путь

\n

Такая карточка не заменяет capacity planning. Она не моделирует autoscaling, cold start, сеть, хранилище, резервирование, скидки, burst-кредиты, простои и общие узлы. Она не доказывает, что меньший request безопасен. Она только не даёт связать цену с SLO напрямую.

\n

Отрицательный путь важнее удачного расчёта. Если метрики собраны за разные окна, остановитесь. Если тариф относится к узлу, а request — к Pod, не складывайте их без правила распределения. Если неизвестно число реплик в пике, не называйте дневную стоимость точной. Если policy изменилась после замера, пересчитайте вход. Не подставляйте ноль вместо неизвестного поля: это превращает отсутствие данных в ложную экономию.

\n

Не стоит запускать уменьшение ресурса только потому, что модель дала меньшую цифру. Сначала проверьте peak и p95 на том же окне, затем выполните изменение по обычному безопасному процессу, а после него сравните ошибки, задержку, throttling и billing. В этой статье нет production-результата и нет обещания экономии. Есть только критерии, по которым такой результат можно будет подтвердить.

\n

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

\n

Проверка готова, если второй инженер может по одной карточке ответить на пять вопросов: какой workload измеряли; какой resource зарезервирован; что означает usage; какая billing unit применена; при каком сигнале сравнение останавливается. Формула должна воспроизводиться на том же входе. Ссылки на тариф и политику должны быть доступны владельцу. При отсутствии любого ответа статус должен быть «данных недостаточно», а не «дешевле».

\n

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

\n" + "excerpt": "Практическая схема для случая, когда задержка укладывается в SLO, а счёт растёт: разделяем нагрузку, резерв, фактическое использование и единицу тарификации.", + "contentHtml": "

Сервис обрабатывает почти столько же запросов, что и месяц назад. Его p95 — 235 мс при целевом значении 300 мс, ошибки не выросли, но ежемесячный счёт за вычисления увеличился. На дашборде всё зелёное, поэтому первая гипотеза звучит неправильно: «раз SLO выполнен, система уже достаточно экономна».

\n

Зелёный SLO доказывает только соответствие выбранному показателю и окну измерения. Он не говорит, сколько ресурсов зарезервировано, сколько фактически потреблено и по какой единице провайдер выставляет счёт. Разберём учебный сценарий, в котором сначала разделим эти величины, затем сверим их командами Kubernetes и посчитаем два соседних варианта. Результат — не обещание экономии, а воспроизводимый способ понять, каких данных не хватает для решения.

\n

Начните с четырёх разных вопросов

\n

До изменения Deployment запишите четыре значения. Workload — сколько работы приходит: requests per second (RPS), размер сообщений, фоновые задачи и окно наблюдения. Allocation — что системе выделено: число реплик, CPU и memory в requests и limits. Usage — фактическое потребление за тот же интервал. Billing unit — что именно превращает ресурс в деньги: час виртуальной машины, CPU-hour, GiB-hour, запрос, байт или составной тариф.

\n

Каждый вопрос имеет другого владельца. Нагрузку подтверждает владелец сервиса или аналитики, allocation — манифест и платформа, usage — система метрик, billing unit — тариф и счёт провайдера. Если два источника используют одно слово «CPU», это ещё не делает их значения сопоставимыми. В частности, низкий usage не доказывает, что request можно без риска уменьшить, а высокий usage не доказывает, что цена вырастет: тариф может считать зарезервированный ресурс или фиксированный инстанс.

\n
\"Схема
Загрузка помогает искать насыщение, allocation задаёт вход модели, quota и limit ограничивают допустимый вариант, а billing unit переводит ресурс в стоимость. Схема учебная: она не показывает реальный кластер или тариф.
\n

Почему SLO не является ценником

\n

SLI (service level indicator) — измеряемый показатель услуги, например задержка или доля успешных запросов. SLO (service level objective) — целевое значение этого показателя. Если SLO сформулирован как «99% запросов быстрее 300 мс за сутки», он отвечает на вопрос о качестве для пользователя. Он не отвечает на вопрос, оплачивается ли CPU по факту потребления, по резерву или как часть фиксированного инстанса.

\n

Процентиль тоже требует контекста. p95 в 235 мс может быть рассчитан по одному региону и только по успешным запросам, а счёт — по всем регионам и часам работы. Поэтому рядом с p95 храните окно, фильтр запросов, регион и число реплик. Иначе зелёный показатель создаёт ложное ощущение сравнимости: мы сопоставляем качество одного среза с ценой другого.

\n

У SLO есть полезная роль в решении о ёмкости: он задаёт границу, после которой уменьшение ресурса нельзя считать приемлемым. Но граница должна включать и другие сигналы — ошибки, очередь, throttling CPU, пики памяти и время восстановления. SLO — контроль качества сервиса, а не доказательство низкой себестоимости.

\n

Что на самом деле делают requests, limits и quota

\n

В Kubernetes scheduler учитывает requests, когда выбирает узел: сумма запросов размещённых контейнеров должна помещаться в доступную ёмкость узла. Фактическое потребление в момент планирования может быть ниже. Это объясняет, почему свободный CPU на графике не превращается автоматически в возможность уменьшить request.

\n

Limit задаёт другой контракт. Для CPU это верхняя граница, применение которой может проявляться throttling. Для памяти превышение лимита может закончиться убийством процесса механизмом OOM. Поэтому предел нельзя использовать как синоним usage или стоимости. В некоторых конфигурациях admission-механизм подставляет request из limit, если request не задан; итоговый объект нужно читать после применения политик, а не угадывать по исходному YAML.

\n

ResourceQuota ограничивает суммарное потребление ресурсов и объектов в namespace. Квота может не пропустить новый Pod или изменение Deployment, но не сообщает цену часа. Она отвечает на вопрос «разрешён ли такой объём в namespace», а billing отвечает на вопрос «как этот объём тарифицируется». Эти проверки полезно выполнять вместе, но не смешивать их результаты.

\n

Диагностическая последовательность

\n

Сначала снимите allocation и политику, затем usage, и только после этого сравнивайте с инвойсом. Команды ниже не меняют кластер. Переменные задают namespace и Deployment; kubectl top требует установленного Metrics Server и показывает текущую оценку usage, а не платёжный документ.

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

Учебный расчёт двух соседних вариантов

\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 условной единицы за сутки.

\n
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 в формулу будет неверной даже при безошибочной арифметике.

\n

Симптом, проверка и безопасное действие

\n
НаблюдениеГипотезаЧто сверитьСледующее действие
SLO зелёный, счёт выросИзменились requests, реплики или тарифМанифест, число реплик, регион, строку инвойсаРазделить изменение allocation и изменение цены
Usage CPU ниже requestRequest оставляет запас на пик или задаёт размещениеПиковый RPS, очередь, throttling, расписаниеСравнить один меньший request на том же окне
Memory usage в среднем низкийРедкий пик скрыт агрегациейМаксимум, OOM, eviction и длительность окнаНе уменьшать memory только по среднему
Цена не изменилась после роста usageТариф фиксирован по инстансу или tierBilling unit и границы tierНе заменять счёт метрикой usage
Новый Pod не создаётсяQuota, LimitRange или admission отклонили объектСобытие namespace и итоговый Pod specИсправить допустимый диапазон до расчёта цены
\n

Таблица задаёт порядок исключения гипотез, а не готовую оптимизацию. Низкий CPU при высоком p95 может означать ожидание внешнего сервиса или блокировку. В таком случае меньший request не устраняет причину задержки и лишь уменьшает запас. Наоборот, рост стоимости при неизменном usage может быть полностью ожидаемым, если оплата идёт за фиксированный инстанс.

\n

Как выбрать границу остановки

\n

Сравнивайте соседние варианты, пока у эксперимента остаётся одинаковый workload и понятен риск. Для сценария задайте заранее: SLO p95 не выше 300 мс, error rate не растёт, нет нового throttling, memory peak не приближается к лимиту, а дневная дельта модели не выше согласованного порога. Это не универсальные значения, а поля конкретного эксперимента.

\n

В учебном расчёте p95 235 мс оставляет запас 65 мс. Если после изменения он стал 295 мс, запас сократился до 5 мс, даже если модель обещает экономию. Если выросли ошибки или появился OOM, эксперимент нужно остановить независимо от зелёного среднего графика. Граница должна быть измерима другим инженером: «кажется, стало хуже» для неё недостаточно.

\n

Порядок проверки перед изменением

\n
  1. Зафиксируйте scope. Укажите сервис, namespace, регион, реплики, тип workload и точное окно.
  2. Снимите baseline. Сохраните RPS, p50/p95/p99, error rate, peak memory, CPU throttling и текущий Deployment.
  3. Разделите величины. Запишите requests, limits и usage отдельными полями с единицами и источниками.
  4. Проверьте допустимость. Прочитайте ResourceQuota, LimitRange и события admission; убедитесь, что новый объект может быть размещён.
  5. Назовите billing unit. Подтвердите, считается ли резерв, usage, целый инстанс, запрос или tier. Если правило неизвестно, пометьте стоимость неизвестной.
  6. Измените одну величину. Не меняйте одновременно request, replicas, тип инстанса и autoscaling: иначе причинность потеряется.
  7. Сравните то же окно. Проверьте SLO, ошибки, saturation и счёт в одинаковом scope. Примените stop condition.
  8. Сохраните решение. Запишите результат, оставшийся риск, владельца тарифа и план отката.
\n

Границы применимости

\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

Критерий готовности

\n

Решение можно передавать на изменение, если по одной карточке видны workload, allocation, usage и billing unit; у каждого поля есть источник и окно; формула воспроизводится; SLO и технические stop condition определены; quota и admission не блокируют вариант; владелец тарифа подтвердил финансовую часть. Если хотя бы один пункт отсутствует, разрешённый результат — продолжить сбор данных, а не объявить систему дешёвой.

\n

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

\n" } diff --git a/editorial/agent-rewrites/118.json b/editorial/agent-rewrites/118.json index b67f7fc..e6e959b 100644 --- a/editorial/agent-rewrites/118.json +++ b/editorial/agent-rewrites/118.json @@ -2,6 +2,6 @@ "index": 118, "slug": "editorial-2024-09-field-adr-decisions", "title": "ADR устарел: как проверить решение и не переписать его историю", - "excerpt": "Старый ADR не становится неверным только потому, что изменился код. Разбираем признаки drift, проверку assumptions, successor-запись и безопасный переход от Accepted к Superseded.", - "contentHtml": "

Через несколько месяцев после принятия ADR команда открывает его перед изменением сервиса. В документе описан синхронный экспорт для небольшого запроса. В коде уже появился фоновый worker и endpoint со статусом операции. Один разработчик предлагает просто исправить старый текст: заменить «синхронный экспорт» на «фоновый экспорт» и оставить прежнюю дату.

\n

Симптом виден сразу: ADR, код и текущий вопрос описывают разные границы системы. Цена ошибки выше, чем кажется. Команда теряет причину прежнего выбора, не видит принятые компромиссы и может повторно принять уже отвергнутый вариант. При аварии становится неясно, к какому решению возвращаться. При проверке доступа или миграции нельзя отличить старое обязательство от нового предложения.

\n

Тезис прост: ADR фиксирует принятое решение в конкретном контексте, но не проверяет систему сам. Когда меняется assumption или constraint, старую запись нужно сохранить, а новое решение оформить как successor. Только после его принятия старый ADR можно связать с ним статусом Superseded. Так история остаётся читаемой, а проверка не маскируется под редактирование документа.

\n

Что именно хранит ADR

\n

ADR отвечает на четыре вопроса: какая проблема наблюдалась, какое решение выбрали, какие альтернативы рассмотрели и какие последствия приняли. Поля Status, Context, Decision и Consequences образуют минимальный каркас. В расширенном шаблоне рядом появляются владельцы, факторы выбора и способ проверки.

\n

Запись не является приказом навсегда. Она говорит: «при этих условиях мы выбрали этот вариант». Условие может измениться из-за нового контракта, класса данных, требования к времени ответа, стоимости отказа или исчезновения владельца. Сам факт изменения кода ещё ничего не доказывает. Нужно показать, какое условие перестало выполняться.

\n

Полезно разделять три объекта. ADR хранит rationale и границу решения. Тест или запрос к метрикам даёт evidence по конкретному вопросу. План изменения описывает выкладку, откат и наблюдение. Ни один объект не заменяет два других.

\n

Симптом → причина → проверка → действие

\n
Признаки устаревания ADR и безопасное первое действие
СимптомПричинаПроверкаДействие
Код больше не совпадает с границей ADRИзменился контракт или способ выполненияСопоставить diff с разделами Context и DecisionСоздать Proposed successor, старый текст не менять
В записи есть assumption, но нет факта о её состоянииADR приняли за автоматическую проверкуНазвать источник, период, среду и результат, который опровергнет assumptionОткрыть отдельную validation task
Наступила дата review, но новых данных нетКалендарный срок подменил сигналПроверить ссылки, владельца, ограничения и evidence gapReconfirm или запланировать проверку; не ставить Superseded
Старое решение кажется «неудобным»Последствия стали дороже или изменился приоритетЗаново сравнить альтернативы по текущим критериямЗаписать новый компромисс и цену миграции
Нужно отменить изменениеSuccessor ещё не принят или его проверка не закрытаПроверить статус и ссылку на прежний Accepted ADRВернуться к известной записи, не стирать историю
\n

Механизм reassessment

\n

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

\n

Новый факт ещё не выбирает архитектуру. Сравните варианты: оставить синхронный путь, добавить очередь со статусом или запустить неограниченную фоновую работу. У каждого варианта есть владелец статуса, путь восстановления, поведение при повторе и цена поддержки. Если критерий «вызывающая сторона должна видеть статус» обязателен, первый вариант отпадает. Если нет владельца очереди и проверки восстановления, второй остаётся только предложением.

\n

Отрицательный путь важен. Если проверка показала, что новый код не отменяет исходную границу, не создавайте successor ради даты review. Зафиксируйте результат проверки и подтвердите прежнее решение. Если evidence отсутствует, не превращайте отсутствие ошибки в доказательство пригодности. Статус должен остаться Proposed, пока ответственный не согласовал контекст и способ проверки.

\n

Условный пример с кодом

\n

Ниже приведена условная модель. Она не читает репозиторий, не меняет ADR и не доказывает свойства реальной очереди. Функции только показывают порядок состояний: сначала формируется предложение, затем отдельная проверка возвращает его без автоматического принятия.

\n
const 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 в примере не означает, что такой контроль уже существует.

\n
\"Цикл
Цикл reassessment: сигнал приводит к проверке контекста, ссылки на код и границы доказательства. Если assumption не выдерживает проверку, создаётся Proposed successor. Старый ADR получает Superseded только после принятия нового.
\n

Как связать запись с кодом и проверкой

\n

Ссылка из ADR должна вести к устойчивой границе: контракту, схеме, модулю или отдельному тесту. Ссылка не подтверждает соответствие сама по себе. Для каждого важного assumption задайте проверяемый вопрос. Например: «видит ли вызывающая сторона три состояния операции?» Ответ должен иметь источник, среду, период и критерий остановки.

\n

Метрика отвечает только на тот вопрос, для которого её собрали. Низкая доля ошибок не подтверждает путь восстановления. Высокая пропускная способность не доказывает корректность прав доступа. Тест схемы не доказывает стоимость эксплуатации. Если один источник не покрывает риск, запишите это как ограничение, а не как скрытое условие готовности.

\n

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

\n
  1. Откройте Accepted ADR и выпишите Context, Decision, Consequences, owner, ссылки и исходные assumptions.
  2. Назовите один наблюдаемый сигнал: изменился контракт, предел запроса, класс данных, владелец или требование к восстановлению.
  3. Сверьте сигнал с кодом и текущим контрактом. Отделите факт от предположения и сохраните источник проверки.
  4. Определите evidence boundary: кто проверяет, где, за какой период, каким артефактом и какой результат опровергнет assumption.
  5. Создайте successor в статусе Proposed. Добавьте ссылку на старый ADR, альтернативы, последствия, стоимость отката и критерий проверки.
  6. Проведите review. Если новое решение принято, свяжите записи и поставьте старой Superseded. Если отклонено, сохраните причину и оставьте старый ADR действующим.
  7. Проведите изменение отдельно: тесты, разрешения, выкладка, наблюдение, stop condition и rollback.
\n

Если старой записи нет

\n

Не восстанавливайте прошлые мотивы по одному фрагменту кода. Составьте текущую запись: что система делает, какие факты доступны, какие неизвестны и кто отвечает за следующий вопрос. Доступный commit или тикет можно указать источником наблюдения. Нельзя выдавать его за доказательство первоначального rationale.

\n

После срочного исправления особенно легко назвать временный обход Accepted архитектурой. Запишите срок действия, риск и условие удаления. Если команда не может назвать альтернативы и последствия, решение ещё не готово. Это честнее, чем создавать уверенную историю задним числом.

\n

Ограничения и критерий готовности

\n

ADR не запускает миграцию, не заменяет threat model, benchmark, тест-план, runbook или incident review. MADR и исходная форма Nygard предлагают структуру, но не устанавливают универсальные сроки review, роли согласования и веса критериев. Периодическая дата полезна только вместе с сигналом. Нельзя объявлять решение устаревшим из-за одной даты или одной метрики.

\n

Проверяемый критерий готовности таков: для одного текущего ADR видны исходная assumption, подтверждающий источник, владелец проверки, отрицательный результат и действие при нём. Если нужен successor, он содержит ссылку назад, альтернативы, последствия, способ проверки и статус Proposed. Связь Superseded появляется только после явного принятия successor. После этого отдельный change plan проходит свои тесты и имеет путь отката.

\n

Если хотя бы одного элемента нет, результатом должна быть открытая проверка или статус Proposed. Это не незавершённость документа. Это точное описание границы знания команды.

\n

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

" + "excerpt": "Старый ADR не становится неверным только потому, что изменился код. Разбираем drift, successor-запись, confirmation и безопасный переход от Accepted к Superseded.", + "contentHtml": "

Проблема в актуальности ADR проявляется, когда репозиторий открывают перед изменением сервиса. В записи зафиксирован синхронный экспорт: клиент ждёт ответ с готовым файлом. В коде уже появился worker, а endpoint возвращает jobId и состояния pending, completed и failed. Возникает соблазн заменить в старом файле пару слов и оставить прежнюю дату.

\n

Такой diff скрывает две разные операции. Первая — уточнить evidence: решение всё ещё подходит, а наблюдение или ссылка стали точнее. Вторая — изменить архитектурную границу: теперь клиент управляет длительной операцией и должен уметь повторно получить её результат. Во втором случае правка старой записи стирает причину прежнего выбора и лишает команду точки сравнения. Разберём, как отличить эти случаи и оформить successor — новую запись, которая заменяет прежнюю.

\n

ADR хранит решение в его контексте

\n

Architectural Decision Record — запись одного архитектурно значимого решения. Официальный сайт ADR описывает три опорные идеи: решение отвечает значимому требованию, сохраняет rationale и показывает trade-offs и последствия. Это не снимок текущего кода и не обещание, что выбранный вариант навсегда останется лучшим.

\n

Практический минимальный каркас выглядит так: Status, Context, Decision и Consequences. В Context попадает проблема и условия выбора. Decision называет выбранную границу. Consequences показывает, что стало легче, а что — дороже или рискованнее. MADR 4.0.0 расширяет каркас decision drivers, рассмотренными вариантами и способом confirmation — подтверждения того, что реализация соответствует записи.

\n

Из этого следует полезное разделение. ADR фиксирует rationale. Исходный код и контракт показывают, что система делает сейчас. Тест, запрос к метрике или результат review отвечает на конкретный вопрос о соответствии. План изменения описывает rollout, наблюдение и rollback. Ссылка из ADR на файл не заменяет ни проверку, ни план.

\n

Drift не равен устаревшему решению

\n

Сначала выпишите исходное условие, а не название технологии. В нашем примере оно звучит так: «маленький экспорт завершается в рамках запроса, а вызывающая сторона получает файл сразу». Затем соберите новый факт: «время выполнения превышает допустимое ожидание, поэтому сервер возвращает идентификатор операции, а клиент опрашивает её состояние».

\n

Если новый факт только уточняет ссылку, владельца или измерение, accepted ADR можно дополнить датированной заметкой по правилам команды. Если изменились требование, assumption или граница ответственности, нужен новый ADR. В нём следует сослаться на старый, описать варианты и объяснить цену перехода. Статус старой записи меняется на superseded только после принятия successor. Это правило статьи — безопасная политика append-only; конкретный проект может выбрать другую процедуру и обязан описать её заранее.

\n
Диагностика расхождения между ADR и текущей системой
НаблюдениеЧто могло изменитьсяПроверкаСледующий артефакт
В ADR синхронный ответ, в контракте jobIdГраница времени и владелец состояния операцииСравнить версию контракта, обработчик и retry-путьSuccessor в статусе Proposed
Ссылка ведёт на удалённый модульАртефакт переехал, но решение не изменилосьНайти новый устойчивый путь и проверить тот же инвариантДатированное обновление evidence
Появился новый класс данныхИзменились требования безопасности или храненияПроверить threat model, права и срок храненияНовый ADR либо отклонённая альтернатива
При review нет факта о consequenceКалендарная дата подменила проверкуНазначить owner, источник, среду и критерий опроверженияValidation task, а не новый статус
Команда хочет «починить текст» после откатаОткат реализации перепутан с отменой решенияСверить принятый successor и фактический rollbackСохранённый Accepted ADR или явный Rejected ADR
\n

Четыре вопроса перед созданием successor

\n

Что изменилось? Запишите наблюдаемый сигнал: новый endpoint, лимит времени, обязанность хранить статус, класс данных или исчезнувший владелец. Формула «код стал другим» недостаточна, потому что код мог изменить реализацию внутри прежней границы.

\n

Какая assumption нарушена? Assumption — условие, на котором держался выбор. Для синхронного экспорта это может быть верхняя граница времени ответа. Для очереди — наличие владельца retry и понятного срока хранения. Назовите условие числом или проверяемым правилом, если это возможно.

\n

Какие варианты остаются? Сравните минимум два реалистичных варианта. Для экспорта это синхронный путь с жёстким лимитом, очередь с видимым статусом или передача работы внешнему сервису. Критерии должны быть связаны с проблемой: время ответа, восстановление после сбоя, права доступа, стоимость сопровождения и обратная совместимость.

\n

Что подтвердит выбор? Confirmation должен отвечать на один конкретный вопрос. Например: «контракт позволяет клиенту получить итог после временного сетевого сбоя, не создавая второй экспорт». Укажите тест, запрос или review, среду, владельца и результат, который заставит пересмотреть решение.

\n

Воспроизводимый учебный пример

\n

Ниже — самодостаточная fixture на Node.js. Она не читает репозиторий и не доказывает свойства реальной очереди. Её задача — сделать правило перехода явным: изменение протокола считается сигналом для successor, но старый Accepted ADR не переводится автоматически.

\n
node --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. Учебная программа лишь защищает от логической ошибки «новый контракт найден — старую историю можно переписать».

\n
\"Схема
Сначала фиксируется сигнал и проверяется исходная assumption. Если граница выдержана, ADR подтверждают новыми данными. Если нет, создают Proposed successor; статус старой записи меняют только после явного принятия нового решения.
\n

Как оформить successor без потери истории

\n

Создайте новую запись рядом со старой и поставьте ей Proposed. В заголовке назовите решаемую проблему и выбранную границу, а не внутреннее имя очереди. Минимальная структура может выглядеть так:

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

\n

Evidence не превращается в гарантию

\n

У evidence есть субъект, область и срок действия. Commit показывает состояние кода в конкретной версии. Контрактный тест показывает допустимые формы запроса и ответа. Метрика показывает измеренное поведение при выбранной нагрузке и окне наблюдения. Review подтверждает согласование, но не заменяет эксплуатационную проверку.

\n

Нельзя делать следующий скачок без отдельного доказательства: «jobId есть в схеме» не означает, что операция переживает повтор; «ошибок мало» не означает, что recovery безопасен; «ADR связан с PR» не означает, что rollout можно откатить. В ADR полезно писать и отрицательный результат: какой риск не проверен, кто его проверит и какое наблюдение остановит выпуск.

\n

Для локального поиска можно начать с таких команд, подставив пути своего проекта:

\n
rg -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 автоматически: совпадение слов не доказывает совпадение смысла, а отсутствие совпадения не доказывает, что решение нарушено.

\n

Порядок действий для команды

\n
  1. Сохраните копию исходного Accepted ADR и выпишите его Context, Decision, Consequences, assumptions, владельца и ссылки.
  2. Зафиксируйте один наблюдаемый сигнал и версию артефакта, в котором он обнаружен.
  3. Сопоставьте исходную границу с кодом, API-контрактом, правами и эксплуатационным маршрутом. Разделите факт, гипотезу и пробел evidence.
  4. Если граница не изменилась, обновите evidence датированной записью и назначьте следующую проверку. Не создавайте successor только из-за календарной даты.
  5. Если граница изменилась, опишите минимум два варианта, decision drivers, последствия, стоимость миграции и условия rollback.
  6. Создайте successor в Proposed, добавьте обратную ссылку и назначьте decision-makers и confirmation.
  7. Проведите review. Только после принятия свяжите записи и переведите старый ADR в Superseded по правилам проекта.
  8. Проведите реализацию отдельным change plan: тесты, rollout, наблюдение, stop condition и rollback.
\n

Ограничения применимости

\n

ADR не является RFC, тестовым раннером, threat model, benchmark, runbook или системой управления изменениями. Официальные шаблоны предлагают язык и поля, но не назначают универсальные сроки review, обязательный набор ролей или порог для создания записи. Статусы Accepted, Rejected и Superseded должны иметь однозначное значение в политике конкретного репозитория.

\n

Append-only подход снижает риск переписать rationale, но увеличивает число записей и требует навигации между ними. Живой документ может быть удобнее для команды, однако тогда нужны датированные изменения и видимая история. Ни один вариант не спасает от неверного Context или отсутствующего owner. Нельзя объявлять решение корректным по одной ссылке, одной метрике или успешному запуску fixture.

\n

Для описанного случая критерий готовности таков: видны исходная assumption, новый сигнал, выбранные варианты, consequence, владелец confirmation и результат, который опровергнет решение. Если не хватает хотя бы одного элемента, честный итог — открытая проверка или Proposed successor, а не отредактированная история.

\n

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

" }