8 lines
24 KiB
JSON
8 lines
24 KiB
JSON
{
|
||
"index": 127,
|
||
"slug": "editorial-2024-06-field-container-orchestration",
|
||
"title": "Kubernetes без ложных сигналов: как связать rollout, readiness и HPA",
|
||
"excerpt": "После обновления образа новые Pod могут запускаться, но не получать трафик, а HPA — не менять число реплик. Разбираем четыре разных контура, команды проверки и безопасный критерий готовности.",
|
||
"contentHtml": "<p>После обновления образа часть Pod остаётся на старой версии, новые Pod долго имеют статус <code>NotReady</code>, а CPU на графике растёт. Первая реакция обычно сводится к увеличению <code>maxReplicas</code> или CPU limit. Но эти поля принадлежат разным контроллерам. Можно добавить реплики и не получить ни одного нового backend в Service, а можно поднять limit и изменить поведение throttling, не исправив причину задержки.</p>\n<p>Цена смешения сигналов — потеря причинной связи. Deployment может продолжать rollout, пока новая ReplicaSet не набирает доступные Pod. Readiness probe может исключить Pod из Service, хотя процесс жив. HPA может не рассчитать CPU utilization из-за отсутствующего request или недоступного metrics API. Сначала назовём владельца каждого сигнала, затем проверим ровно тот контур, который породил симптом.</p>\n<p><strong>Главный принцип.</strong> <code>Ready</code> означает допуск к трафику, а не доказанную производительность. CPU utilization в HPA — отношение потребления к объявленному request, а не процент CPU Node и не процент limit. Rolling update отвечает за замену ReplicaSet, но не исправляет плохую readiness-проверку.</p>\n<h2>Четыре контура одной рабочей нагрузки</h2>\n<p><strong>Deployment и ReplicaSet.</strong> Deployment хранит желаемый шаблон Pod и управляет ReplicaSet. При стратегии <code>RollingUpdate</code> новая ReplicaSet создаёт Pod, а старая постепенно уменьшается. Поля <code>maxSurge</code> и <code>maxUnavailable</code> ограничивают число дополнительных и недоступных Pod. Поэтому во время rollout нормально временно видеть старую и новую версии одновременно. Ненормально — считать сам факт создания нового Pod доказательством его готовности.</p>\n<p><strong>Readiness.</strong> Kubelet выполняет readiness probe на протяжении жизни контейнера. Если она не проходит, Pod получает состояние unready и Kubernetes Services не должны отправлять ему трафик. Такая probe отвечает только на вопрос «можно ли обслуживать запросы сейчас». Она не обязана доказывать отсутствие утечки памяти, правильность миграции или запас CPU. Для долгого старта применяют <code>startupProbe</code>, чтобы не превращать штатную инициализацию в перезапуски.</p>\n<p><strong>Service и EndpointSlice.</strong> Service выбирает Pod по label selector, а control plane формирует связанные EndpointSlice. В EndpointSlice условие <code>ready</code> отражает готовность endpoint; это полезная проверка фактического набора backend, а не только списка Pod. Сетевой mesh, балансировщик и настройка <code>publishNotReadyAddresses</code> могут добавить собственное поведение, поэтому команда должна проверить реальный путь трафика. Для обычного Service нельзя переносить вывод «Pod Running» на «Pod получает запросы».</p>\n<p><strong>Ресурсы и HPA.</strong> Scheduler использует requests контейнеров для выбора Node. Limits задают границу выполнения: CPU ограничивается throttling, а превышение memory limit может привести к OOM kill. HPA с ресурсной метрикой <code>averageUtilization</code> сравнивает usage с request. Если у релевантного контейнера нет request, utilization для этой метрики не определён, и HPA не обязан масштабировать по ней.</p>\n<figure><img src='/assets/editorial/2024/container-orchestration-2024-readiness-gate.svg' alt='Схема связи readiness probe и custom readiness gate: только совместное состояние формирует Ready и допуск к Service traffic, тогда как HPA CPU считает usage относительно request отдельно' loading='lazy' /><figcaption>Схема показывает границу между traffic eligibility и autoscaling. Подпись <code>synthetic.example/contract</code> на рисунке — условное имя custom condition; рисунок не представляет состояние конкретного кластера.</figcaption></figure>\n<h2>Почему старый Pod может оставаться в системе</h2>\n<p>Рассмотрим последовательность без привязки к конкретному облаку. У Deployment две реплики, стратегия — <code>RollingUpdate</code>. После смены образа контроллер создаёт новую ReplicaSet. Новый контейнер запускается, но readiness endpoint отвечает ошибкой, потому что приложение ещё загружает конфигурацию. Pod остаётся живым, однако Service не получает его в качестве готового endpoint. Старый Pod продолжает обслуживать запросы, пока контроллер не может безопасно уменьшить старую ReplicaSet.</p>\n<p>В такой ситуации фраза «релиз завис» слишком общая. Нужно разделить пять наблюдений: какой image digest реально запущен; какую condition получил новый Pod; что написано в событиях probe; какие endpoints видит Service; какое решение показывает Deployment controller. Одного вывода <code>kubectl get pods</code> недостаточно.</p>\n<div class='table-scroll'><table><caption>Симптом, источник доказательства и допустимое действие</caption><thead><tr><th scope='col'>Симптом</th><th scope='col'>Что могло произойти</th><th scope='col'>Чем проверить</th><th scope='col'>Что делать первым</th></tr></thead><tbody><tr><td>Новые Pod <code>Running</code>, но <code>READY 0/1</code></td><td>Readiness probe не проходит или custom gate остаётся false</td><td><code>kubectl describe pod</code>, conditions и Events</td><td>Проверить контракт endpoint/gate и зависимость; не увеличивать replicas вслепую</td></tr><tr><td>Старая ReplicaSet не уменьшается</td><td>Новая версия не набрала доступные Pod или достигнут лимит rollout</td><td><code>kubectl rollout status</code>, Deployment conditions, ReplicaSets</td><td>Сопоставить <code>maxUnavailable</code> с доступными Pod и найти причину Unready</td></tr><tr><td>CPU высок, HPA не меняет replicas</td><td>Нет request, нет resource metrics или выбран другой target type</td><td><code>kubectl describe hpa</code>, HPA conditions, metrics API</td><td>Восстановить источник и знаменатель; не считать отсутствующий scale доказательством низкой нагрузки</td></tr><tr><td>Pod <code>Ready</code>, но запросов нет</td><td>Selector/port не совпадает, EndpointSlice устарел или трафик идёт через другой слой</td><td>Service selector, EndpointSlice и путь data plane</td><td>Сверить label, порт и фактический backend; не менять probe без этого сравнения</td></tr><tr><td>Memory близка к limit</td><td>Растёт cache, buffer, retained object или слишком тесен лимит</td><td>Runtime metrics, restart reason, events и профиль приложения</td><td>Отделить memory investigation от CPU HPA и назвать владельца роста</td></tr></tbody></table></div>\n<h2>Минимальный манифест для проверки контракта</h2>\n<p>Ниже — учебный фрагмент. Он намеренно содержит requests, limits, startup/readiness probes, custom readiness gate и HPA. Образ, путь endpoint, имя condition и числа ресурсов нужно заменить на значения конкретного приложения. Custom readiness gate не станет <code>True</code> сам по себе: внешний контроллер или другой владелец состояния должен установить condition, иначе Pod останется неготовым.</p>\n<pre><code>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</code></pre>\n<p>В этом примере CPU target равен 70% от request, то есть 350m на Pod при request 500m. Это рабочая точка алгоритма, а не обещание, что сервис выдержит нужную нагрузку. <code>maxUnavailable: 0</code> увеличивает потребность в свободной ёмкости: новая версия должна стать доступной до удаления старой. На маленьком кластере rollout может остановиться из-за нехватки ресурсов, даже если probe исправна.</p>\n<h2>Воспроизводимая проверка в разрешённом кластере</h2>\n<p>Команды ниже только читают состояние и подходят для namespace, к которому у вас есть доступ. Сначала запишите имя Deployment, namespace и новый image digest. Затем повторяйте команды с тем же объектом; так временная картина не смешивается с другой нагрузкой.</p>\n<pre><code>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</code></pre>\n<p>Команда <code>rollout status</code> сообщает, завершился ли rollout, но не объясняет каждую причину. <code>describe pod</code> даёт condition и Events, однако не заменяет логи приложения. <code>get endpointslice</code> показывает объект control plane; если между Service и клиентом есть mesh или внешний балансировщик, его состояние нужно проверять отдельно. <code>top</code> требует работающего resource metrics API и показывает текущий срез, а не историю.</p>\n<p>Для безопасного чтения образа сравните digest, а не только короткий tag. Для проверки именно нового ReplicaSet выберите его label из вывода Deployment и повторите <code>describe pod</code> по этому selector. Если в cluster policy запрещён доступ к EndpointSlice или metrics API, это не повод подставлять число из fixture: результат проверки должен быть «источник недоступен».</p>\n<h2>Разбираем HPA без иллюзии о процентах</h2>\n<p>При <code>requests.cpu: 500m</code> и среднем потреблении 350m utilization равен 70%. При том же потреблении, но request 1000m, utilization равен 35%. CPU workload не изменился, а решение HPA стало другим. Одновременно Scheduler увидит другой reservation contract. Поэтому изменение request нельзя считать только настройкой autoscaling: оно меняет и размещение, и интерпретацию процента.</p>\n<p>Limit — другой знаменатель и другая граница. Если limit равен 1 CPU, контейнер может упереться в throttling на этой границе; HPA с <code>averageUtilization</code> всё равно сравнивает usage с request. Если memory приближается к 512Mi, это не объясняет само по себе высокий CPU и не доказывает OOM. Для memory нужно проверить restart reason, события и профиль приложения.</p>\n<p>Есть ещё одна конфликтующая настройка. Kubernetes предупреждает: когда HPA активен, применение Deployment manifest с фиксированным <code>spec.replicas</code> может снова записать число реплик и вызвать колебания. Перед тем как удалить <code>replicas</code> из manifest, проверьте способ применения и разовый эффект: API по умолчанию может трактовать отсутствие поля как одну реплику. Это изменение нужно выполнять отдельным контролируемым шагом, а не попутно с исправлением probe.</p>\n<h2>Порядок изменения и отрицательные пути</h2>\n<ol><li><strong>Зафиксируйте объект.</strong> Запишите namespace, Deployment, image digest, label selector, Service, период наблюдения и владельца изменения.</li><li><strong>Снимите декларацию.</strong> Сохраните requests/limits каждого контейнера, probes, gates, стратегию rollout, HPA metric, target, min/max и способ применения manifest.</li><li><strong>Проверьте новую версию.</strong> Убедитесь, что Pod действительно использует ожидаемый digest. Сопоставьте его condition, Events и логи с readiness-контрактом.</li><li><strong>Проверьте путь трафика.</strong> Сверьте Service selector, port/targetPort и EndpointSlice. При наличии mesh или внешнего балансировщика добавьте его собственный источник.</li><li><strong>Проверьте rollout.</strong> Сравните доступные и желаемые Pod, maxSurge/maxUnavailable, условия Deployment и время остановки. Не меняйте сразу стратегию и probe.</li><li><strong>Проверьте HPA.</strong> Убедитесь, что metrics API отвечает, request присутствует у релевантных контейнеров, target type соответствует задаче, а status содержит текущую метрику и conditions.</li><li><strong>Выберите одну гипотезу.</strong> На один эксперимент меняйте один контракт: endpoint readiness, request, limit, rollout policy или metric. Заранее задайте ожидаемое наблюдение и stop condition.</li><li><strong>Проверьте отрицательный путь.</strong> Зафиксируйте поведение при false readiness, missing request, недоступной метрике, несовпадающем selector и нехватке Node capacity. Если источник неизвестен, остановите вывод.</li><li><strong>Закройте результат.</strong> Сравните исходный симптом теми же командами, сохраните observed result, owner и rollback snapshot. Не объявляйте rollout готовым по одному зелёному статусу.</li></ol>\n<h2>Ограничения применимости и откат</h2>\n<p>Этот разбор не выбирает универсальные значения CPU, memory, probe timeout, maxReplicas или maxUnavailable. Их определяют профиль приложения, SLA, стоимость Node, размер ответа, время старта и downstream-зависимости. Статус <code>Ready</code> не проверяет бизнес-корректность ответа. HPA не заменяет очередь, rate limit, вертикальное масштабирование или capacity planning. Resource metrics не дают трассировку причины задержки.</p>\n<p>Манифест не учитывает admission webhook, LimitRange, ResourceQuota, PodDisruptionBudget, topology spread, NetworkPolicy и правила конкретного service mesh. Эти механизмы могут изменить effective configuration или доступность. <code>publishNotReadyAddresses: true</code> меняет обычную семантику готовых endpoints, поэтому вывод о трафике нужно сверять с фактическим Service contract.</p>\n<p>Откат делайте после сохранения ревизии и проверки зависимости. Для Deployment можно посмотреть историю и вернуть предыдущую ревизию командами ниже. Откат не исправляет неверный readiness endpoint и не освобождает уже занятый Node; после него снова проверьте conditions, endpoints и rollout status.</p>\n<pre><code>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</code></pre>\n<p><strong>Критерий готовности.</strong> Изменение готово, если известны image digest и владелец состояния, новый Pod проходит startup/readiness по смыслу приложения, Service видит ожидаемые endpoints, rollout достигает доступного состояния, а HPA получает метрику с объявленным request или явно использует другой документированный target type. Для каждого отрицательного пути есть наблюдаемое действие: остановка, откат или передача конкретному владельцу. Если команда видит только <code>Running</code>, но не может показать condition, endpoint и источник метрики, причина ещё не доказана.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://kubernetes.io/docs/tasks/run-application/update-deployment-rolling/' target='_blank' rel='noopener noreferrer'>Kubernetes: Update a Deployment Without Downtime</a> — описывает RollingUpdate, <code>maxSurge</code>, <code>maxUnavailable</code>, stalled rollout и rollback.</li><li><a href='https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/' target='_blank' rel='noopener noreferrer'>Kubernetes: Configure Liveness, Readiness and Startup Probes</a> — фиксирует поведение readiness для Service traffic и роль startup probe.</li><li><a href='https://kubernetes.io/docs/concepts/services-networking/endpoint-slices/' target='_blank' rel='noopener noreferrer'>Kubernetes: EndpointSlices</a> — описывает условия <code>ready</code>, <code>serving</code>, <code>terminating</code> и роль EndpointSlice в маршрутизации.</li><li><a href='https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' target='_blank' rel='noopener noreferrer'>Kubernetes: Resource Management for Pods and Containers</a> — объясняет requests, limits, scheduling, CPU throttling и memory limit.</li><li><a href='https://kubernetes.io/docs/concepts/workloads/autoscaling/horizontal-pod-autoscale/' target='_blank' rel='noopener noreferrer'>Kubernetes: Horizontal Pod Autoscaling</a> — определяет utilization относительно request, отсутствие действия при missing request и конфликт фиксированного <code>spec.replicas</code> с HPA.</li></ul>"
|
||
}
|