From 2ebe23662424e0a77ce19f65ee9afc305e40dc41 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 16:14:47 +0300 Subject: [PATCH] editorial: revise articles 125-130 to 10/10 --- editorial/agent-rewrites/125.json | 6 +++--- editorial/agent-rewrites/126.json | 4 ++-- editorial/agent-rewrites/127.json | 6 +++--- editorial/agent-rewrites/128.json | 2 +- editorial/agent-rewrites/129.json | 3 ++- editorial/agent-rewrites/130.json | 4 ++-- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/editorial/agent-rewrites/125.json b/editorial/agent-rewrites/125.json index 61f823a..d43fb10 100644 --- a/editorial/agent-rewrites/125.json +++ b/editorial/agent-rewrites/125.json @@ -1,7 +1,7 @@ { "index": 125, "slug": "editorial-2024-07-mechanism-release-engineering", - "title": "Инженерия релиза: как связать артефакт, миграцию и откат", - "excerpt": "Один номер версии не доказывает, что команда выпускает нужный код. Разбираем цепочку commit → artifact → migration → rollout и ставим проверяемый стоп перед ошибочным deploy.", - "contentHtml": "

После выкладки сервис отвечает старым поведением, хотя в CI и карточке релиза стоит одна версия — 2024.07.0. Откат возвращает прежний контейнер, но ошибка в данных остаётся. Команда повторяет deploy, меняет таймаут и смотрит на зелёный статус job. Это не исправляет расхождение. Цена ошибки — потерянное время, спор о том, что именно работает, и риск усугубить миграцию данных.

\n

Тезис простой: релиз нужно проверять как цепочку связей, а не как строку с версией. Commit должен быть источником артефакта. Rollout должен ссылаться на точный digest артефакта. Миграция должна называть целевую версию и границу совместимости. Для возврата нужно заранее назвать версию и digest. Если хотя бы одна связь не сходится, процесс останавливается до deploy.

\n

Механизм: четыре связи вместо одного тега

\n

Тег отвечает на вопрос «как назвали выпуск». Он не отвечает на вопросы «из какого commit собрали образ», «какой образ запросил rollout» и «для какой схемы написана миграция». Для этих вопросов нужны неизменяемые значения и явные предикаты.

\n\n

Эти условия проверяют согласованность записей. Они не доказывают, что deploy завершился, что registry доступен или что миграция обратима. Execution result и release evidence — разные вещи. Успешный rollout может работать с неправильным артефактом. Согласованный record может ещё не быть разрешением на выкладку.

\n

Учебный пример расхождения

\n

Ниже — синтетические записи. Они не получены из production и не описывают реальную доставку.

\n
const commit = {\n  id: 'synthetic-commit-91',\n  releaseVersion: '2024.07.0'\n};\n\nconst artifact = {\n  digest: 'sha256:synthetic-artifact-42',\n  sourceCommitId: 'synthetic-commit-other-91'\n};\n\nconst migration = {\n  targetReleaseVersion: '2024.07.0',\n  compatibleWith: '2024.06.x'\n};\n\nconst rollout = {\n  requestedArtifactDigest: 'sha256:synthetic-artifact-42'\n};\n\nconst checks = {\n  sourceMatches: artifact.sourceCommitId === commit.id,\n  artifactMatches: rollout.requestedArtifactDigest === artifact.digest,\n  migrationMatches: migration.targetReleaseVersion === commit.releaseVersion\n};\n\nconst canDeploy = Object.values(checks).every(Boolean);\n// false: остановить процесс и сверить записи\n
\n

Две проверки проходят. Артефакт и rollout называют один digest, миграция нацелена на правильную версию. Но source commit не совпадает. Поэтому canDeploy равен false. Нельзя делать вывод, что контейнер содержит код из synthetic-commit-91. Нельзя лечить это повторным запуском того же deploy. Сначала нужно найти источник расхождения и заново зафиксировать запись.

\n

Обратный путь важен не меньше. Возврат контейнера к предыдущему digest не отменяет изменение схемы или данных. Если миграция уже прошла, прежний код может не поддерживать новую схему. В карточке возврата нужно разделить два действия: вернуть code artifact и решить, что делать с data effect. Если второго решения нет, честный статус — «возврат артефакта подготовлен, откат данных не определён».

\n
\"Связи
Учебная схема показывает границу: code rollback возвращает названный артефакт, но не обещает отмену миграции.
\n

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

\n
Диагностика рассогласованного релиза
СимптомПричинаПроверкаДействие
Везде одна версия, но поведение разноеТег используют как единственный идентификаторСравнить exact commit id и artifact sourceCommitIdОстановить выпуск и пересобрать evidence chain
Rollout зелёный, но загружен не тот образКарточка хранит tag вместо digestСравнить requestedArtifactDigest с digest артефактаИсправить intent record, не повторять deploy
После возврата код падает на данныхRollback контейнера приняли за rollback данныхПроверить target schema, compatibility и migration statusПередать data effect отдельному владельцу и остановить автоматический возврат
Миграция прошла для другой версииПлан миграции следует ветке или последнему mainСравнить targetReleaseVersion с release.versionЗакрыть gate и выпустить новый migration review
Невозможно объяснить, что вернётсяReturn point описана словом «предыдущий»Проверить конкретные version и digestНе давать approval, пока точка возврата не названа
\n

Таблица полезна только тогда, когда каждая проверка имеет владельца и stop action. Строка «все jobs зелёные» недостаточна: она не связывает job с содержимым артефакта и контрактом данных. Строка «digest совпал» тоже недостаточна: она не подтверждает доступность сервиса после выкладки. Не смешивайте semantic consistency с результатом исполнения.

\n

Почему миграция меняет смысл отката

\n

У релиза есть как минимум два состояния: code state и data state. Deployment обычно управляет шаблоном Pod или другим runtime artifact. Миграция меняет схему, записи или внешний контракт. Эти операции могут иметь разные владельцы, журналы и способы возврата.

\n

Безопасный порядок требует compatibility window. Новый код сначала должен работать со старой и новой формой данных, если это возможно. Затем миграция меняет данные. После проверки трафика команда может удалить старую ветку совместимости. В такой схеме возврат на старый код возможен только до закрытия окна. После него нужен отдельный план: обратная миграция, восстановление из backup или сохранение нового кода с исправлением.

\n

Это не универсальная стратегия миграций. Некоторые изменения нельзя отменить. Некоторые системы разрешают только forward migration. Статья не утверждает, что любой Kubernetes Deployment или любой image digest можно безопасно вернуть. Она требует назвать границу действия и не приписывать rollback то, чего он не делает.

\n

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

\n
  1. Зафиксируйте release version и owner проверки. Owner отвечает за сравнение записей, но это не делает его автоматически исполнителем deploy.
  2. Проверьте связь commit → artifact. Сравните exact id. Название ветки, последний merge и номер задачи не заменяют идентификатор commit.
  3. Проверьте artifact → rollout. Сравните immutable digest. Не подставляйте digest по тегу и не считайте совпадение имён доказательством.
  4. Опишите migration target и compatibility. Назовите версию, допустимый предыдущий контракт и отдельный data effect.
  5. Проверьте все предикаты. При первом false верните статус stop-and-reconcile-records. Не запускайте новую попытку ради зелёного job.
  6. Подготовьте return point. Запишите version и digest артефакта для возврата. Рядом укажите, что произойдёт с данными.
  7. Отделите approval от исполнения. Проверка записи разрешает перейти к авторизованному review, но сама не вызывает registry, cluster или deploy runner.
\n

Ограничения

\n

Эта модель не проверяет настоящий Git history, подпись, identity builder, provenance, registry, environment configuration, secrets, права доступа, database state, трафик и telemetry. Она не выдаёт уровень SLSA и не доказывает, что конкретный attestation заслуживает доверия. Для этого нужны отдельные политики, хранилища и проверяющие компоненты.

\n

Учебный код также не является CI-конфигурацией. Синтетические id и digest нужны, чтобы показать рассуждение на закрытом наборе данных. Реальные значения нельзя подменить в этом примере и затем считать результат производственным evidence. Практический перенос начинается с одного разрешённого release record и read-only проверки, а не с подключения fixture к deploy.

\n

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

\n

Релиз готов к авторизованному review, если второй проверяющий без устных пояснений находит в одной записи:

\n\n

Проверяющий должен назвать результат каждой связи: true или false, stop action при false и владельца следующего вопроса. Если он может только сказать «job зелёный», критерий не выполнен. Это проверяемый предел статьи: согласовать записи до действия, не объявить production success.

\n

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

" + "title": "Инженерия релиза: как доказать связь артефакта, миграции и отката", + "excerpt": "Одинаковая версия в CI не доказывает, что будет доставлен нужный код. Разбираем цепочку commit → artifact → migration → rollout и ставим проверяемый stop до deploy.", + "contentHtml": "

Симптом после выкладки: сервис отвечает старым поведением, хотя в CI и карточке релиза написано 2024.07.0. Команда повторяет deploy, увеличивает таймаут и смотрит на зелёный job. Если перед этим миграция изменила данные, откат контейнера может вернуть старый код, но не вернуть прежнее состояние базы. В итоге спорят не о причине сбоя, а о том, какой именно артефакт вообще работает.

\n

Номер версии удобен человеку, но слаб как единственное доказательство. Надёжнее проверять цепочку: заявленный commit связан с provenance сборки, rollout указывает на неизменяемый digest образа, миграция называет целевую границу совместимости, а для возврата записана конкретная ревизия. Это не разрешение на выкладку и не обещание успешного production deploy. Это stop-проверка, которая не даёт продолжить при расхождении записей.

\n

Что должна доказать запись о релизе

\n

Сначала разделим четыре разных вопроса. Источник. Из какого точного commit получены входы сборки? Артефакт. Какой digest соответствует результату сборки? Изменение данных. Какую схему или форму записи ожидает новая версия? Исполнение. Какой digest запросил rollout и чем подтверждено его завершение?

\n

Эти вопросы связаны, но не взаимозаменяемы. Provenance — это подписанная или иным образом проверяемая информация о том, как получен артефакт; она не заменяет политику потребителя. Digest идентифицирует содержимое образа, но не подтверждает, что образ запущен в нужном окружении. Успешный статус rollout говорит о состоянии Deployment, а не об обратимости миграции. Поэтому release record должен хранить отдельные поля, а проверка — отдельные результаты.

\n
Границы доказательств в цепочке релиза
СвязьПроверяемое утверждениеЧто не следует из успеха проверки
commit → provenanceВ provenance зафиксирован ожидаемый источник или dependency с точным идентификатором.Сборка безопасна от всех атак и полностью воспроизводима.
provenance → artifactSubject attestation относится к конкретному артефакту, а signer и builder входят в доверенную политику.Артефакт без уязвимостей и подходит каждому окружению.
artifact → rolloutВ намерении выкладки указан тот же immutable digest.Контейнер уже запущен и прошёл проверки доступности.
migration → releaseМиграция называет совместимую целевую версию и известный data effect.Миграцию можно безопасно отменить одной командой.
return point → rollbackНазваны точные version, digest и действие для данных.Старый код поддерживает новую схему после закрытия окна совместимости.
\n

Маленький gate на синтетических данных

\n

Ниже — самодостаточная проверка для командной строки. Записи вымышлены: значение примера в том, что одна связь намеренно сломана. Файл не вызывает registry, cluster или deploy runner; он только вычисляет логический результат из локального release record.

\n
cat > release-record.json <<'JSON'\n{\n  \"releaseVersion\": \"2024.07.0\",\n  \"commit\": { \"id\": \"commit-91\" },\n  \"provenance\": {\n    \"sourceCommitId\": \"commit-91\",\n    \"artifactDigest\": \"sha256:artifact-42\",\n    \"builderId\": \"ci.example/build\"\n  },\n  \"rollout\": { \"requestedDigest\": \"sha256:artifact-other\" },\n  \"migration\": {\n    \"targetVersion\": \"2024.07.0\",\n    \"compatibleWith\": [\"2024.06.x\"],\n    \"dataEffect\": \"adds nullable profile.locale\"\n  },\n  \"returnPoint\": {\n    \"version\": \"2024.06.4\",\n    \"digest\": \"sha256:previous-17\",\n    \"dataAction\": \"keep forward-compatible column\"\n  }\n}\nJSON\n\njq -e '\n  .provenance.sourceCommitId == .commit.id\n  and .provenance.artifactDigest == .rollout.requestedDigest\n  and .migration.targetVersion == .releaseVersion\n  and (.returnPoint.version != \"\" and .returnPoint.digest != \"\")\n' release-record.json\n# jq: false, exit code 1 — rollout digest не совпал\n
\n

Команда завершится с кодом 1, потому что sha256:artifact-other не равен digest, записанному в provenance. Три прочие части примера согласованы, но этого недостаточно: gate должен остановить действие при одном false. После исправления записи полезно повторить ту же команду и отдельно проверить, что исполнение rollout действительно завершилось. Логический true не подменяет runtime evidence.

\n
\"Схема
Схема разделяет проверку кода и проверку данных: возврат Deployment касается шаблона Pod, но не обещает отменить уже выполненную миграцию.
\n

Как читать расхождение

\n

Если в карточке и в CI одна версия, а поведение разное, первым делом не запускайте новый deploy. Получите фактический digest запущенного образа и сравните его с digest в намерении выкладки. Затем сопоставьте provenance с ожидаемым commit и доверенным builder. Здесь важно не восстановить «примерно ту же сборку», а найти конкретную точку, где цепочка перестала быть доказуемой.

\n

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

\n

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

\n

Почему откат образа не откатывает данные

\n

У приложения есть минимум два состояния: кодовый артефакт и состояние данных. Kubernetes Deployment хранит историю ревизий и позволяет вернуть Pod template к предыдущей ревизии, если она ещё доступна. Это полезно при проблеме с образом или параметрами запуска. Но команда kubectl rollout undo не отменяет SQL-миграцию, изменение документа или уже отправленное внешнему сервису событие.

\n

Поэтому миграцию стоит проектировать с окном совместимости, когда новый код умеет читать старую и новую форму данных. Сначала выкладывается код, способный работать в этом окне, затем выполняется изменение данных, после наблюдения удаляется старая ветка. Конкретный порядок зависит от хранилища и миграционного инструмента; это не универсальная лицензия на обратную миграцию.

\n

До закрытия окна точка возврата может быть обычным предыдущим артефактом. После закрытия нужно отдельное решение: forward fix, обратная миграция, восстановление резервной копии или сохранение нового кода с исправлением. В release record это должен быть явный dataAction, а не слово «откат» без объекта действия.

\n

Порядок проверки перед approval

\n
  1. Снимите исходные идентификаторы. Запишите release version, точный commit SHA, digest артефакта, builder и окружение. Ветка, номер задачи и tag остаются удобными ссылками, но не заменяют SHA и digest.
  2. Проверьте provenance. Сверьте subject артефакта, source или dependency, signer и builder с политикой команды. Если используется attestation, проверяйте её криптографически, а не только открывайте страницу с метаданными.
  3. Сверьте намерение и исполнение. В manifest или release record должен быть тот же digest, который был разрешён. После deploy сохраните отдельное подтверждение состояния и времени rollout.
  4. Опишите data effect. Назовите таблицу, поля, документы или события, которые изменятся, а также допустимую старую форму. Не ставьте approval, если миграция существует только как название job.
  5. Назовите return point. Укажите конкретные version и digest для кода и отдельное решение для данных. Проверьте, что требуемая ревизия не удалена политикой хранения истории.
  6. Остановитесь при первом false. Верните запись на reconcile, не запускайте повторную попытку ради зелёного CI. Approval относится к проверенной записи; он не является доказательством фактического успеха выкладки.
\n

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

\n

Этот маршрут подходит как минимальный контроль согласованности для релиза, где команда может получить commit, provenance, digest, план миграции и запись rollout. Он не заменяет сканирование уязвимостей, review кода, проверку секретов, контроль прав, тесты совместимости, резервное копирование, мониторинг или процедуру incident response.

\n

SLSA описывает модель provenance и требования к её проверке, но не объявляет конкретный артефакт безопасным. GitHub отдельно предупреждает, что artifact attestation связывает артефакт с источником и инструкциями сборки, а решение о доверии требует собственной политики. В частном registry, другой CI-системе или без доверенного корня проверки команды и поля будут другими.

\n

Синтетический release-record.json нельзя подключать к настоящему deploy без адаптации схемы, прав и источников фактов. Kubernetes-команды требуют доступа к конкретному кластеру и работают с историей, которую можно ограничить настройкой revisionHistoryLimit. Если миграция необратима или внешний эффект уже ушёл, честный результат может быть «код возвращён, data effect остаётся».

\n

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

\n

Перед авторизованной выкладкой второй инженер должен без устных пояснений найти в записи пять вещей: точный источник сборки, digest артефакта, digest в rollout, границу совместимости миграции и раздельный план возврата кода и данных. Для каждой связи должен быть результат true или false, а для false — владелец сверки и стоп-действие.

\n

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

\n

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

" } diff --git a/editorial/agent-rewrites/126.json b/editorial/agent-rewrites/126.json index 5d8fd6f..243e578 100644 --- a/editorial/agent-rewrites/126.json +++ b/editorial/agent-rewrites/126.json @@ -1,7 +1,7 @@ { "index": 126, "slug": "editorial-2024-07-practice-release-engineering", - "title": "Релиз без догадок: как проверить связь между commit, artifact и rollback", + "title": "Релиз без догадок: как связать commit, artifact и rollback", "excerpt": "Одинаковый tag не доказывает, что команда собирается доставить нужный код. Разбираем проверяемую цепочку от commit до rollout, отрицательный путь и границу rollback для данных.", - "contentHtml": "

После выкладки сервис отвечает кодом старой версии, хотя в заявке указан новый релиз. В карточке сборки, образе и rollout стоит один tag. Команда повторяет запуск, но не может быстро ответить на три вопроса: из какого commit собран artifact, какой digest отправили и совместима ли migration с данными. Цена ошибки растёт с каждой попыткой: увеличивается окно сбоя, меняется состояние базы, а точку возврата приходится восстанавливать по разным журналам.

\n

Одинаковая версия не связывает объекты сама по себе. Релиз готов к следующему действию только тогда, когда можно сравнить exact commit id, immutable digest, migration target и return point. Если одна связь неизвестна или ложна, проверка должна остановить выпуск. Новый retry не исправляет расхождение записей.

\n

Механизм цепочки

\n

У релиза есть несколько разных объектов. commit фиксирует исходный revision. artifact содержит собранное содержимое и digest. migration меняет схему или данные и должна назвать целевую версию и совместимость. rollout описывает намерение отправить конкретный digest. return point указывает версию и digest, к которым можно вернуться.

\n

Эти записи не заменяют друг друга. Artifact должен ссылаться на exact commit, а не только на имя ветки. Rollout должен содержать digest artifact, а не mutable tag. Migration должна отвечать на вопрос о совместимости. Return point должен быть известен до разрешения операции. Такая цепочка не доказывает, что deploy уже состоялся. Она делает расхождение видимым до действия.

\n
\"Схема
Учебная схема показывает связи между записями релиза. Она не является журналом CI, registry, кластером или результатом production-выкладки.
\n

Минимальный пример

\n

Ниже приведён учебный пример с вымышленными значениями. Код не обращается к Git, registry, CI, Kubernetes API или базе данных. Он только сравнивает записи. Положительный результат означает согласованность этих записей, а не готовность реальной среды.

\n
const release = { version: '2024.07.0' };\nconst commit = { id: 'commit-7f4a0c1' };\nconst artifact = {\n  digest: 'sha256:release-070-a1',\n  sourceCommitId: 'commit-7f4a0c1',\n};\nconst migration = {\n  targetReleaseVersion: '2024.07.0',\n  compatibleWith: '2024.06.3',\n};\nconst rollout = {\n  requestedArtifactDigest: 'sha256:release-070-a1',\n  migrationVersion: '2024.07.0',\n};\n\nconst checks = {\n  source: artifact.sourceCommitId === commit.id,\n  migration: migration.targetReleaseVersion === release.version,\n  artifact: rollout.requestedArtifactDigest === artifact.digest,\n  rollout: rollout.migrationVersion === migration.targetReleaseVersion,\n};\n\nconst readyForReview = Object.values(checks).every(Boolean);\nif (!readyForReview) throw new Error('stop: reconcile release records');
\n

Каждая проверка отвечает только на один вопрос. Если заменить sourceCommitId на другой id, результат станет отрицательным. Если изменить digest в rollout, tag всё ещё будет выглядеть правильно, но содержимое уже не совпадёт. Отрицательный путь важнее зелёной строки: система не выбирает за инженера «примерно подходящую» запись.

\n

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

\n

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

\n
Диагностика расхождений до запуска
СимптомПричинаПроверкаДействие
Tag совпадает, но artifact ссылается на другой commit.Tag используют вместо точной связи с исходным revision.Сравнить artifact.sourceCommitId и commit id.Остановить выпуск. Исправить запись или пересобрать artifact после решения владельца.
Старый код не читает новую схему.Rollback образа ошибочно считают rollback данных.Проверить target migration, совместимость и обратную процедуру.Вернуть только разрешённый artifact. Изменение данных рассмотреть отдельно.
Rollout прошёл с тем же tag, но другим digest.Намерение ссылается на изменяемую метку.Сравнить requested digest с digest artifact.Не запускать rollout. Пересоздать запись после сверки.
После остановки предлагают повторить deploy.Retry используют вместо объяснения mismatch.Найти первую ложную связь и её источник.Сначала исправить записи, затем повторить только проверки.
Точку возврата называют «предыдущим релизом».У return point нет конкретного содержимого.Проверить version и immutable digest.Не обещать возврат, пока обе величины не записаны.
\n

Таблица разделяет разные классы риска. Ошибка commit относится к происхождению artifact. Ошибка digest относится к содержимому, выбранному для rollout. Ошибка migration относится к совместимости данных и кода. Неопределённый return point относится к возможности безопасно назвать действие после сбоя. Одно поле release=green не заменяет эти проверки.

\n

Порядок действий перед выпуском

\n
  1. Назовите границу проверки. Зафиксируйте release version, owner и источник каждой записи. Укажите, что сейчас выполняется сверка, а не deploy.
  2. Свяжите artifact с commit. Проверьте exact source commit и digest. Имя ветки, последний merge и короткий tag не заменяют идентификатор.
  3. Опишите migration отдельно. Назовите target version, совместимость с текущим contract и действие по данным. Не прячьте migration в комментарии к образу.
  4. Сверьте rollout. Он должен содержать immutable digest artifact и migration version. Любое расхождение переводит процесс в stop.
  5. Назовите return point. Запишите предыдущую версию и digest. Отдельно укажите, что произойдёт с данными и кто проверит этот путь.
  6. Повторите проверки после исправления. Передайте дальше только записи без ложных связей. Положительный результат открывает авторизованную проверку, но не выдаёт разрешение на deploy.
\n

Почему rollback не возвращает данные

\n

Rollback Deployment возвращает прежнюю ревизию Pod template. Это помогает вернуть код и настройки, которые входят в этот template. Но такой rollback не отменяет произвольный SQL, удалённую запись, заполненное поле или изменение внешнего contract. Если migration уже изменила данные, старый artifact может не уметь с ними работать.

\n

Поэтому return point должен содержать две границы. Первая говорит, какой artifact можно запустить. Вторая говорит, что разрешено делать с данными. Возможны обратная migration, совместимый промежуточный код, восстановление из резервной копии или запрет автоматического возврата. Пока выбранный путь не проверен, честная формулировка звучит так: «возврат artifact определён; откат данных не доказан».

\n

Та же граница действует для provenance и attestation. Provenance описывает происхождение сборки. Attestation может подтверждать утверждение об этом происхождении. Ни одно из них само по себе не доказывает совместимость migration, approval rollout или здоровье сервиса. Эти вопросы требуют собственных источников и проверок.

\n

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

\n

Схема ловит расхождения между названными записями. Она не доказывает правдивость каждого значения. Она не проверяет историю Git, содержимое образа, подпись, policy CI, права на кластер, runtime configuration, состояние базы, трафик, метрики или факт доставки.

\n

Если commit неизвестен, digest отсутствует, migration не содержит compatibility statement или return point не назван, результат должен быть отрицательным. Не подставляйте «последний main». Не ищите образ по tag. Не объявляйте rollback данных по факту возврата Pod template. Остановитесь на первой неизвестной границе и назначьте источник, который может её подтвердить.

\n

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

\n

Запись готова к передаче на авторизованную проверку, если второй инженер без устных пояснений может показать exact commit id, artifact digest, связь artifact с commit, migration target и compatibility, rollout digest, migration version и return point. Для каждой строки есть источник, владелец и действие при mismatch. Проверка даёт либо все утверждения true, либо конкретный stop с названием ложной связи.

\n

В учебном примере все значения вымышлены и не описывают production-результат. В реальном выпуске критерий нужно применять к доступным и разрешённым записям. Если одна строка не имеет источника или действия, выпуск не готов: сначала уточните contract, затем повторите сверку.

\n

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

\n" + "contentHtml": "

После выкладки сервис отвечает кодом старой версии, хотя в заявке указан новый релиз. В карточке сборки, образе и rollout стоит один tag. Команда повторяет запуск, но не может быстро ответить на три вопроса: из какого commit собран artifact, какой digest отправили и совместима ли migration с данными. Цена ошибки растёт с каждой попыткой: увеличивается окно сбоя, меняется состояние базы, а точку возврата приходится восстанавливать по разным журналам.

\n

Одинаковая версия не связывает объекты сама по себе. Релиз готов к следующему действию только тогда, когда можно сравнить exact commit id, immutable digest, migration target и return point. Если одна связь неизвестна или ложна, проверка должна остановить выпуск. Новый retry не исправляет расхождение записей.

\n

Что именно связывает релиз

\n

У релиза есть несколько разных объектов. commit фиксирует исходный revision. artifact содержит собранное содержимое и digest. migration меняет схему или данные и должна назвать целевую версию и совместимость. rollout описывает намерение отправить конкретный digest. return point указывает версию и digest, к которым можно вернуться.

\n

В Git tag является ссылкой в пространстве имён refs/tags/. Он удобен для имени релиза, но запись с одним tag не заменяет зафиксированный commit: ссылку нужно разрешить и сохранить полный идентификатор. Для контейнерного образа digest — content identifier: OCI описывает его как хеш содержимого, который можно независимо проверить.

\n

Поэтому rollout должен ссылаться на digest, а не только на имя, которое может разрешиться иначе. Эти связи проверяют согласованность записей, но не являются разрешением на выкладку: они не проверяют права, политики CI, состояние registry или здоровье сервиса.

\n
\"Схема
Учебная схема показывает связи между записями релиза. Она не является журналом CI, registry, кластером или результатом production-выкладки.
\n

Минимальный пример

\n

Ниже приведён учебный пример с вымышленными значениями. Код не обращается к Git, registry, CI, Kubernetes API или базе данных. Он только сравнивает записи. Положительный результат означает согласованность этих записей, а не готовность реальной среды.

\n
const release = { version: '2024.07.0' };\nconst commit = { id: 'commit-7f4a0c1' };\nconst artifact = {\n  digest: 'sha256:release-070-a1',\n  sourceCommitId: 'commit-7f4a0c1',\n};\nconst migration = {\n  targetReleaseVersion: '2024.07.0',\n  compatibleWith: '2024.06.3',\n};\nconst rollout = {\n  requestedArtifactDigest: 'sha256:release-070-a1',\n  migrationVersion: '2024.07.0',\n};\nconst returnPoint = {\n  version: '2024.06.3',\n  digest: 'sha256:release-063-b7',\n};\n\nfunction inspect(candidateRollout) {\n  const checks = {\n    source: artifact.sourceCommitId === commit.id,\n    migration: migration.targetReleaseVersion === release.version,\n    artifact: candidateRollout.requestedArtifactDigest === artifact.digest,\n    rollout: candidateRollout.migrationVersion === migration.targetReleaseVersion,\n    returnPoint: Boolean(returnPoint.version && returnPoint.digest),\n  };\n  return { checks, ok: Object.values(checks).every(Boolean) };\n}\n\nconst aligned = inspect(rollout);\nconst broken = inspect({\n  ...rollout,\n  requestedArtifactDigest: 'sha256:release-070-b2',\n});\n\nconsole.log(JSON.stringify({ aligned, broken }, null, 2));\nif (aligned.ok !== true || broken.ok !== false) {\n  throw new Error('unexpected validator result');\n}
\n

Каждая проверка отвечает только на один вопрос. Если заменить sourceCommitId на другой id, результат станет отрицательным. Если изменить digest в rollout, tag всё ещё будет выглядеть правильно, но содержимое уже не совпадёт. Отрицательный путь важнее зелёной строки: система не выбирает за инженера «примерно подходящую» запись.

\n

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

\n

Как получить точные значения

\n

Сначала соберите evidence в режиме чтения. Команды ниже используют примерные имена и не изменяют удалённый Git или Kubernetes. Выполняйте их только в репозитории и namespace, к которым у вас есть разрешение. Git разрешает tag до полного commit; Kubernetes показывает image reference в Pod template, историю ревизий и состояние rollout.

\n
tag=v2024.07.0\ngit rev-parse \"$tag^{commit}\"\ngit show -s --format='%H %s' \"$tag^{commit}\"\n\nkubectl -n production get deployment/app   -o jsonpath='{.spec.template.spec.containers[?(@.name==\"app\")].image}{\"\\n\"}'\nkubectl -n production rollout history deployment/app\nkubectl -n production rollout status deployment/app --timeout=60s
\n

Сохраните вывод рядом с release record и сравните его с заявленными значениями. Для контейнера ожидайте ссылку вида registry.example/app@sha256:...; если в Pod template остался только registry.example/app:v2024.07.0, имя ещё не доказывает выбранный digest. Последняя команда подтверждает состояние контроллера, но не происхождение образа, совместимость данных или пользовательский результат.

\n

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

\n
Диагностика расхождений до запуска
СимптомПричинаПроверкаДействие
Tag совпадает, но artifact ссылается на другой commit.Tag используют вместо точной связи с исходным revision.Сравнить artifact.sourceCommitId и commit id.Остановить выпуск. Исправить запись или пересобрать artifact после решения владельца.
Старый код не читает новую схему.Rollback образа ошибочно считают rollback данных.Проверить target migration, совместимость и обратную процедуру.Вернуть только разрешённый artifact. Изменение данных рассмотреть отдельно.
Rollout прошёл с тем же tag, но другим digest.Намерение ссылается на изменяемую метку.Сравнить requested digest с digest artifact.Не запускать rollout. Пересоздать запись после сверки.
После остановки предлагают повторить deploy.Retry используют вместо объяснения mismatch.Найти первую ложную связь и её источник.Сначала исправить записи, затем повторить только проверки.
Точку возврата называют «предыдущим релизом».У return point нет конкретного содержимого.Проверить version и immutable digest.Не обещать возврат, пока обе величины не записаны.
\n

Таблица разделяет разные классы риска. Ошибка commit относится к происхождению artifact. Ошибка digest относится к содержимому, выбранному для rollout. Ошибка migration относится к совместимости данных и кода. Неопределённый return point относится к возможности безопасно назвать действие после сбоя. Одно поле release=green не заменяет эти проверки.

\n

Порядок действий перед выпуском

\n
  1. Назовите границу проверки. Зафиксируйте release version, owner и источник каждой записи. Укажите, что сейчас выполняется сверка, а не deploy.
  2. Свяжите artifact с commit. Проверьте exact source commit и digest. Имя ветки, последний merge и короткий tag не заменяют идентификатор.
  3. Опишите migration отдельно. Назовите target version, совместимость с текущим contract и действие по данным. Не прячьте migration в комментарии к образу.
  4. Сверьте rollout. Он должен содержать immutable digest artifact и migration version. Любое расхождение переводит процесс в stop.
  5. Назовите return point. Запишите предыдущую версию и digest. Отдельно укажите, что произойдёт с данными и кто проверит этот путь.
  6. Повторите проверки после исправления. Передайте дальше только записи без ложных связей. Положительный результат открывает авторизованную проверку, но не выдаёт разрешение на deploy.
\n

Почему rollback не возвращает данные

\n

В Kubernetes новая ревизия Deployment создаётся при изменении Pod template, например image или label. Rollback возвращает часть template к предыдущей ревизии. Он не отменяет произвольный SQL, удалённую запись, заполненное поле, отправленное событие или изменение во внешней системе. Поэтому успех kubectl rollout undo нельзя называть откатом данных.

\n

Поэтому return point должен содержать две границы. Первая говорит, какой artifact можно запустить. Вторая говорит, что разрешено делать с данными. Возможны обратная migration, совместимый промежуточный код, восстановление из резервной копии или запрет автоматического возврата. Пока выбранный путь не проверен, честная формулировка звучит так: «возврат artifact определён; откат данных не доказан».

\n

Та же граница действует для provenance и attestation. Provenance описывает происхождение сборки. Attestation может подтверждать утверждение об этом происхождении. Ни одно из них само по себе не доказывает совместимость migration, approval rollout или здоровье сервиса. Эти вопросы требуют собственных источников и проверок.

\n

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

\n

Схема ловит расхождения между названными записями. Она не доказывает правдивость каждого значения. Она не проверяет историю Git, содержимое образа, подпись, policy CI, права на кластер, runtime configuration, состояние базы, трафик, метрики или факт доставки.

\n

Если commit неизвестен, digest отсутствует, migration не содержит compatibility statement или return point не назван, результат должен быть отрицательным. Не подставляйте «последний main». Не ищите образ по tag. Не объявляйте rollback данных по факту возврата Pod template. Остановитесь на первой неизвестной границе и назначьте источник, который может её подтвердить.

\n

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

\n

Запись готова к передаче на авторизованную проверку, если второй инженер без устных пояснений может показать exact commit id, artifact digest, связь artifact с commit, migration target и compatibility, rollout digest, migration version и return point. Для каждой строки есть источник, владелец и действие при mismatch. Проверка даёт либо все утверждения true, либо конкретный stop с названием ложной связи.

\n

В учебном примере все значения вымышлены и не описывают production-результат. В реальном выпуске критерий нужно применять к доступным и разрешённым записям. Если одна строка не имеет источника или действия, выпуск не готов: сначала уточните contract, затем повторите сверку.

\n

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

\n" } diff --git a/editorial/agent-rewrites/127.json b/editorial/agent-rewrites/127.json index 747b267..87636d1 100644 --- a/editorial/agent-rewrites/127.json +++ b/editorial/agent-rewrites/127.json @@ -1,7 +1,7 @@ { "index": 127, "slug": "editorial-2024-06-field-container-orchestration", - "title": "Kubernetes без ложных сигналов: как связать ресурсы, readiness и HPA", - "excerpt": "После релиза Pod может быть Unready, CPU — высоким, а HPA — не менять число реплик. Разбираем, какой контур породил симптом, чем его проверить и когда изменение манифеста действительно готово.", - "contentHtml": "

После обновления образа часть Pod-ов долго остаётся Unready. В графике растёт CPU. Команда предлагает увеличить maxReplicas и CPU limit. Это может не изменить ни одного симптома. Readiness управляет допуском Pod к трафику Service. HPA рассчитывает реплики по метрике. Scheduler размещает Pod по requests. Runtime применяет limits. Один Pod, четыре контура.

\n

Цена ошибки — не только лишние ресурсы. Новый Pod может не пройти readiness из-за зависимости. HPA может не считать CPU, если у контейнера нет request. Увеличенный limit может скрыть рост памяти до следующего отказа. Если изменить все поля сразу, команда потеряет причинную связь. Она не узнает, что именно сработало и какой риск остался.

\n

Тезис. Сначала нужно назвать контракт сигнала, затем проверить его источником того же типа. Не называйте Ready доказательством capacity. Не называйте CPU percentage самостоятельным числом. Не называйте значение из учебной модели показанием кластера.

\n

Механизм: четыре контура вместо одной «нагрузки»

\n

Request задаёт reservation contract. Scheduler учитывает requests контейнеров при выборе Node. Для одного ресурса request Pod складывается из requests его контейнеров. Это не прогноз постоянного потребления. Это условие размещения.

\n

Limit задаёт границу ресурса для контейнера. CPU и memory ведут себя по-разному. CPU limit может ограничивать выполнение. Memory limit не превращается в прогноз пикового потребления и не объясняет lifetime cache или batch buffer. Нельзя вывести безопасные значения из одного универсального коэффициента.

\n

Readiness отвечает на другой вопрос: можно ли отправлять трафик этому Pod сейчас. Когда Pod не готов, Service не должен использовать его как backend. Readiness probe не обязана объяснять причину. Readiness gate добавляет named condition, но не задаёт ей смысл. Владелец приложения должен определить producer, переход в True и путь восстановления.

\n

HPA формирует предложение по числу реплик из метрики и target. CPU utilization — процент относительно CPU request контейнеров, которые попали в выборку. Если relevant request отсутствует, controller не может получить такой utilization для контейнера. Значит, строка target: 65 без request не является рабочим scaling contract.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: api\nspec:\n  replicas: 2\n  template:\n    spec:\n      containers:\n        - name: api\n          image: registry.example/api@sha256:...\n          resources:\n            requests:\n              cpu: 500m\n              memory: 256Mi\n            limits:\n              cpu: \"1\"\n              memory: 512Mi\n          startupProbe:\n            httpGet:\n              path: /startup\n              port: 8080\n          readinessProbe:\n            httpGet:\n              path: /ready\n              port: 8080\n          readinessGates:\n            - conditionType: example.com/SchemaReady\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: api\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: api\n  minReplicas: 2\n  maxReplicas: 10\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

Фрагмент учебный. Образ, пути probes, custom condition и значения ресурсов нельзя переносить в production без profile приложения и проверки среды. В манифесте видно главное: HPA percentage имеет denominator requests.cpu: 500m. Readiness gate не становится HPA metric. Limit 1 не меняет denominator HPA.

\n

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

\n
Диагностическая матрица для одного workload
СимптомВозможная причинаПроверкаДействие
Pod Unready после релизаProbe или gate не выполняет контракт; зависимость ещё не готоваСопоставить condition, probe event и смысл ReadyИсправить owner и recovery path; не увеличивать replicas автоматически
CPU высокий, HPA не меняет репликиНет CPU request, нет metrics API или выбран другой target typeПроверить effective request, HPA status и источник метрикиСначала восстановить denominator или метрику; не рисовать scale policy по одному графику
Memory близка к limitРост cache, batch, retained object или неверный limitОпределить lifetime памяти и проверить runtime evidenceОграничить владельца роста; отделить memory action от CPU HPA
Новые Pod запускаются, но трафик не растётReadiness false, gate не становится True или Service не видит endpointПроверить Pod condition и endpoints разрешённым способомПроверить traffic contract; не считать создание Pod доказательством capacity
Процент выглядит убедительно только в fixtureСинтетическое число приняли за telemetryПроверить источник и marker данныхОграничить вывод учебной моделью и запросить реальное evidence отдельно
\n

Конкретный пример: почему request меняет смысл процента

\n

Пусть CPU request равен 500m, а HPA target — 70%. В модели controller это означает usage около 350m на Pod для целевой точки. Это 70% request, а не 70% Node и не 70% CPU limit. Если request изменить на 1000m, тот же target будет означать другую рабочую точку. Одновременно Scheduler начнёт резервировать больше CPU. Одно изменение затронет placement и interpretation метрики.

\n

Теперь уберём request. Значение synthetic CPU 600m всё ещё выглядит конкретно, но процент больше не имеет объявленного denominator. Корректный verdict — «нельзя интерпретировать utilization», а не «нужно больше Pod». В этом отрицательном пути отсутствие действия HPA — ожидаемый результат проверки контракта. Сначала нужно определить serving unit, для которой request имеет смысл, и подтвердить состояние metrics API в разрешённой среде.

\n

Другой пример — memory. Если synthetic observation показывает 470Mi при limit 512Mi, это не доказывает OOM, eviction, restart или throttling. Без runtime event это только учебное значение рядом с границей. Следующий вопрос относится к владельцу памяти: cache, batch buffer, response aggregation или connection pool. Readiness false при этом остаётся отдельным сигналом traffic, а не именем причины memory pressure.

\n
\"Схема
Учебная схема показывает две границы. Readiness определяет traffic eligibility. HPA использует свою метрику и свой denominator. Asset не описывает состояние реального кластера.
\n

Как проверять безопасно

\n

Проверка должна отвечать на один вопрос и использовать один тип evidence. Условие Pod, HPA status, metrics API, controlled request и runtime event не взаимозаменяемы. Если источник не разрешён, его отсутствие фиксируют как blocker. Не подменяйте его значением из fixture, screenshot или случайным графиком.

\n
  1. Ограничьте scope. Выберите один Deployment, owner, среду, период и разрешённые источники. Не начинайте с массового изменения Pod.
  2. Снимите декларацию. Выпишите requests и limits для каждого контейнера. Отдельно зафиксируйте startup, readiness, gates, HPA metric, target и min/max.
  3. Опишите profile. Назовите serving unit, startup work, concurrency, dominant resource, dependency policy и точный смысл Ready.
  4. Проверьте denominator. Для utilization найдите relevant request и effective values после admission. Если request отсутствует, остановите процентный вывод.
  5. Разделите сигналы. Сопоставьте readiness с traffic, metric с replica proposal, limit с runtime boundary, request с placement. Запишите, чего каждый сигнал не доказывает.
  6. Выберите одну гипотезу. Назначьте один evidence source, ожидаемый результат и stop condition. Не меняйте request, probe и HPA в одном эксперименте.
  7. Проверьте отрицательный путь. Зафиксируйте, что произойдёт при missing request, false gate, stale metric или memory growth. Отсутствие решения иногда и есть корректный результат.
  8. Закройте изменение. Сохраните observed result, owner, ограничение и rollback snapshot. Повторите исходный вопрос тем же типом evidence.
\n

Что не должна делать учебная fixture

\n

Учебный код может держать три фиксированные карточки в памяти: steady serving с request, memory growth с false readiness и warmup без request. Он может проверять, что synthetic value не получила ярлык telemetry и что внешний field отклоняется. Он не должен читать kubeconfig, namespace, файл манифеста, CI, HTTP, trace или production. Комментарий syntheticObservedPodBehavior должен прямо говорить, что это не kubectl и не metrics API.

\n
const card = {\n  id: 'fixed-warmup-request-missing-v1',\n  declared: { cpuRequest: null, cpuTarget: 65 },\n  observed: {\n    kind: 'embedded-fixed-js-object-not-telemetry',\n    ready: false,\n    cpu: '600m'\n  }\n};\n\nconst verdict = card.declared.cpuRequest === null\n  ? 'block-utilization-conclusion'\n  : 'compare-with-request';\n\nconsole.log(verdict);\n// Учебная модель. Нет cluster, file, CI, HTTP или production data.
\n

Для этой карточки корректен verdict block-utilization-conclusion. Код не говорит, сколько реплик нужно реальному сервису. Он проверяет только отрицательную ветку: процент без request нельзя честно объяснить. Production-инструмент требует отдельной авторизации, источника данных и правил изменения. Учебный пример эти полномочия не получает.

\n

Ограничения и rollback

\n

Материал не выбирает размер Node, ratio CPU и memory, значения probes, тип custom metric или безопасный maxReplicas. Он не видит admission webhooks, quotas, EndpointSlice, runtime cgroups, Metrics Server, custom adapter, logs, traces и downstream dependencies. Официальная семантика Kubernetes не заменяет проверку конкретного кластера.

\n

Rollback должен возвращать snapshot декларации и проверять side effects. Откат HPA не исправляет неверный readiness contract. Возврат limit не объясняет рост памяти. Изменение probe не восстанавливает потерянные evidence. Для каждого изменения укажите owner, условие остановки, временную границу и наблюдаемый критерий отката.

\n

Критерий готовности. Изменение готово, если profile назван, у каждого сигнала есть источник и owner, CPU utilization имеет declared request, Ready имеет отдельный traffic contract, synthetic данные не выданы за production, а отрицательная ветка приводит к остановке или явному blocker. Кроме того, команда может повторить исходную проверку тем же типом evidence и получить объяснимый результат. Если хотя бы одна строка отвечает «кажется», манифест не готов.

\n

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

\n" + "title": "Kubernetes без ложных сигналов: как связать rollout, readiness и HPA", + "excerpt": "После обновления образа новые Pod могут запускаться, но не получать трафик, а HPA — не менять число реплик. Разбираем четыре разных контура, команды проверки и безопасный критерий готовности.", + "contentHtml": "

После обновления образа часть Pod остаётся на старой версии, новые Pod долго имеют статус NotReady, а CPU на графике растёт. Первая реакция обычно сводится к увеличению maxReplicas или CPU limit. Но эти поля принадлежат разным контроллерам. Можно добавить реплики и не получить ни одного нового backend в Service, а можно поднять limit и изменить поведение throttling, не исправив причину задержки.

\n

Цена смешения сигналов — потеря причинной связи. Deployment может продолжать rollout, пока новая ReplicaSet не набирает доступные Pod. Readiness probe может исключить Pod из Service, хотя процесс жив. HPA может не рассчитать CPU utilization из-за отсутствующего request или недоступного metrics API. Сначала назовём владельца каждого сигнала, затем проверим ровно тот контур, который породил симптом.

\n

Главный принцип. Ready означает допуск к трафику, а не доказанную производительность. CPU utilization в HPA — отношение потребления к объявленному request, а не процент CPU Node и не процент limit. Rolling update отвечает за замену ReplicaSet, но не исправляет плохую readiness-проверку.

\n

Четыре контура одной рабочей нагрузки

\n

Deployment и ReplicaSet. Deployment хранит желаемый шаблон Pod и управляет ReplicaSet. При стратегии RollingUpdate новая ReplicaSet создаёт Pod, а старая постепенно уменьшается. Поля maxSurge и maxUnavailable ограничивают число дополнительных и недоступных Pod. Поэтому во время rollout нормально временно видеть старую и новую версии одновременно. Ненормально — считать сам факт создания нового Pod доказательством его готовности.

\n

Readiness. Kubelet выполняет readiness probe на протяжении жизни контейнера. Если она не проходит, Pod получает состояние unready и Kubernetes Services не должны отправлять ему трафик. Такая probe отвечает только на вопрос «можно ли обслуживать запросы сейчас». Она не обязана доказывать отсутствие утечки памяти, правильность миграции или запас CPU. Для долгого старта применяют startupProbe, чтобы не превращать штатную инициализацию в перезапуски.

\n

Service и EndpointSlice. Service выбирает Pod по label selector, а control plane формирует связанные EndpointSlice. В EndpointSlice условие ready отражает готовность endpoint; это полезная проверка фактического набора backend, а не только списка Pod. Сетевой mesh, балансировщик и настройка publishNotReadyAddresses могут добавить собственное поведение, поэтому команда должна проверить реальный путь трафика. Для обычного Service нельзя переносить вывод «Pod Running» на «Pod получает запросы».

\n

Ресурсы и HPA. Scheduler использует requests контейнеров для выбора Node. Limits задают границу выполнения: CPU ограничивается throttling, а превышение memory limit может привести к OOM kill. HPA с ресурсной метрикой averageUtilization сравнивает usage с request. Если у релевантного контейнера нет request, utilization для этой метрики не определён, и HPA не обязан масштабировать по ней.

\n
Схема связи readiness probe и custom readiness gate: только совместное состояние формирует Ready и допуск к Service traffic, тогда как HPA CPU считает usage относительно request отдельно
Схема показывает границу между traffic eligibility и autoscaling. Подпись synthetic.example/contract на рисунке — условное имя custom condition; рисунок не представляет состояние конкретного кластера.
\n

Почему старый Pod может оставаться в системе

\n

Рассмотрим последовательность без привязки к конкретному облаку. У Deployment две реплики, стратегия — RollingUpdate. После смены образа контроллер создаёт новую ReplicaSet. Новый контейнер запускается, но readiness endpoint отвечает ошибкой, потому что приложение ещё загружает конфигурацию. Pod остаётся живым, однако Service не получает его в качестве готового endpoint. Старый Pod продолжает обслуживать запросы, пока контроллер не может безопасно уменьшить старую ReplicaSet.

\n

В такой ситуации фраза «релиз завис» слишком общая. Нужно разделить пять наблюдений: какой image digest реально запущен; какую condition получил новый Pod; что написано в событиях probe; какие endpoints видит Service; какое решение показывает Deployment controller. Одного вывода kubectl get pods недостаточно.

\n
Симптом, источник доказательства и допустимое действие
СимптомЧто могло произойтиЧем проверитьЧто делать первым
Новые Pod Running, но READY 0/1Readiness probe не проходит или custom gate остаётся falsekubectl describe pod, conditions и EventsПроверить контракт endpoint/gate и зависимость; не увеличивать replicas вслепую
Старая ReplicaSet не уменьшаетсяНовая версия не набрала доступные Pod или достигнут лимит rolloutkubectl rollout status, Deployment conditions, ReplicaSetsСопоставить maxUnavailable с доступными Pod и найти причину Unready
CPU высок, HPA не меняет replicasНет request, нет resource metrics или выбран другой target typekubectl describe hpa, HPA conditions, metrics APIВосстановить источник и знаменатель; не считать отсутствующий scale доказательством низкой нагрузки
Pod Ready, но запросов нетSelector/port не совпадает, EndpointSlice устарел или трафик идёт через другой слойService selector, EndpointSlice и путь data planeСверить label, порт и фактический backend; не менять probe без этого сравнения
Memory близка к limitРастёт cache, buffer, retained object или слишком тесен лимитRuntime metrics, restart reason, events и профиль приложенияОтделить memory investigation от CPU HPA и назвать владельца роста
\n

Минимальный манифест для проверки контракта

\n

Ниже — учебный фрагмент. Он намеренно содержит requests, limits, startup/readiness probes, custom readiness gate и HPA. Образ, путь endpoint, имя condition и числа ресурсов нужно заменить на значения конкретного приложения. Custom readiness gate не станет True сам по себе: внешний контроллер или другой владелец состояния должен установить condition, иначе Pod останется неготовым.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: api\nspec:\n  # Если Deployment управляется HPA, не дублируйте replicas\n  # в постоянно применяемом manifest без отдельной политики.\n  replicas: 2\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      maxSurge: 1\n      maxUnavailable: 0\n  selector:\n    matchLabels:\n      app: api\n  template:\n    metadata:\n      labels:\n        app: api\n    spec:\n      readinessGates:\n        - conditionType: synthetic.example/contract\n      containers:\n        - name: api\n          image: registry.example/api:v3.4.1\n          ports:\n            - name: http\n              containerPort: 8080\n          resources:\n            requests:\n              cpu: 500m\n              memory: 256Mi\n            limits:\n              cpu: '1'\n              memory: 512Mi\n          startupProbe:\n            httpGet:\n              path: /startup\n              port: http\n            periodSeconds: 5\n            failureThreshold: 24\n          readinessProbe:\n            httpGet:\n              path: /ready\n              port: http\n            periodSeconds: 5\n            failureThreshold: 2\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: api\nspec:\n  selector:\n    app: api\n  ports:\n    - name: http\n      port: 80\n      targetPort: http\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: api\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: api\n  minReplicas: 2\n  maxReplicas: 10\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

В этом примере CPU target равен 70% от request, то есть 350m на Pod при request 500m. Это рабочая точка алгоритма, а не обещание, что сервис выдержит нужную нагрузку. maxUnavailable: 0 увеличивает потребность в свободной ёмкости: новая версия должна стать доступной до удаления старой. На маленьком кластере rollout может остановиться из-за нехватки ресурсов, даже если probe исправна.

\n

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

\n

Команды ниже только читают состояние и подходят для namespace, к которому у вас есть доступ. Сначала запишите имя Deployment, namespace и новый image digest. Затем повторяйте команды с тем же объектом; так временная картина не смешивается с другой нагрузкой.

\n
NS=demo\nDEPLOY=api\n\nkubectl -n \\\"$NS\\\" get deployment \\\"$DEPLOY\\\" -o wide\nkubectl -n \\\"$NS\\\" rollout status deployment/\\\"$DEPLOY\\\" --timeout=120s\nkubectl -n \\\"$NS\\\" get rs,pods -l app=api \\\n  -o custom-columns='KIND:kind,NAME:metadata.name,IMAGE:spec.template.spec.containers[0].image,READY:status.containerStatuses[0].ready'\nkubectl -n \\\"$NS\\\" describe deployment \\\"$DEPLOY\\\"\nkubectl -n \\\"$NS\\\" describe pod -l app=api\nkubectl -n \\\"$NS\\\" get endpointslice \\\n  -l kubernetes.io/service-name=api -o yaml\nkubectl -n \\\"$NS\\\" describe hpa \\\"$DEPLOY\\\"\nkubectl -n \\\"$NS\\\" top pod -l app=api
\n

Команда rollout status сообщает, завершился ли rollout, но не объясняет каждую причину. describe pod даёт condition и Events, однако не заменяет логи приложения. get endpointslice показывает объект control plane; если между Service и клиентом есть mesh или внешний балансировщик, его состояние нужно проверять отдельно. top требует работающего resource metrics API и показывает текущий срез, а не историю.

\n

Для безопасного чтения образа сравните digest, а не только короткий tag. Для проверки именно нового ReplicaSet выберите его label из вывода Deployment и повторите describe pod по этому selector. Если в cluster policy запрещён доступ к EndpointSlice или metrics API, это не повод подставлять число из fixture: результат проверки должен быть «источник недоступен».

\n

Разбираем HPA без иллюзии о процентах

\n

При requests.cpu: 500m и среднем потреблении 350m utilization равен 70%. При том же потреблении, но request 1000m, utilization равен 35%. CPU workload не изменился, а решение HPA стало другим. Одновременно Scheduler увидит другой reservation contract. Поэтому изменение request нельзя считать только настройкой autoscaling: оно меняет и размещение, и интерпретацию процента.

\n

Limit — другой знаменатель и другая граница. Если limit равен 1 CPU, контейнер может упереться в throttling на этой границе; HPA с averageUtilization всё равно сравнивает usage с request. Если memory приближается к 512Mi, это не объясняет само по себе высокий CPU и не доказывает OOM. Для memory нужно проверить restart reason, события и профиль приложения.

\n

Есть ещё одна конфликтующая настройка. Kubernetes предупреждает: когда HPA активен, применение Deployment manifest с фиксированным spec.replicas может снова записать число реплик и вызвать колебания. Перед тем как удалить replicas из manifest, проверьте способ применения и разовый эффект: API по умолчанию может трактовать отсутствие поля как одну реплику. Это изменение нужно выполнять отдельным контролируемым шагом, а не попутно с исправлением probe.

\n

Порядок изменения и отрицательные пути

\n
  1. Зафиксируйте объект. Запишите namespace, Deployment, image digest, label selector, Service, период наблюдения и владельца изменения.
  2. Снимите декларацию. Сохраните requests/limits каждого контейнера, probes, gates, стратегию rollout, HPA metric, target, min/max и способ применения manifest.
  3. Проверьте новую версию. Убедитесь, что Pod действительно использует ожидаемый digest. Сопоставьте его condition, Events и логи с readiness-контрактом.
  4. Проверьте путь трафика. Сверьте Service selector, port/targetPort и EndpointSlice. При наличии mesh или внешнего балансировщика добавьте его собственный источник.
  5. Проверьте rollout. Сравните доступные и желаемые Pod, maxSurge/maxUnavailable, условия Deployment и время остановки. Не меняйте сразу стратегию и probe.
  6. Проверьте HPA. Убедитесь, что metrics API отвечает, request присутствует у релевантных контейнеров, target type соответствует задаче, а status содержит текущую метрику и conditions.
  7. Выберите одну гипотезу. На один эксперимент меняйте один контракт: endpoint readiness, request, limit, rollout policy или metric. Заранее задайте ожидаемое наблюдение и stop condition.
  8. Проверьте отрицательный путь. Зафиксируйте поведение при false readiness, missing request, недоступной метрике, несовпадающем selector и нехватке Node capacity. Если источник неизвестен, остановите вывод.
  9. Закройте результат. Сравните исходный симптом теми же командами, сохраните observed result, owner и rollback snapshot. Не объявляйте rollout готовым по одному зелёному статусу.
\n

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

\n

Этот разбор не выбирает универсальные значения CPU, memory, probe timeout, maxReplicas или maxUnavailable. Их определяют профиль приложения, SLA, стоимость Node, размер ответа, время старта и downstream-зависимости. Статус Ready не проверяет бизнес-корректность ответа. HPA не заменяет очередь, rate limit, вертикальное масштабирование или capacity planning. Resource metrics не дают трассировку причины задержки.

\n

Манифест не учитывает admission webhook, LimitRange, ResourceQuota, PodDisruptionBudget, topology spread, NetworkPolicy и правила конкретного service mesh. Эти механизмы могут изменить effective configuration или доступность. publishNotReadyAddresses: true меняет обычную семантику готовых endpoints, поэтому вывод о трафике нужно сверять с фактическим Service contract.

\n

Откат делайте после сохранения ревизии и проверки зависимости. Для Deployment можно посмотреть историю и вернуть предыдущую ревизию командами ниже. Откат не исправляет неверный readiness endpoint и не освобождает уже занятый Node; после него снова проверьте conditions, endpoints и rollout status.

\n
kubectl -n demo rollout history deployment/api\nkubectl -n demo rollout undo deployment/api --to-revision=3\nkubectl -n demo rollout status deployment/api --timeout=120s
\n

Критерий готовности. Изменение готово, если известны image digest и владелец состояния, новый Pod проходит startup/readiness по смыслу приложения, Service видит ожидаемые endpoints, rollout достигает доступного состояния, а HPA получает метрику с объявленным request или явно использует другой документированный target type. Для каждого отрицательного пути есть наблюдаемое действие: остановка, откат или передача конкретному владельцу. Если команда видит только Running, но не может показать condition, endpoint и источник метрики, причина ещё не доказана.

\n

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

" } diff --git a/editorial/agent-rewrites/128.json b/editorial/agent-rewrites/128.json index dac0ff4..f770001 100644 --- a/editorial/agent-rewrites/128.json +++ b/editorial/agent-rewrites/128.json @@ -3,5 +3,5 @@ "slug": "editorial-2024-06-mechanism-container-orchestration", "title": "Контейнерная нагрузка без догадок: как связать requests, readiness и HPA", "excerpt": "После обновления образа Pod может стать Unready, а HPA — не изменить число реплик. Разбираем, какой механизм отвечает за placement, traffic и масштабирование, как проверить гипотезу и когда изменение манифеста действительно готово.", - "contentHtml": "

После обновления образа часть Pod долго остаётся на старой версии. Другие Pod переходят в Ready, но сразу теряют готовность. В ответ команда увеличивает maxReplicas, поднимает CPU limit и запускает rollout ещё раз. Симптомы меняются, а причина остаётся. Цена ошибки — лишние реплики, неуправляемая нагрузка на Node и более длинный путь отката. В худшем случае Service получает Pod, который ещё не готов обслуживать запросы.

\n

Тезис простой: Kubernetes не управляет контейнерной нагрузкой одной ручкой. Scheduler учитывает requests. Runtime ограничивает container по limits. Readiness решает, можно ли отправлять Pod трафик. HPA предлагает число реплик по своей метрике. Эти контуры связаны, но не заменяют друг друга. Пока у каждого значения нет явного смысла, процент CPU и статус Ready легко принять за доказательство capacity.

\n

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

\n
Четыре частых подмены в разборе контейнерной нагрузки
СимптомПричинаПроверкаДействие
HPA показывает процент, но реплики не растутУ container нет CPU request или метрика не имеет нужного знаменателяПроверить request каждого container, тип metric и источник metrics APIСначала исправить контракт метрики; не увеличивать replicas вслепую
Pod Unready после стартаReadiness проверяет недоступную зависимость, либо probe срабатывает раньше прогреваСопоставить probe, startup path, condition и Service endpointsИзменить семантику readiness или порядок старта, затем повторить проверку
Pod Pending после изменения ресурсовСумма requests не помещается на доступные NodeСравнить requests с allocatable, quota и правилами admissionПересмотреть профиль workload или размещение
CPU limit увеличен, но задержка не исчезлаПричина находится в памяти, очереди или внешней зависимостиРазделить CPU, memory, queue, latency и dependency signalsПроверять один доминирующий ресурс, а не менять все поля сразу
\n

Как работают четыре контура

\n

Requests отвечают за размещение. Scheduler использует CPU и memory requests при выборе Node. Для Pod учитывается сумма requests контейнеров. Request не обещает постоянное потребление и не задаёт верхнюю границу. Это объявленная потребность, по которой система решает, может ли Pod быть размещён.

\n

Limits задают границу ресурса. CPU и memory limit принадлежат container. Они не являются целью HPA. Memory limit не описывает безопасный размер cache, а CPU limit не обещает throughput. При изменении limit нужно знать, какое поведение ожидается после достижения границы: ограничение CPU, ошибка выделения памяти или другой runtime effect. Без этого число в YAML не объясняет проблему.

\n

Readiness управляет допуском к трафику. Когда readiness probe возвращает failure, Kubernetes не считает Pod готовым backend для Service. Это полезный сигнал маршрутизации. Он не говорит, почему приложение не готово, насколько высока latency и хватит ли ему CPU при пике. Probe должна проверять короткий факт, которым владеет приложение или его платформа. Проверка десятка внешних зависимостей превращает краткий сбой одной зависимости в удаление Pod из трафика.

\n

HPA предлагает desired replicas. Для CPU utilization процент рассчитывается относительно CPU request целевых Pod. Поэтому target в 70 процентов — не 70 процентов Node и не 70 процентов limit. При request 500m такой target имеет другой смысл, чем при request 1000m. Изменение request одновременно влияет на placement и на интерпретацию HPA. Это одна причина, чтобы менять оба решения в одной проверяемой гипотезе.

\n
\"Учебная
Кривая показывает отношения между величинами, а не состояние конкретного кластера. Target HPA имеет знаменателем request; limit — отдельная граница container.
\n

Конкретный пример

\n

Пусть приложение обслуживает HTTP-запросы. Для одного container объявлены cpu request: 500m и cpu limit: 1000m. HPA использует targetAverageUtilization: 70. В учебном примере значение 70 процентов относится к 500m request. Условная точка сравнения равна 350m usage на Pod. Это арифметика для объяснения знаменателя, а не наблюдение из кластера и не рекомендация для реального сервиса.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: orders\nspec:\n  replicas: 2\n  template:\n    spec:\n      containers:\n        - name: app\n          image: registry.example/orders:sha256-example\n          resources:\n            requests:\n              cpu: 500m\n              memory: 384Mi\n            limits:\n              cpu: 1000m\n              memory: 768Mi\n          readinessProbe:\n            httpGet:\n              path: /ready\n              port: 8080\n            periodSeconds: 5\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: orders\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: orders\n  minReplicas: 2\n  maxReplicas: 8\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

Этот фрагмент показывает форму контракта. Он не доказывает, что 500m достаточно, что endpoint отвечает быстро или что HPA получит метрики. В реальной системе нужно отдельно проверить effective manifest после admission, состояние metrics API, readiness transitions и фактический workload. Если CPU request убрать, процентный target теряет ожидаемый знаменатель. Если readiness отвечает 200 до завершения прогрева, Service направит трафик слишком рано. Если readiness зависит от внешней базы, краткий сбой базы может убрать все backend.

\n

Отрицательный путь: почему масштабирование не лечит готовность

\n

Рассмотрим запуск новой версии. Приложение мигрирует локальный cache 20 секунд, но readiness endpoint начинает отвечать успехом через две секунды. HPA видит CPU прогрева и предлагает больше реплик. Новые Pod повторяют тот же тяжёлый startup. Число реплик растёт, а полезная ёмкость не появляется. Это не доказательство, что HPA сломан. Сначала нужно отделить startup work от serving work.

\n

Обратная ошибка тоже опасна. Readiness проверяет внешний сервис, который не нужен каждому запросу. При коротком отказе зависимости все Pod становятся Unready, хотя основная функция могла бы продолжать работу. Увеличение replicas не помогает: новые Pod проходят ту же проверку и исключаются из Service. Действие находится в контракте readiness и failure policy, а не в capacity curve.

\n

Упорядоченный маршрут проверки

\n
  1. Ограничьте workload. Назовите Deployment, owner, serving path, startup path, единицу работы и период наблюдения. Не смешивайте два сервиса в одну гипотезу.
  2. Зафиксируйте декларацию. Выпишите requests и limits каждого container, probe, startup settings, HPA metric, minReplicas и maxReplicas. Отделите написанное в manifest от effective values после admission.
  3. Назовите смысл Ready. Запишите короткое условие, после которого Pod действительно может принимать Service traffic. Отдельно запишите, что readiness не доказывает: например, throughput, latency или здоровье всех зависимостей.
  4. Проверьте знаменатель метрики. Для CPU utilization свяжите target с CPU request. Для raw, custom и external metrics укажите target type, selector и источник. При missing data остановите вывод, а не подставляйте число.
  5. Соберите одно evidence. Выберите разрешённый condition, metric или controlled test. У evidence должны быть источник, время, workload и ограничение интерпретации. Учебные значения не заменяют этот шаг.
  6. Измените одну гипотезу. Выберите request, limit, probe или HPA policy. Запишите ожидаемый сигнал, stop condition и rollback. Не меняйте четыре контура одним commit.
  7. Повторите ту же проверку. Сравните результат с первоначальной гипотезой. Если изменился тип evidence или workload, результат нельзя считать подтверждением.
\n

Ограничения

\n

Эта модель не выбирает универсальные значения CPU и memory. Она не учитывает автоматически admission webhooks, ResourceQuota, PodDisruptionBudget, Node allocatable, runtime, Metrics Server, custom adapter, queueing, cache retention и downstream saturation. Официальная документация описывает общий механизм Kubernetes, но не сообщает конфигурацию конкретного кластера. Нельзя переносить учебную арифметику в production profile без измерения.

\n

Readiness не заменяет liveness и startup probes. HPA не устраняет утечку памяти и не гарантирует доступность внешней зависимости. Limit не превращается в SLO. Если причина не разделяется одним evidence, правильное действие — остановить изменение и уточнить контракт. Это отрицательный результат, но он дешевле массового rollout без объяснимого эффекта.

\n

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

\n

Изменение готово, когда для одного workload выполнены все условия: владелец назван; effective requests и limits зафиксированы по container; смысл readiness записан одной фразой; HPA metric имеет объявленный знаменатель и источник; выбранное evidence получено в указанном периоде; ожидаемый эффект измерим; stop condition и rollback проверяемы. Если после изменения команда всё ещё говорит только «Pod стал лучше» или «процент выглядит нормально», контракт не закрыт.

\n

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

\n" + "contentHtml": "

После обновления образа часть Pod может долго оставаться на старой версии, а новая версия — перейти в Unready. Команда увеличивает maxReplicas, поднимает CPU limit и запускает rollout повторно. Симптомы меняются, но причина не становится яснее. Цена такой подмены — лишние реплики, перегруженные Node и откат, который трудно объяснить.

\n

У Kubernetes здесь не одна «ручка нагрузки», а несколько независимых контуров. Scheduler размещает Pod по requests. Runtime применяет limits. Readiness определяет, можно ли отправлять Pod трафик Service. HPA рассчитывает желаемое число реплик по выбранной метрике. Эти контуры встречаются в одном манифесте, но не доказывают друг друга.

\n

Сначала разделите симптом и механизм

\n
Диагностическая матрица для четырёх похожих симптомов
СимптомВероятный контурПроверкаСледующее действие
HPA показывает <unknown> или не меняет репликиМетрика недоступна либо для Pod нет нужного resource requestПроверить HPA conditions, metrics.k8s.io и request каждого containerВосстановить контракт метрики; не поднимать maxReplicas вслепую
Pod запущен, но UnreadyReadiness не проходит или приложение ещё прогреваетсяСопоставить probe, Pod conditions, события и endpointИсправить условие готовности или добавить startup-защиту
Pod остаётся PendingСумма requests не помещается на доступные NodeСравнить requests с allocatable, quota и admission-правиламиПересмотреть профиль ресурсов или размещение
CPU limit увеличили, но задержка не исчезлаДоминирует память, очередь или внешняя зависимостьРазделить CPU, memory, latency, queue и dependency signalsИзменить один подтверждённый фактор и повторить замер
\n

Четыре контура и их границы

\n

Request отвечает за планирование. Scheduler учитывает requests контейнеров при выборе Node. Для одного ресурса Pod получает сумму requests своих контейнеров. Поэтому изменение request может перевести Pod из «размещается» в «не помещается», даже если текущее потребление на Node пока невелико. Request — заявленная потребность для планирования, а не обещание постоянной скорости.

\n

Limit задаёт потолок контейнера. CPU limit может ограничивать долю CPU-времени, а превышение memory limit может привести к срабатыванию механизма out-of-memory. Limit не сообщает, сколько запросов выдержит приложение, и не является автоматически целью HPA. Между «контейнер не превысил лимит» и «у сервиса есть запас по latency» нет логического равенства.

\n

Readiness управляет маршрутизацией. При неуспешной readiness probe Kubernetes помечает контейнер неготовым, а адрес Pod перестаёт быть готовым endpoint для соответствующих Service. Это сигнал «можно ли принимать этот трафик сейчас», а не проверка всех зависимостей системы. Если endpoint опрашивает необязательную базу или внешний API, краткий сбой этой зависимости может убрать из трафика исправное приложение.

\n

HPA меняет желаемое число реплик. Для resource metric с averageUtilization процент считается относительно соответствующего request. Target 70 процентов — это 70 процентов request, не Node и не limit. Для CPU request 500m учебная точка 70 процентов равна 350m на Pod. Это объяснение знаменателя, а не рекомендация профиля.

\n
\"Учебная
Схема разделяет target HPA и CPU limit. Точки синтетические: они объясняют арифметику и не являются telemetry или прогнозом capacity конкретного кластера.
\n

Контролируемая замена версии

\n

Deployment отвечает ещё за один отдельный вопрос: как заменить Pod старой версии на Pod новой. При RollingUpdate параметры maxUnavailable и maxSurge ограничивают число временно недоступных и дополнительных Pod. Readiness влияет на то, когда новая реплика считается пригодной для трафика, но сама по себе не гарантирует, что rollout завершится: новый образ может не стартовать, не пройти probe или не поместиться по requests.

\n

Поэтому «новые Pod появились» и «новая версия обслуживает нагрузку» — разные проверки. Для отката нужно заранее знать имя Deployment, границу времени и состояние, которое считается безопасным. Команда kubectl rollout status подтверждает наблюдаемое состояние rollout, но не заменяет проверку ошибок приложения и latency.

\n

Полный учебный манифест

\n

Ниже один минимальный пример для HTTP-сервиса. В нём добавлены обязательные для Deployment selector и labels, startup probe для долгого старта и readiness probe для допуска к трафику. Значения ресурсов, пути и образ проектные: их нельзя переносить в production без измерения.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: orders\nspec:\n  replicas: 2\n  strategy:\n    type: RollingUpdate\n    rollingUpdate:\n      maxUnavailable: 0\n      maxSurge: 1\n  selector:\n    matchLabels:\n      app: orders\n  template:\n    metadata:\n      labels:\n        app: orders\n    spec:\n      containers:\n        - name: app\n          image: registry.example/orders:2024-06-01\n          ports:\n            - name: http\n              containerPort: 8080\n          resources:\n            requests:\n              cpu: 500m\n              memory: 384Mi\n            limits:\n              cpu: 1000m\n              memory: 768Mi\n          startupProbe:\n            httpGet:\n              path: /healthz\n              port: http\n            periodSeconds: 10\n            failureThreshold: 30\n          readinessProbe:\n            httpGet:\n              path: /ready\n              port: http\n            periodSeconds: 5\n            failureThreshold: 3\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: orders\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: orders\n  minReplicas: 2\n  maxReplicas: 8\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

При таком request target 70 процентов означает условные 350m среднего CPU на Pod. Но манифест не доказывает, что 500m достаточно, endpoint действительно отделяет startup от serving, а metrics API доступен. Если у контейнера, который участвует в CPU utilization, нет CPU request, HPA не сможет определить эту utilization для метрики. Это повод проверить конфигурацию и condition HPA, а не подставить процент вручную.

\n

Команды для воспроизводимой проверки

\n

Эти команды предполагают доступ к namespace и установленный kubectl. Первая команда проверяет манифест сервером без изменения объекта; остальные читают состояние или запускают обычный rollout после явного применения. В тестовом кластере замените namespace и имя образа на свои.

\n
kubectl apply --dry-run=server -f deployment.yaml\nkubectl apply -f deployment.yaml\nkubectl rollout status deployment/orders --timeout=10m\nkubectl get deployment/orders\nkubectl get pods -l app=orders -o wide\nkubectl describe deployment/orders\nkubectl describe pod -l app=orders\nkubectl get hpa orders\nkubectl top pods -l app=orders
\n

Ожидаемый результат нужно формулировать заранее: например, rollout завершается за 10 минут, две минимальные реплики находятся в состоянии Ready, а HPA получает числовую CPU-метрику. Если kubectl top или HPA показывают отсутствие метрик, это ограничение наблюдаемости. Оно не подтверждает ни низкую нагрузку, ни исправность autoscaling. Для остановившегося rollout дополнительно смотрите Events и condition Deployment; для Pending — причины планировщика и effective requests.

\n

Почему HPA не лечит неготовность

\n

Представим новую версию, которая прогревает локальный cache 20 секунд. Readiness endpoint начинает отвечать через две секунды, а CPU во время старта высок. Если metrics API отдаёт данные и контроллер учитывает их, HPA может принять решение о масштабировании, но новые Pod повторят тот же startup. Реплик станет больше, а serving capacity не обязательно вырастет. Это не доказательство неисправности HPA: сначала нужно отделить startup work от serving work.

\n

Обратная ошибка симметрична. Readiness проверяет внешний сервис, который нужен только части запросов. При кратком отказе все Pod становятся Unready, хотя основной маршрут ещё может работать. Дополнительные реплики проходят ту же проверку и тоже исключаются из Service. Действие находится в контракте readiness и политике деградации, а не в увеличении maxReplicas.

\n

Порядок расследования

\n
  1. Ограничьте объект. Запишите namespace, Deployment, owner, serving path, startup path, единицу работы и окно наблюдения. Один вывод — один workload.
  2. Снимите effective-конфигурацию. Зафиксируйте requests и limits каждого container, probes, selector, rollout strategy и HPA metric. Отделяйте YAML в репозитории от объекта после admission.
  3. Сформулируйте смысл Ready. Одной фразой опишите, после какого условия Pod может принимать Service traffic. Отдельно перечислите, чего Ready не доказывает: throughput, latency и здоровье необязательных зависимостей.
  4. Проверьте источник метрики. Для CPU utilization свяжите target с request и проверьте resource metrics API. Для custom или external metric укажите target type, selector и владельца pipeline. При отсутствии данных остановите вывод.
  5. Соберите одно evidence. Выберите condition, метрику или контролируемый тест. Сохраните источник, время, workload и ограничение интерпретации. Учебный манифест не заменяет наблюдение.
  6. Измените одну гипотезу. Выберите request, limit, probe, rollout policy или HPA policy. Запишите ожидаемый сигнал, stop condition и команду отката.
  7. Повторите ту же проверку. Сравните одинаковое окно, workload и тип evidence. Если изменились сразу несколько контуров, эффект нельзя приписать одному решению.
\n

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

\n

Эта модель не выбирает универсальные CPU и memory values. На результат влияют admission webhooks, ResourceQuota, PodDisruptionBudget, Node allocatable, планировщик, runtime, Metrics Server, custom adapter, очередь, cache и downstream saturation. Официальная документация описывает механизм Kubernetes, но не конфигурацию вашего кластера.

\n

Readiness не заменяет liveness и startup probes. HPA не устраняет утечку памяти и не гарантирует доступность внешней зависимости. CPU utilization не равен throughput, а отсутствие роста реплик не всегда означает ошибку autoscaling: контроллер может упереться в min/max, не получить метрику или увидеть, что целевой workload уже соответствует target. Учебную арифметику нельзя объявлять production-результатом без профиля на реальной нагрузке.

\n

Критерий готовности изменения

\n

Изменение можно считать объяснимым, когда для одного workload названы владелец и окно наблюдения; effective requests и limits зафиксированы по container; смысл readiness записан одной фразой; HPA metric имеет источник и знаменатель; rollout имеет timeout и rollback; а выбранный эффект измерен тем же evidence до и после. Формулировки «Pod стал лучше» и «процент выглядит нормально» этого критерия не закрывают.

\n

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

\n" } diff --git a/editorial/agent-rewrites/129.json b/editorial/agent-rewrites/129.json index 243120e..06c1137 100644 --- a/editorial/agent-rewrites/129.json +++ b/editorial/agent-rewrites/129.json @@ -3,5 +3,6 @@ "slug": "editorial-2024-06-practice-container-orchestration", "title": "Контейнерная нагрузка: как связать ресурсы, готовность и автомасштабирование", "excerpt": "Почему Pod может успешно разместиться, но не выдержать трафик: разбираем requests и limits, readiness и HPA на одном учебном примере.", - "contentHtml": "

После выкладки Pod получает статус Running, но запросы к сервису ждут дольше обычного. Иногда HPA увеличивает число реплик, а доступных backend не становится больше: новые Pod остаются неготовыми. В другой версии проблемы readiness отвечает успешно ещё до прогрева, и Service отправляет трафик в приложение, которое не держит рабочую нагрузку. Цена ошибки — задержки для клиентов, лишние реплики и трудный откат. Команда видит зелёный rollout и ищет причину уже под нагрузкой.

\n

Тезис простой: requests, limits, readiness и HPA описывают разные границы. Их нельзя настраивать как четыре независимые строки в манифесте. Сначала нужно назвать профиль приложения и единицу работы. Затем связать каждую границу с проверяемым сигналом. Тогда Kubernetes размещает Pod по одному правилу, допускает его к трафику по другому, а autoscaler меняет replicas по третьему. Это не даёт готовых чисел для любого сервиса. Зато не позволяет принять один сигнал за другой.

\n

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

\n
Как отделить похожие симптомы
СимптомПричинаПроверкаДействие
Pod долго остаётся PendingRequest не помещается на доступный Node или не учтены requests всех containers.Сверить request каждого container с событиями scheduler и allocatable Node.Исправить размер или размещение только после проверки профиля.
Pod Running, но сервис медленныйRunning означает процесс, а не готовность и не capacity.Сравнить Ready, readiness probe, latency и очередь за один период.Разделить контракт готовности и гипотезу о ресурсе.
HPA меняет replicas без эффектаНовые Pod не готовы, либо CPU target считается от неверного request.Проверить effective request, metric source, Ready и время прогрева.Не повышать maxReplicas, пока не исправлен сигнал.
Container получает OOMKilledMemory limit ограничивает container, но не описывает жизненный цикл cache или объектов.Сопоставить предел, рост working set и действие приложения при нехватке памяти.Изменять limit вместе с политикой роста и восстановления.
\n

Как устроена связка

\n

Request — заявка на ресурс для размещения. Scheduler использует её, когда выбирает Node. Для Pod ресурсная заявка складывается из заявок его containers. Request не равен фактическому потреблению. Container может использовать больше request, если на Node есть свободный ресурс и limit это позволяет. Поэтому фраза «у Pod есть 500m CPU» неполна: нужно сказать, это request, limit или наблюдаемое usage.

\n

Limit — верхняя граница исполнения для container. Он не обещает пропускную способность и не является знаменателем CPU utilization HPA. Для CPU превышение limit ограничивает доступ к CPU. Для memory превышение может закончиться убийством container. Применение зависит от ресурса и среды, поэтому нельзя переносить правило для CPU на memory. Если limit задан без request, конкретная admission-политика может использовать limit как request. Effective значения нужно увидеть в разрешённой проверке, а не угадывать по шаблону.

\n

Readiness отвечает на узкий вопрос: можно ли сейчас отправлять трафик в этот container. При failed readiness Kubernetes убирает Pod из EndpointSlice соответствующего Service. Probe не измеряет запас capacity, throughput и качество каждого ответа. Не стоит включать в неё все внешние зависимости без явной политики отказа: краткий сбой одной зависимости способен вывести из трафика все реплики. Обратная ошибка не менее опасна: слишком ранний success пускает запросы до окончания прогрева.

\n

HPA периодически меняет desired replicas по наблюдаемой метрике. Для CPU utilization в процентах важен request, к которому относится usage. Target 70 процентов — это не 70 процентов Node и не 70 процентов limit. Если request отсутствует там, где он нужен для расчёта, вывод по такой метрике нельзя считать осмысленным. При этом новый Pod ещё должен пройти startup и readiness. Автомасштабирование не может исправить неверный healthcheck и не сокращает время загрузки большого cache.

\n

Учебный манифест

\n

Ниже — ограниченный пример для HTTP-сервиса с устойчивой CPU-нагрузкой после прогрева. Значения не описывают реальный production workload. Они нужны, чтобы увидеть отношения между полями. Здесь request равен 500m, limit — 1000m, а target HPA относится к request. Startup probe отделяет запуск от liveness и readiness. Readiness проверяет локальный признак готовности приложения, а не весь внешний мир.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: app\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: app\n  template:\n    metadata:\n      labels:\n        app: app\n    spec:\n      containers:\n        - name: app\n          image: registry.example/app:1.4.0\n          resources:\n            requests:\n              cpu: \"500m\"\n              memory: \"384Mi\"\n            limits:\n              cpu: \"1000m\"\n              memory: \"768Mi\"\n          startupProbe:\n            httpGet: { path: /startup, port: 8080 }\n            failureThreshold: 30\n            periodSeconds: 2\n          readinessProbe:\n            httpGet: { path: /ready, port: 8080 }\n            periodSeconds: 5\n          livenessProbe:\n            httpGet: { path: /live, port: 8080 }\n            periodSeconds: 10\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: app\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: app\n  minReplicas: 2\n  maxReplicas: 6\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

Этот манифест не доказывает, что 500m достаточно. Он задаёт гипотезу: после прогрева одна реплика выдерживает согласованную steady-нагрузку, а CPU — её главный ограничитель. Если в реальности первым растёт memory или очередь, HPA по CPU не решает проблему. Если /ready отвечает до открытия рабочих пулов, реплики формально Ready, но фактически бесполезны. Отрицательный путь важен: когда гипотеза не подтверждается, нужно остановить изменение чисел и пересмотреть профиль, а не автоматически добавлять replicas.

\n

Иллюстрация границ Pod

\n
\"Связь
Схема отделяет состояния Pending, Running, Ready и Unready от границ ресурсов. Она иллюстрирует порядок рассуждения и не показывает данные живого кластера.
\n

Схему полезно читать слева направо. Профиль задаёт вопрос к manifest: что является единицей работы и какой ресурс ограничивает её первым. Request влияет на размещение. Limit ограничивает исполнение. Только затем readiness отвечает на вопрос о допуске к Service traffic. HPA использует свою метрику и свой знаменатель. Перепрыгнуть через профиль нельзя: иначе одинаковый target будет означать разные вещи для CPU-bound HTTP, memory-retaining cache и batch-задачи.

\n

Проверка на конкретном workload

\n
  1. Назовите workload. Запишите owner, режим serving или batch, обычный startup, рабочую единицу и dominant resource. Фраза «сервис нагружен» для этого слишком расплывчата.
  2. Разберите каждый container. Выпишите CPU и memory request и limit отдельно. Укажите, складываются ли значения нескольких containers. Разрешённым способом проверьте admission defaults и effective manifest.
  3. Зафиксируйте readiness contract. Одним предложением опишите, что значит «можно принять запрос». Отдельно назовите случаи, когда Pod должен стать Unready, и случаи, которые не должны выводить его из трафика.
  4. Проверьте startup и liveness. Убедитесь, что долгая инициализация не выглядит как зависший процесс. Liveness должна обнаруживать неисправимое состояние, а не временную очередь или медленную внешнюю зависимость.
  5. Опишите метрику HPA. Запишите тип метрики, источник, request-знаменатель для utilization, minReplicas, maxReplicas и поведение при missing или not-yet-ready Pod. Не подменяйте эту проверку значением из учебного примера.
  6. Соберите ограниченное evidence. Выберите среду, владельца, временное окно и разрешённые источники. Сопоставьте Ready, usage, restart, latency и queue с одной гипотезой. Не объединяйте их в безымянное «состояние Pod».
  7. Измените одну границу. Задайте stop condition и rollback для request, limit, probe или HPA. После изменения повторите ту же проверку. Если сигнал не изменился, вернитесь к причине, а не к следующему коэффициенту.
\n

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

\n

Эта схема не назначает ресурсы настоящему сервису и не подтверждает состояние кластера. Она не читает Metrics API, события Node, EndpointSlice, controller flags, feature gates, логи или trace. Официальные документы описывают механизм Kubernetes, но не знают версию, admission-политику и настройки конкретной среды. Поэтому учебные 500m, 384Mi, 70 процентов и два Pod нельзя выдавать за результат измерения.

\n

Есть и граница применимости. HPA по CPU подходит не каждому workload. Для memory-heavy приложения нужно отдельно описать рост рабочего набора и способ его ограничить. Для batch часто важнее размер очереди и время обработки. Для сервиса с дорогим warmup нужно учитывать startup и скорость появления Ready backend. Если исходный сигнал не объясняет цену ошибки, корректное действие — не менять manifest. Сначала нужно получить недостающее разрешённое наблюдение или признать задачу неготовой к настройке.

\n

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

\n

Конфигурация готова к обсуждению, когда для одного workload существует короткая карточка: request и limit каждого container, смысл readiness, условие startup, назначение liveness, metric HPA и её знаменатель, источник evidence, stop condition и rollback. Проверка должна связать каждое поле с одним наблюдаемым вопросом. После учебного прогона готовность не означает «Pod зелёный». Она означает, что команда может объяснить, какой сигнал изменился, почему это подтверждает или опровергает гипотезу и что произойдёт при отрицательном результате.

\n

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

" + "contentHtml": "

После выкладки Pod получает статус Running, но запросы к сервису ждут дольше обычного. Иногда HorizontalPodAutoscaler (HPA) увеличивает число реплик, а доступных backend не становится больше: новые Pod не проходят readiness. В другой версии проблемы endpoint отвечает успешно ещё до прогрева, и Service отправляет трафик в приложение, которое не готово к рабочей нагрузке. Команда видит зелёный rollout и ищет причину уже под трафиком.

\n

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

\n

Сначала разделите симптомы

\n
Один симптом не заменяет проверку соседних границ
НаблюдениеЧто оно подтверждаетЧто проверить дальше
Pod остаётся PendingPod не назначен на Node; request может не помещаться в доступный ресурс.События Pod, requests всех containers, allocatable Node, taints и affinity.
Pod Running, но не ReadyПроцесс запущен, но probe не разрешает отправлять ему трафик.Смысл endpoint, результаты probe, время прогрева и состояние Service.
Pod Ready, latency растётТекущая probe пропускает трафик; она не доказывает запас capacity.CPU, memory, очередь, внешние вызовы и лимиты приложения.
HPA поднял replicas без эффектаРешение о масштабировании было принято, но новые реплики могут не стать Ready или метрика не описывает bottleneck.Метрику HPA, request-знаменатель, события, readiness и время появления backend.
Container перезапускается с OOMKilledПамять пересекла ограничение или приложение получило другую фатальную ошибку.Причину завершения, working set, limit, рост cache и политику восстановления.
\n

Начинайте с колонки «наблюдение», а не с изменения манифеста. Running описывает состояние процесса, не готовность к запросам. Значение CPU в HPA не означает процент от Node или от limit: для ресурсной метрики utilization это отношение usage к request. Эти различия и есть рабочая карта диагностики.

\n

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

\n

Request — заявка на ресурс. Scheduler учитывает requests контейнеров, когда выбирает Node; для обычного Pod суммарная заявка контейнеров определяет, сколько ресурса нужно зарезервировать при размещении. Request не равен фактическому usage: container может потреблять больше заявки, если это разрешают limit и свободный ресурс. Без request нельзя осмысленно интерпретировать CPU utilization HPA для такого контейнера.

\n

Limit — ограничение выполнения контейнера. Для CPU превышение limit может привести к throttling, а для memory превышение может завершить процесс с OOMKilled. Limit не является обещанием throughput и не заменяет нагрузочное измерение. Если request или limit добавляет admission-политика namespace, смотрите итоговый объект в кластере, а не только исходный файл.

\n

Readiness probe отвечает на узкий вопрос: можно ли сейчас отправить запрос этому контейнеру. При неуспешной readiness Kubernetes перестаёт считать Pod готовым endpoint для Service. Probe не измеряет запас capacity и не должна бездумно превращаться в проверку всех внешних зависимостей. Иначе краткий сбой партнёра способен убрать из трафика все реплики. Слишком ранний успешный ответ создаёт обратную проблему: приложение принимает запросы до открытия пулов, миграций или cache.

\n

HPA периодически вычисляет желаемое число реплик по метрике. Для CPU с averageUtilization: 70 контроллер сравнивает среднее потребление с CPU request, а не с limit. Pod без нужного request не даёт корректного CPU utilization; Pod, который ещё не готов или не имеет метрики, может учитываться консервативно в расчёте. Поэтому HPA нельзя читать без describe, состояния реплик и понимания того, откуда пришла метрика.

\n

Учебный манифест с явной гипотезой

\n

Ниже — минимальный serving-workload для HTTP-приложения. Предположим, что после прогрева CPU — главный ограничитель, endpoint /startup появляется один раз, /ready проверяет локальную готовность пулов, а /live отвечает только при неисправимом состоянии процесса. Значения 500m, 384Mi, две реплики и target 70% — не рекомендация и не результат измерения. Это числа для воспроизводимого учебного прогона.

\n
apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: app\nspec:\n  replicas: 2\n  selector:\n    matchLabels:\n      app: app\n  template:\n    metadata:\n      labels:\n        app: app\n    spec:\n      containers:\n        - name: app\n          image: registry.example/app:1.4.0\n          ports:\n            - name: http\n              containerPort: 8080\n          resources:\n            requests:\n              cpu: \"500m\"\n              memory: \"384Mi\"\n            limits:\n              cpu: \"1000m\"\n              memory: \"768Mi\"\n          startupProbe:\n            httpGet:\n              path: /startup\n              port: http\n            periodSeconds: 2\n            failureThreshold: 30\n          readinessProbe:\n            httpGet:\n              path: /ready\n              port: http\n            periodSeconds: 5\n            timeoutSeconds: 2\n          livenessProbe:\n            httpGet:\n              path: /live\n              port: http\n            periodSeconds: 10\n            timeoutSeconds: 2\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: app\nspec:\n  selector:\n    app: app\n  ports:\n    - name: http\n      port: 80\n      targetPort: http\n---\napiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n  name: app\nspec:\n  scaleTargetRef:\n    apiVersion: apps/v1\n    kind: Deployment\n    name: app\n  minReplicas: 2\n  maxReplicas: 6\n  metrics:\n    - type: Resource\n      resource:\n        name: cpu\n        target:\n          type: Utilization\n          averageUtilization: 70
\n

startupProbe отделяет медленный запуск от последующих проверок: пока она не завершилась успешно, liveness и readiness не начинают обычную работу. Это защищает процесс от преждевременного перезапуска, но не ускоряет прогрев. После прогрева readiness должна означать «этот Pod может принять обычный запрос», а не «процесс слушает порт». У HPA есть верхняя граница шесть реплик, но она не гарантирует, что шесть Pod поместятся в кластер или выдержат запросы.

\n

Иллюстрация пути запроса

\n
\"Путь
Учебная схема разделяет размещение Pod, допуск к Service и масштабирование по метрике. Она показывает порядок вопросов, а не состояние конкретного кластера.
\n

Читать схему нужно слева направо. Сначала scheduler решает, где Pod может быть размещён по requests. Затем kubelet запускает контейнер и выполняет startup, readiness и liveness в своих ролях. Service направляет запросы только к готовым backend. Параллельно HPA получает свою метрику и меняет desired replicas. При таком порядке увеличение replicas не исправляет неверный readiness, а поднятие memory limit не исправляет очередь запросов.

\n

Воспроизводимый прогон

\n

Сохраните манифест в файл app.yaml в тестовом namespace и сначала проверьте его сервером. Эта команда обращается к API и может требовать прав; локальный кластер, версия Kubernetes, policy admission и наличие Metrics API должны быть известны заранее.

\n
kubectl config current-context\nkubectl auth can-i create deployment -n demo\nkubectl describe pod -n demo -l app=app --show-events\nkubectl apply --dry-run=server -n demo -f app.yaml\nkubectl diff -n demo -f app.yaml\nkubectl apply -n demo -f app.yaml\nkubectl rollout status deployment/app -n demo --timeout=120s\nkubectl get pods -n demo -l app=app -o wide\nkubectl get endpointslice -n demo -l kubernetes.io/service-name=app\nkubectl describe hpa app -n demo\nkubectl top pods -n demo -l app=app
\n

Последняя команда требует работающего Metrics API, часто его предоставляет Metrics Server. Если она возвращает ошибку, это не доказательство нулевой нагрузки и не повод подставить число вручную. Зафиксируйте ошибку как ограничение прогона. kubectl diff и --dry-run=server также не заменяют rollout: первая сравнивает объект, вторая проверяет запрос к API, а третья показывает, что Deployment действительно продвигается. Команда describe pod -l удобна как быстрый запрос, но при нескольких Pod для детального анализа выберите конкретное имя из get pods.

\n

Как читать результат и отрицательный путь

\n
  1. Проверьте контекст. Убедитесь, что команды обращаются к ожидаемому кластеру и namespace. Не применяйте учебный файл в production только потому, что текущий context называется коротко.
  2. Отделите размещение от запуска. Для Pending прочитайте kubectl describe pod и события. Сопоставьте requests с allocatable, а не с общей памятью Node. Если Pod размещён, переходите к probes.
  3. Проверьте время. Сравните длительность startup с failureThreshold × periodSeconds. Для этого примера окно startup — до 60 секунд при последовательных неуспехах, но реальное время зависит от результата probe и приложения.
  4. Сверьте Ready и endpoint. Сопоставьте поле Ready у Pod с EndpointSlice и фактическим ответом /ready. Если Pod Ready, а latency растёт, probe выполняет свой узкий контракт; ищите bottleneck в CPU, memory, очереди или внешнем вызове.
  5. Разберите HPA. В describe hpa найдите текущую и целевую метрики, desired replicas, события и ошибки. Сопоставьте их с request. Отсутствие данных Metrics API нельзя трактовать как отсутствие нагрузки.
  6. Проверьте отрицательный путь. Если новые Pod не становятся Ready, не увеличивайте maxReplicas. Сначала исправьте контракт готовности или startup, затем повторите тот же прогон. Если HPA масштабируется, но latency не улучшается, проверьте, является ли CPU главным ограничителем.
  7. Изменяйте одну границу. Зафиксируйте гипотезу, одно изменение, окно наблюдения, критерий остановки и rollback. Иначе после одновременного изменения limit, probe и HPA нельзя понять, что именно изменило результат.
\n

Какие числа можно считать доказанными

\n

В учебном файле доказан только синтаксический и объектный контракт, если его принял API. Значение 500m становится обоснованным request лишь после повторяемого измерения representative-нагрузки с согласованным latency budget и запасом на пики. Target 70% становится рабочей гипотезой только вместе с наблюдаемым временем масштабирования, размером очереди и готовностью новых реплик. Значение 768Mi нельзя оправдать одной строкой OOMKilled: нужно понять, растёт ли cache, есть ли утечка и сколько памяти нужно процессу при штатном пике.

\n

После каждого изменения сохраняйте минимум: версию образа, итоговый Deployment, временное окно, входную нагрузку, latency/error rate, состояние Pod и HPA, а также решение об откате. Это превращает настройку из перебора коэффициентов в проверяемый эксперимент. Если не хватает разрешённого наблюдения, корректное действие — остановить эксперимент, а не додумывать результат.

\n

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

\n

Схема рассчитана на stateless HTTP-сервис, который можно безопасно масштабировать горизонтально. Она не решает координацию StatefulSet, порядок миграций базы, работу с локальным диском, очередями, GPU или внешним rate limit. HPA по CPU может быть плохим сигналом для memory-heavy приложения, batch-задачи или сервиса, где bottleneck — очередь и время ответа партнёра. Для таких случаев нужна другая метрика и отдельная проверка её источника.

\n

Точный результат зависит от версии Kubernetes, API и controller flags, Metrics API, admission defaults, политики namespace, сетевого маршрута и реализации приложения. Схема также не проверяет PDB, topology spread, node autoscaling и безопасность образа. Наличие статуса Available или зелёного rollout не является доказательством производительности. Граница вывода простая: эта статья даёт порядок проверки, а не готовый production-манифест.

\n

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

\n", + "readingMinutes": 9 } diff --git a/editorial/agent-rewrites/130.json b/editorial/agent-rewrites/130.json index ea88bb3..2774225 100644 --- a/editorial/agent-rewrites/130.json +++ b/editorial/agent-rewrites/130.json @@ -2,6 +2,6 @@ "index": 130, "slug": "editorial-2024-05-field-platform-templates", "title": "Платформенный шаблон без ловушки fork: как провести границу решения", - "excerpt": "Шаблон ускоряет повторяемый путь, но не заменяет архитектурное решение. Разбираем симптомы слишком широкой формы, проверяем golden path, узкое расширение и корректный отказ.", - "contentHtml": "

Новая команда просит создать сервис, а платформа предлагает одну форму: имя, владелец, runtime, репозиторий и несколько флагов. Сначала это выглядит удобно. Через месяц появляются локальные правки, особые healthcheck, другой retention и ручные исключения в CI. Два сервиса уже не похожи на исходный шаблон, но команда всё ещё считает их его вариантами. Цена ошибки — не только лишняя работа. Теряется владелец контракта, обновления перестают доходить до копий, а рискованный выбор прячется за кнопкой Create.

\n

Тезис простой: шаблон должен принимать только повторяемый класс задач. Совпадение с базовым контрактом ведёт в golden path. Одно заранее описанное отличие ведёт на review расширения. Несовместимый или одноразовый запрос нужно остановить. Отказ дешевле fork-а, который создаёт ложное ощущение поддержки.

\n

Симптомы широкой формы

\n

Первый симптом — просьба добавить универсальное поле: «если понадобится, разрешим внешний data class», «пусть runtime выбирается позже», «добавим произвольный adapter». Поле кажется небольшим, но меняет инвариант. После него шаблон уже не описывает один тип сервиса. Он принимает несколько архитектурных решений без владельца.

\n

Второй симптом — локальный patch сразу после создания репозитория. Команда удаляет обязательный шаг, переписывает pipeline или меняет доступы, а потом обещает вернуть полезное изменение в общий шаблон. Если различие не имеет имени, владельца, границы и условия удаления, это не extension. Это отдельный проект, который маскируется под стандартный путь.

\n

Третий симптом — платформа выдаёт skeleton для задачи, у которой ещё нет data policy, access model или ответственного. Файлы создаются быстро, но структура начинает диктовать решение. Команда подгоняет требования под уже созданный репозиторий. Технический артефакт появляется раньше архитектурного договора.

\n

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

\n

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

\n

Backstage описывает шаблон как набор параметров и последовательных шагов. Это полезный механизм, но он не делает любой параметр безопасным. Параметр собирает значение. Решение о том, разрешено ли значение, должно жить в контракте и проверке. GitHub template repository копирует структуру и файлы в новый репозиторий, но создаёт несвязанную историю. Автоматического канала изменений исходного шаблона это не даёт.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Просят «универсальный» флагПоле меняет runtime, ownership, data class, access или retentionСравнить поле с базовым инвариантом и назвать владельцаУбрать поле из golden path; оформить отдельный путь
После создания нужен patchРазличие не описано как versioned extensionПроверить identifier, owner, boundary и rollbackОстановить копирование; вынести различие на review
Шаблон приняли для миграцииНет повторяемого типа и жизненного циклаПроверить повторяемость того же контрактаОтказать шаблону и провести отдельное решение
Repository считают fork-омСмешаны копирование и наследование измененийПроверить историю и канал обновленийЗафиксировать самостоятельное владение или другой механизм
\n
\"Цикл
Сначала запрос сверяется с контрактом, затем выбирается путь. Схема не обозначает измеренный adoption или production-результат.
\n

Пример контракта

\n

Ниже — учебный фрагмент. Он не запускает реальную задачу и не доказывает пригодность набора полей для вашей организации. Базовый путь принимает внутренний HTTP-сервис с известным владельцем и утверждённым runtime. Observability adapter разрешён как именованное расширение. Внешние регулируемые данные форму не проходят.

\n
apiVersion: scaffolder.backstage.io/v1beta3\\nkind: Template\\nmetadata:\\n  name: internal-http-service\\nspec:\\n  owner: group:platform\\n  type: service\\n  parameters:\\n    - title: Service contract\\n      required: [name, owner, runtime, dataClass]\\n      properties:\\n        runtime:\\n          type: string\\n          enum: [node20, go122]\\n        dataClass:\\n          type: string\\n          enum: [internal]\\n        extension:\\n          type: string\\n          enum: [none, observability-adapter]\\n  steps:\\n    - id: write-skeleton\\n      action: fetch:template\\n    - id: publish\\n      action: publish:github
\n

В примере enum ограничивает форму, но не заменяет проверку прав и политики. Owner должен ссылаться на существующую группу, а публикация требует разрешений и проверки credentials. В рабочем шаблоне эти условия подтверждаются средствами вашей платформы. YAML не доказывает успешный запуск, безопасность или пригодность runtime.

\n

Первая заявка содержит известный service type, владельца, approved runtime и internal data. Она совпадает с контрактом: golden path. Вторая содержит те же факты и один adapter из закрытого списка. Если у расширения есть owner, граница и способ удаления, это review extension. Третья описывает одноразовую регулируемую миграцию, но не содержит владельца, retention и access model. Ей нужен отказ от шаблона и отдельное решение.

\n

Почему fork не исправляет несовпадение

\n

Fork отвечает на вопрос «как начать самостоятельный проект на основе текущих файлов». Он не отвечает на вопрос «как поддерживать общий контракт между проектами». Repository, созданный из template, получает несвязанную историю. Pull request между копией и шаблоном не становится штатным каналом синхронизации.

\n

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

\n

Не расширяйте базовый шаблон ради редкого запроса. Новое поле увеличивает число состояний для всех пользователей. Особенно опасны поля, которые откладывают решение: «позже выберем runtime», «потом определим доступ», «retention настроит команда». Отсутствующее решение нельзя превратить в безопасный default названием параметра.

\n

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

\n
  1. Запишите заявку: component type, owner, runtime, data class, access, retention и срок жизни. Не начинайте с копирования файлов.
  2. Сверьте каждый факт с контрактом. Пометьте значения, для которых нет допустимого варианта или владельца.
  3. Выберите результат. Полное совпадение даёт golden path. Одно названное отличие с границей и rollback даёт review extension. Остальные запросы получают decline.
  4. Проверьте отрицательный путь: неизвестный runtime, внешний data class, отсутствие owner и неподдерживаемое расширение должны остановиться до создания артефакта.
  5. Для расширения зафиксируйте identifier, owner, boundary, version и условие удаления. Если поле нельзя заполнить, расширение не готово.
  6. После review запускайте реальную публикацию. Проверьте credentials, права, pipeline, healthcheck, наблюдаемость и rollback в вашей среде.
  7. Пересмотрите расширение через согласованный срок. Повторяемый класс можно включить в новую версию контракта. Одноразовый случай не превращайте в обязательную опцию.
\n

Ограничения

\n

Шаблон не выбирает владельца, не определяет классификацию данных и не делает action безопасным. Список runtime устаревает. Документация объясняет форму и порядок шагов, но не знает ваших сетевых прав, требований регулятора и правил отката. Эти условия проходят отдельную проверку.

\n

Учебный YAML не является production-конфигурацией. В нём нет конкретной схемы прав, политики секретов, branch protection, SLO и обязательных проверок поставки. Не переносите его в рабочую систему без адаптации и review. Цифры adoption, скорости и снижения дефектов здесь не заявлены.

\n

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

\n

Решение готово, когда другая команда берёт ту же заявку и получает тот же вердикт по тем же фактам. Для golden path форма принимает только значения контракта. Для extension документ содержит owner, boundary, version и rollback. Для отказа есть причина и следующий вопрос вне шаблона. Неизвестное или рискованное значение останавливается до создания репозитория и не превращается в локальный patch.

\n

Проверка состоит из четырёх записей: базовая заявка, узкое расширение, несовместимая заявка и повторный запуск первой. Готовность есть, если базовые записи дают одинаковый результат, расширение не меняет инварианты, отказ не создаёт артефакт, а повторный запуск не дублирует обязательные действия.

\n

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

" + "excerpt": "Шаблон ускоряет повторяемый путь, но не заменяет архитектурное решение. Разбираем симптомы слишком широкой формы, проверяем контракт, безопасное расширение и корректный отказ до создания репозитория.", + "contentHtml": "

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

\n

Так шаблон превращается в каталог скрытых архитектурных решений. Кнопка Create всё ещё выглядит простой, но за ней уже нет единого контракта: разные команды получают разные права, жизненные циклы и обязанности поддержки. Тезис статьи простой: автоматизировать стоит повторяемую задачу, а несовпадение нужно увидеть до генерации репозитория. Полное совпадение ведёт в golden path, одно явно ограниченное отличие — на review расширения, изменение инварианта — к отказу от шаблона.

\n

Симптомы шаблона, который стал слишком широким

\n

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

\n

Второй симптом — patch сразу после создания проекта. Команда выключает обязательную проверку, переписывает pipeline или меняет видимость репозитория, а затем обещает вернуть полезную часть в общий шаблон. Если различие нельзя назвать, назначить ему владельца, очертить границу и описать обратный ход, это не расширение базового пути. Это самостоятельный проект, который временно маскируется под стандартный.

\n

Третий симптом — шаблон создаёт код раньше, чем зафиксированы класс данных и модель доступа. Skeleton уже содержит Dockerfile, workflow и manifest, поэтому команда начинает подгонять требования под готовую структуру. Платформа помогла быстро получить файлы, но незаметно стала источником политики. Удобная форма не должна принимать решение за владельца данных или службы безопасности.

\n

Что именно делает шаблон, а чего он не делает

\n

В Backstage Software Templates пользователь вводит параметры, после чего Scaffolder выполняет последовательность шагов. В документации среди таких шагов показаны загрузка skeleton, подстановка значений и публикация результата в GitHub или GitLab. Это механизм автоматизации, а не доказательство того, что любое сочетание параметров разрешено. Допустимые значения, обязательные поля и проверки должны быть частью контракта вашей платформы.

\n

GitHub template repository решает другую задачу: создаёт новый репозиторий с той же структурой, ветками и файлами. GitHub отдельно предупреждает, что ветки из шаблона имеют несвязанные истории; новый fork, напротив, сохраняет историю родительского репозитория. Поэтому template — удобный старт нового проекта, но не канал доставки обновлений во все уже созданные проекты. Ошибка начинается, когда копию называют fork-ом и обещают ей автоматическое наследование.

\n
Решение до генерации: симптом, проверка и действие
НаблюдениеЧто проверяемВердиктСледующий шаг
Все входы входят в закрытый контрактТип сервиса, владелец, runtime, класс данных и видимость имеют допустимые значенияGolden pathЗапустить стандартные шаги и записать версию контракта
Есть одно отличие от базыУ отличия есть идентификатор, owner, граница, срок действия и rollbackReview extensionРассмотреть отдельную ветку; не добавлять свободное поле в форму
Меняется инвариантПоявляется новый класс данных, модель доступа или жизненный циклDecline templateОстановить генерацию и провести отдельное архитектурное решение
Просят синхронизировать копии с базойЕсть ли реальный канал поставки обновлений и владелец миграцийНе обещать наследованиеВыбрать upstream-механику или зафиксировать независимое владение
\n
\"Схема
Развилка должна быть видна до генерации проекта: свободный параметр скрывает отличие, а именованное расширение оставляет его проверяемым.
\n

Контракт: входные факты, инварианты и результат

\n

Начните не с YAML, а с короткой заявки. Запишите тип компонента, владельца, runtime, класс данных, видимость репозитория, требования к доступу и срок хранения. Затем отделите инварианты от вариантов. Например, для внутреннего HTTP-сервиса инвариантами могут быть подтверждённый владелец, закрытый класс данных и один из поддерживаемых runtime. Название проекта и регион могут быть вариантами, если они не меняют модель риска.

\n

У контракта должны быть три явных результата. Golden path применяет стандарт без ручного решения. Расширение добавляет ровно одну заранее названную возможность и проходит review до создания артефакта. Отказ не означает «никогда»: он говорит, что текущая форма не является правильным местом для нового инварианта. Следующий вопрос должен вести к отдельному design path — с владельцем и проверками.

\n

Поле допустимо только тогда, когда его множество значений закрыто или его проверка определена отдельно. Удобное правило: если для значения нельзя сразу назвать owner, policy и способ отката, значение не должно попадать в golden path. Неизвестный runtime нельзя сделать безопасным значением с помощью default, а внешний data class нельзя превратить во внутренний одним текстом подсказки.

\n

Учебный шаблон Backstage с закрытыми значениями

\n

Ниже приведён минимальный учебный фрагмент для Backstage Scaffolder. Он показывает форму и порядок шагов, но не является готовой конфигурацией вашей инсталляции. В реальном проекте замените URL skeleton, организацию, группу владельца и action публикации на разрешённые вашей платформой значения.

\n
apiVersion: scaffolder.backstage.io/v1beta3\nkind: Template\nmetadata:\n  name: internal-http-service\nspec:\n  owner: group:platform\n  type: service\n  parameters:\n    - title: Service contract\n      required:\n        - name\n        - owner\n        - runtime\n        - dataClass\n        - repoVisibility\n      properties:\n        name:\n          title: Service name\n          type: string\n          pattern: '^[a-z0-9-]+$'\n        owner:\n          title: Owning group\n          type: string\n        runtime:\n          title: Runtime\n          type: string\n          enum:\n            - node20\n            - go122\n        dataClass:\n          title: Data class\n          type: string\n          enum:\n            - internal\n        repoVisibility:\n          title: Repository visibility\n          type: string\n          enum:\n            - private\n            - internal\n  steps:\n    - id: fetchBase\n      name: Fetch approved skeleton\n      action: fetch:template\n      input:\n        url: ./template\n        values:\n          name: ${{ parameters.name }}\n          runtime: ${{ parameters.runtime }}\n    - id: publish\n      name: Publish repository\n      action: publish:github\n      input:\n        repoUrl: 'github.com?owner=acme&repo=${{ parameters.name }}'\n        repoVisibility: ${{ parameters.repoVisibility }}
\n

В этом примере enum ограничивает runtime, класс данных и видимость, а pattern отсекает часть случайных имён. Это полезная граница формы, но не полноценная авторизация. Строка owner всё ещё должна разрешаться по каталогу групп, action публикации требует настроенной интеграции и credentials, а закрытый класс данных не отменяет проверку содержимого skeleton. Backstage описывает owner шаблона как ответственную сущность, но прямо отделяет это поле от runtime-авторизации. Проверки прав и секретов остаются обязанностью конфигурации вашей платформы.

\n

Воспроизводимая проверка разницы между template и fork

\n

Проверить обещание «проект наследует шаблон» можно без спора о терминах. Создайте тестовый репозиторий из шаблона, клонируйте оба репозитория и сравните корневые коммиты. В команде ниже замените acme/service-template и acme/orders-api на доступные вам репозитории. Команда создания изменяет удалённый GitHub и поэтому предназначена только для тестового владельца, у которого есть право создавать репозитории.

\n
gh repo create acme/orders-api --private --template acme/service-template\ngh repo clone acme/service-template /tmp/service-template\ngh repo clone acme/orders-api /tmp/orders-api\nprintf 'template roots: '\ngit -C /tmp/service-template rev-list --max-parents=0 HEAD\nprintf 'created roots: '\ngit -C /tmp/orders-api rev-list --max-parents=0 HEAD\ngit -C /tmp/orders-api log --oneline --decorate -5
\n

Ожидаемый результат для GitHub template — новый репозиторий с собственной начальной историей. Это не измерение качества шаблона и не проверка того, что файлы совпадают навсегда. Для проверки содержимого зафиксируйте commit шаблона, сравните нужные файлы и отдельно решите, как доставлять будущие изменения: pull request из upstream, пакет, генератор миграций или ручное владение. Если такого канала нет, в документации проекта нужно прямо написать: после создания репозиторий самостоятельный.

\n

Как оформить узкое расширение

\n

Расширение — это не поле «custom». Сначала дайте ему имя, например observability-adapter-v1, и опишите, что оно добавляет и чего не меняет. Затем назначьте owner, перечислите затронутые файлы и разрешения, задайте условие включения, срок пересмотра и rollback. Владелец должен быть способен принять инцидент и удалить расширение, а не только согласовать его в каталоге.

\n

Проверка расширения должна включать положительный и отрицательный случаи. Положительный случай доказывает, что базовый сервис создаётся и получает adapter. Отрицательный — что произвольное значение или другой класс данных останавливают задачу до публикации. Если action уже создал репозиторий, а затем обнаружил несовместимость на deploy, граница стоит слишком поздно: потребуется cleanup, а часть риска уже прошла.

\n

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

\n

Отрицательный путь: когда шаблон обязан остановиться

\n
  1. Зафиксируйте заявку до создания файлов: тип компонента, owner, runtime, data class, видимость, access и retention.
  2. Сверьте каждое поле с версией контракта. Неизвестное значение пометьте как несовместимое, а не как «временное».
  3. Проверьте владельца по реальному каталогу групп и убедитесь, что он отвечает и за результат, и за последующие изменения.
  4. Запустите dry-run или тестовый Scaffolder task, если такая возможность включена в вашей Backstage-инсталляции. Убедитесь, что отказ происходит до publish.
  5. Для расширения проверьте owner, границу, permissions, rollback и срок пересмотра. Отсутствующий пункт — причина вернуть запрос на review.
  6. После создания проверьте не только наличие файлов, но и visibility, branch protection, CI, секреты, healthcheck и наблюдаемость в вашей среде.
  7. Запишите commit шаблона и версию контракта рядом с созданным проектом. Это позволяет понять, от какой исходной формы он стартовал, даже если история репозиториев несвязана.
\n

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

\n

Описанный подход не заменяет threat model, классификацию данных, согласование доступа и правила вашей организации. Backstage может проверять схему параметров и выполнять actions, но конкретный набор integrations, permissions, secrets и custom actions зависит от инсталляции. Учебный YAML не доказывает безопасность публикации и не должен переноситься в production без адаптации и review.

\n

Команда gh repo create требует установленного GitHub CLI, входа в нужный аккаунт и права создания репозитория; выполнение команды изменяет внешний GitHub. Сравнение корневых коммитов доказывает различие историй только для конкретной пары репозиториев. Оно не проверяет актуальность файлов, policy или pipeline. Для GitLab, другой forge или внутреннего шаблонизатора действуют аналогичные вопросы, но точные команды и семантика могут отличаться.

\n

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

\n

Шаблон готов к использованию, когда другая команда может взять ту же заявку и получить тот же вердикт по тем же фактам. Для golden path разрешены только значения закрытого контракта. Для extension есть owner, boundary, version, проверка отрицательного пути и rollback. Для отказа указана причина и следующий владелец отдельного решения. Ни один неизвестный параметр не создаёт репозиторий «на авось».

\n

Минимальный набор доказательств — четыре записи: стандартная заявка, разрешённое расширение, несовместимая заявка и повтор стандартной заявки. Первая и четвёртая дают один результат, вторая не меняет базовых инвариантов, третья останавливается до publish. После этого отдельно проверяются права, secrets, CI и ручные шаги окружения. Такой результат честнее, чем обещание, что одна форма поддерживает все будущие варианты.

\n

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

" }